@xulthekl/team-flow 0.63.0 → 0.65.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/.github/workflows/ci.yml +2 -0
- package/CHANGELOG.md +57 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/agents/architecture-design.md +1 -1
- package/agents/architecture-reviewer.md +2 -2
- package/docs/README_en.md +1 -1
- package/docs/state-machine.md +4 -1
- package/docs/team-flow /344/275/277/347/224/250/350/257/264/346/230/216/357/274/210/347/240/224/345/217/221/345/233/242/351/230/237/347/211/210/357/274/211.md" +2 -2
- 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/guard/checks/_fs-utils.mjs +18 -0
- package/scripts/guard/checks/arch-design-light.mjs +39 -0
- package/scripts/guard/checks/arch-gate-exemptions.mjs +5 -3
- package/scripts/guard/checks/arch-merged-light.mjs +67 -0
- package/scripts/guard/checks/arch-readiness.mjs +1 -1
- package/scripts/guard/checks/arch-snapshot-light.mjs +30 -0
- package/scripts/guard/checks/arch-snapshot.mjs +5 -3
- package/scripts/guard/checks/artifacts-planned.mjs +38 -0
- package/scripts/guard/checks/compound-writeback-light.mjs +45 -0
- package/scripts/guard/checks/cross-change-consistency-light.mjs +75 -0
- package/scripts/guard/checks/direct-short-path.mjs +52 -0
- package/scripts/guard/checks/direct-test-result.mjs +30 -0
- package/scripts/guard/checks/execution-plan-ready.mjs +7 -1
- package/scripts/guard/checks/execution-reviews-passed-light.mjs +28 -0
- package/scripts/guard/checks/lightweight-completion-evidence.mjs +27 -0
- package/scripts/guard/checks/specs-merged.mjs +25 -1
- package/scripts/guard/checks/test-matrix-complete.mjs +27 -1
- package/scripts/guard/checks/test-matrix-ready.mjs +28 -1
- package/scripts/guard/checks/test-merged-light.mjs +36 -0
- package/scripts/guard/guard.mjs +102 -12
- package/scripts/infer-workflow.mjs +35 -4
- package/scripts/lib/arch-merge.mjs +404 -54
- package/scripts/lib/arch-parse.mjs +5 -2
- package/scripts/lib/arch-registry.mjs +523 -0
- package/scripts/lib/arch-scan-code.mjs +518 -0
- package/scripts/lib/cmd-arch.mjs +9 -1
- package/scripts/lib/cmd-doctor.mjs +1 -1
- package/scripts/lib/cmd-execution.mjs +44 -1
- package/scripts/lib/cmd-state.mjs +94 -4
- package/scripts/lib/config-loader.mjs +20 -0
- package/scripts/lib/execution-plan.mjs +3 -1
- package/scripts/lib/state-loader.mjs +43 -0
- package/scripts/lib/surface-scan.mjs +156 -0
- package/scripts/lib/test-merge.mjs +10 -2
- package/scripts/team-flow.mjs +6 -3
- package/skills/architecture-design/SKILL.md +29 -11
- package/skills/architecture-design/chapters/ch04-entity-to-aggregate.md +18 -7
- package/skills/architecture-design/chapters/ch06-integration.md +12 -3
- package/skills/architecture-design/glossary.md +5 -1
- package/skills/architecture-design/references/adr-templates.md +56 -0
- package/skills/architecture-design/references/context-map-8.md +47 -0
- package/skills/architecture-design/references/ddd-evented-playbook.md +41 -0
- package/skills/architecture-design/references/s3.5-architecture-template.md +36 -4
- package/skills/architecture-design/references/s3.5-loading-protocol.md +5 -4
- package/skills/architecture-design/references/s3.5-product-architecture.md +6 -6
- package/skills/architecture-design/templates/architecture.md +20 -0
- package/skills/ce-compound/references/concepts-vocabulary.md +1 -1
- package/skills/ce-compound/references/full-mode-workflow.md +2 -2
- package/skills/ce-compound/references/lightweight-mode.md +1 -1
- package/skills/clean-code/SKILL.md +1 -1
- package/skills/jarvis/SKILL.md +2 -0
- package/skills/release-archivist/SKILL.md +60 -13
- package/skills/release-archivist/references/closing-procedures.md +1 -1
- package/skills/session-handoff/SKILL.md +1 -0
- package/skills/test-strategy/SKILL.md +1 -1
- package/skills/workflow-orchestrator/SKILL.md +2 -2
- package/skills/workflow-start/SKILL.md +63 -5
- package/skills/workflow-start/references/routing-rules.md +4 -4
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
extractTablesFromSql, readFrontmatter, PATH_CELL_RE,
|
|
27
27
|
} from './arch-parse.mjs';
|
|
28
28
|
import { detectWorkspaceRoot, getGitRoot, parsePorcelainPaths } from './git-utils.mjs';
|
|
29
|
+
import {
|
|
30
|
+
emptyRegistry, loadRegistry, saveRegistry, registryPath,
|
|
31
|
+
validateDelta, applyDelta, seedRegistry, canonicalizeRegistry,
|
|
32
|
+
normalizeAggregateRow,
|
|
33
|
+
} from './arch-registry.mjs';
|
|
29
34
|
|
|
30
35
|
const MARKER_BEGIN = '<!-- arch:current-state:begin -->';
|
|
31
36
|
const MARKER_END = '<!-- arch:current-state:end -->';
|
|
@@ -65,7 +70,7 @@ function releaseLock(lockPath) {
|
|
|
65
70
|
* 全局文档维护 marker 区(当前态:BC/聚合注册表)+ 演进日志段。
|
|
66
71
|
* marker 区重建 = 所有已合并 change 增量的投影(按聚合 id upsert,非原地编辑)。
|
|
67
72
|
*/
|
|
68
|
-
function mergeArchitecture(changeDir, archDir, globalArchDir, changeName, projectRoot) {
|
|
73
|
+
function mergeArchitecture(changeDir, archDir, globalArchDir, changeName, projectRoot, registry = null) {
|
|
69
74
|
const srcPath = join(archDir, 'architecture.md');
|
|
70
75
|
const dstPath = join(globalArchDir, 'ARCHITECTURE.md');
|
|
71
76
|
if (!existsSync(srcPath)) return { merged: false, reason: 'no-source' };
|
|
@@ -93,7 +98,30 @@ function mergeArchitecture(changeDir, archDir, globalArchDir, changeName, projec
|
|
|
93
98
|
|
|
94
99
|
const existing = existsSync(dstPath) ? readFileSync(dstPath, 'utf-8') : '';
|
|
95
100
|
// v0.53.0 §108 R8:marker 区 = **已合并 change 的聚合全集投影**(非仅当前 change)
|
|
96
|
-
|
|
101
|
+
// O8 双路径:registry 非 null(delta 路径)→ marker 直接从 registry 渲染(单一真相源,
|
|
102
|
+
// §5.4-1「来源列由 source 渲染」);null(legacy 软着陆路径)→ 既有重算逻辑不变。
|
|
103
|
+
let allAggregates;
|
|
104
|
+
let seeded = 0;
|
|
105
|
+
let registryMode = false;
|
|
106
|
+
const deltaMissing = [];
|
|
107
|
+
if (registry) {
|
|
108
|
+
registryMode = true;
|
|
109
|
+
allAggregates = registry.aggregates;
|
|
110
|
+
// 行为等价保护(软着陆):current change 的聚合若 delta 未登记 → 机械补种 + WARN(不静默)
|
|
111
|
+
// P3 C-2:补种行经 normalize(context→bc + events 缺省),防落盘行违反 schema
|
|
112
|
+
for (const a of aggregates) {
|
|
113
|
+
if (!registry.aggregates.some(x => x.id === a.id)) {
|
|
114
|
+
registry.aggregates.push(normalizeAggregateRow(a, { source: `change:${changeName}` }));
|
|
115
|
+
deltaMissing.push(a.id);
|
|
116
|
+
seeded++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (deltaMissing.length > 0) {
|
|
120
|
+
console.warn(` [WARN] delta 未登记本 change 聚合,已按 legacy 语义机械补种:${deltaMissing.join(', ')} — 请语义段核对 .arch-delta.json`);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
({ list: allAggregates, seeded } = collectAllAggregates(projectRoot, changeName, aggregates));
|
|
124
|
+
}
|
|
97
125
|
const currentStateSection = buildCurrentStateSection(allAggregates);
|
|
98
126
|
|
|
99
127
|
// marker 区替换(代码独占写):存在则替换,缺失则插入
|
|
@@ -122,6 +150,8 @@ function mergeArchitecture(changeDir, archDir, globalArchDir, changeName, projec
|
|
|
122
150
|
file: 'ARCHITECTURE.md',
|
|
123
151
|
aggregates: aggregates.map(a => a.id),
|
|
124
152
|
projected: allAggregates.length,
|
|
153
|
+
seeded, // P0-B'-① 快照只读 seed 条数;registry 路径 = current 补种条数
|
|
154
|
+
...(registryMode ? { registry: true } : {}),
|
|
125
155
|
...(aggregates.length === 0 ? { reason: 'no-aggregate-delta' } : {}),
|
|
126
156
|
};
|
|
127
157
|
}
|
|
@@ -160,14 +190,130 @@ function collectAllAggregates(projectRoot, changeName, currentAggregates) {
|
|
|
160
190
|
}
|
|
161
191
|
// 当前 change 覆盖同 id 项(本次合并权威)
|
|
162
192
|
for (const a of currentAggregates) collected.set(a.id, { ...a, source: `change:${changeName}` });
|
|
163
|
-
|
|
193
|
+
// P0-B'-① 最小替代:快照只读 seed,只补缺(最低优先级),见 seedFromSnapshots
|
|
194
|
+
const seeded = seedFromSnapshots(projectRoot, collected);
|
|
195
|
+
return { list: [...collected.values()], seeded };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* P0-B'-① 最小替代:从产品级快照 `docs/architecture/iterations/<v>/architecture.md` 只读 seed 聚合。
|
|
200
|
+
*
|
|
201
|
+
* 修 R-a(投影域缺基线):原投影域只有 changes/,产品级基线聚合在首次 change 回写后被
|
|
202
|
+
* 整片排除(emp-auth 实测 11+6 → 1)。本函数把快照聚合补进投影,但**每次 merge 重算、
|
|
203
|
+
* 不落盘**(治标——快照缺失/被归档时行仍会消失;治本 = P0-B'' 的 registry 持久化)。
|
|
204
|
+
*
|
|
205
|
+
* 设计要点(docs/plan/ddd-purity-and-arch-merge-design.md v1.4 §5.7 / §5.3 分档表 / 红牌 7):
|
|
206
|
+
* - **provenance 分档**:仅 `reverse-engineered`(实测态反推)可 seed;
|
|
207
|
+
* `forward-designed`(预测态)禁止进事实源;`mixed` 快照无节级标记 → 保守不 seed;
|
|
208
|
+
* frontmatter 缺失同样保守跳过。跳过一律 log,不静默。
|
|
209
|
+
* - **优先级最低**:只补 collected 中不存在的 id——closed/current change 行恒优先
|
|
210
|
+
* (快照是基线兜底,不是最新权威);多个快照间按版本号升序合并、后序覆盖前序。
|
|
211
|
+
* - **source 标记** `base@snapshot-backfill`(§5.3 规定值):当前态表内可辨识;
|
|
212
|
+
* `extractAggregates` 回读时非 `change:` 前缀会置空 → conflictCheck 不把 seed 行
|
|
213
|
+
* 当作 change 所有权声明(后续 change 仍可正常声明同 id,与存量行为一致)。
|
|
214
|
+
*/
|
|
215
|
+
function seedFromSnapshots(projectRoot, collected) {
|
|
216
|
+
const iterDir = join(projectRoot, 'docs', 'architecture', 'iterations');
|
|
217
|
+
if (!existsSync(iterDir)) return 0;
|
|
218
|
+
const versions = readdirSync(iterDir)
|
|
219
|
+
.filter(n => /^v\d+$/.test(n))
|
|
220
|
+
.sort((a, b) => parseInt(a.slice(1), 10) - parseInt(b.slice(1), 10));
|
|
221
|
+
const snapAgg = new Map(); // id → aggregate;升序 set,后序快照覆盖前序同 id
|
|
222
|
+
for (const v of versions) {
|
|
223
|
+
const p = join(iterDir, v, 'architecture.md');
|
|
224
|
+
if (!existsSync(p)) continue;
|
|
225
|
+
const content = readFileSync(p, 'utf-8');
|
|
226
|
+
const provenance = readFrontmatter(content).provenance;
|
|
227
|
+
if (provenance !== 'reverse-engineered') {
|
|
228
|
+
console.log(` [seed] iterations/${v}: provenance=${provenance || '(缺失)'} → 跳过(红牌 7 分档:仅 reverse-engineered 可 seed)`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const snapAggs = extractAggregates(content);
|
|
232
|
+
if (snapAggs.length === 0) {
|
|
233
|
+
// P3 MIN-5:provenance 合格但聚合段定位/解析为空 → 不静默(表头失配有 WARN,标题失配没有)
|
|
234
|
+
console.log(` [seed] iterations/${v}: provenance 合格但提取 0 条聚合 — 检查聚合注册表标题/表头`);
|
|
235
|
+
}
|
|
236
|
+
for (const a of snapAggs) {
|
|
237
|
+
snapAgg.set(a.id, { ...a, source: 'base@snapshot-backfill' });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
let seeded = 0;
|
|
241
|
+
for (const [id, a] of snapAgg) {
|
|
242
|
+
if (collected.has(id)) continue;
|
|
243
|
+
collected.set(id, a);
|
|
244
|
+
seeded++;
|
|
245
|
+
}
|
|
246
|
+
if (seeded > 0) {
|
|
247
|
+
console.log(` [seed] 快照只读 seed ${seeded} 条聚合(iterations/vN,reverse-engineered;每次 merge 重算,不落盘)`);
|
|
248
|
+
}
|
|
249
|
+
return seeded;
|
|
164
250
|
}
|
|
165
251
|
|
|
166
252
|
/** 构建当前态 marker 区内容(BC 表 + 聚合注册表,厚锚点一句话级;来源列标注真实来源 change)。 */
|
|
253
|
+
/**
|
|
254
|
+
* O8 · 首跑 seed(红牌 12:registry 首次建立必须自动 seed,语义 = 既有累积状态的等价迁移)。
|
|
255
|
+
*
|
|
256
|
+
* seed 范围 = **基线**(不含 current change——current 经 `.arch-delta.json` 或物理事实补种进入):
|
|
257
|
+
* - aggregates:closed changes 投影 + 产品级快照(复用 collectAllAggregates,currentAggregates 传空)
|
|
258
|
+
* - tables:schema-baseline.sql 既有表(上次 merge 为止的累积 DDL——本次 change 的 DDL 由
|
|
259
|
+
* syncSchemaBaseline 之后的机械补种负责,见 run())
|
|
260
|
+
* - endpoints:API-INDEX.md 既有行(上次生成的权威快照)
|
|
261
|
+
* 只补缺不覆盖(seedRegistry 语义 = 最低优先级)。返回 { registry, seeded }。
|
|
262
|
+
*/
|
|
263
|
+
function seedRegistryFromLegacy(globalArchDir, projectRoot, changeName) {
|
|
264
|
+
let reg = loadRegistry(globalArchDir);
|
|
265
|
+
if (reg) return { registry: reg, seeded: 0, existed: true };
|
|
266
|
+
|
|
267
|
+
reg = emptyRegistry();
|
|
268
|
+
reg.updated_at = new Date().toISOString();
|
|
269
|
+
let seeded = 0;
|
|
270
|
+
|
|
271
|
+
// 聚合:closed changes + 快照 seed(current 传空 → 不含本次 change)
|
|
272
|
+
// P3 C-2:注入前 normalize(context→bc 映射 + events 缺省)——否则落盘行违反自身 schema(C-1)
|
|
273
|
+
const { list: baselineAggs } = collectAllAggregates(projectRoot, changeName, []);
|
|
274
|
+
seeded += seedRegistry(reg, { aggregates: baselineAggs.map(a => normalizeAggregateRow(a)) }, 'base').seeded;
|
|
275
|
+
|
|
276
|
+
// 表:schema-baseline.sql 既有累积
|
|
277
|
+
const schemaPath = join(globalArchDir, 'schema-baseline.sql');
|
|
278
|
+
if (existsSync(schemaPath)) {
|
|
279
|
+
const names = extractTablesFromSql(readFileSync(schemaPath, 'utf-8'));
|
|
280
|
+
seeded += seedRegistry(reg, {
|
|
281
|
+
tables: names.map(n => ({ name: n, ddl_ref: 'schema-baseline.sql' })),
|
|
282
|
+
}, 'base@schema-baseline').seeded;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 端点:既有 API-INDEX.md(列 = 方法 | 路径 | 分流 | 来源)
|
|
286
|
+
const indexPath = join(globalArchDir, 'API-INDEX.md');
|
|
287
|
+
if (existsSync(indexPath)) {
|
|
288
|
+
const eps = [];
|
|
289
|
+
for (const line of readFileSync(indexPath, 'utf-8').split('\n')) {
|
|
290
|
+
const cells = parseTableRow(line);
|
|
291
|
+
if (!cells || cells.length < 4) continue;
|
|
292
|
+
const [method, path, kind, source] = cells;
|
|
293
|
+
if (!PATH_CELL_RE.test(path)) continue;
|
|
294
|
+
// P3 C-1:kind 归一化进 schema 枚举(server|feign)——legacy「分流」列装的是章节 kind
|
|
295
|
+
//(Command/Read/Query 等),非 feign 一律 server(feign 生产者见 I-7 登记,未实现前不产 feign)。
|
|
296
|
+
// P3 M-6:来源列是裸 change 目录名(不带 change: 前缀)→ 直接作归因,防 change:null 断链。
|
|
297
|
+
const change = source.startsWith('change:') ? source.slice(7)
|
|
298
|
+
: (/^[A-Za-z0-9][\w-]*$/.test(source) ? source : null);
|
|
299
|
+
eps.push({ method, path, kind: kind === 'feign' ? 'feign' : 'server', change });
|
|
300
|
+
}
|
|
301
|
+
seeded += seedRegistry(reg, { endpoints: eps }, 'base@api-index').seeded;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { registry: reg, seeded, existed: false };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 渲染 marker 当前态区。入参兼容两种来源(O8 双路径并存期):
|
|
309
|
+
* - legacy:collectAllAggregates 行(字段 context,无 status → 视作已落地)
|
|
310
|
+
* - registry:registry.aggregates 行(字段 bc + status: active|retired)
|
|
311
|
+
* 状态列由 registry 的生命周期字段驱动(红牌 6:retire 行保留可见、标已退役)。
|
|
312
|
+
*/
|
|
167
313
|
function buildCurrentStateSection(aggregates) {
|
|
168
314
|
const bcMap = new Map();
|
|
169
315
|
for (const a of aggregates) {
|
|
170
|
-
const bc = a.context || '(未标注)';
|
|
316
|
+
const bc = a.context || a.bc || '(未标注)';
|
|
171
317
|
if (!bcMap.has(bc)) bcMap.set(bc, []);
|
|
172
318
|
bcMap.get(bc).push(a);
|
|
173
319
|
}
|
|
@@ -180,7 +326,8 @@ function buildCurrentStateSection(aggregates) {
|
|
|
180
326
|
lines.push('', '### 聚合注册表');
|
|
181
327
|
lines.push('| 聚合ID | 上下文 | 根实体 | 来源 | 状态 | 关键不变量 |', '|--------|--------|--------|------|------|-----------|');
|
|
182
328
|
for (const a of aggregates) {
|
|
183
|
-
|
|
329
|
+
const status = a.status === 'retired' ? '已退役' : '已落地';
|
|
330
|
+
lines.push(`| ${a.id} | ${a.context || a.bc || '—'} | ${a.root || '—'} | ${a.source || '—'} | ${status} | ${a.invariants || '—'} |`);
|
|
184
331
|
}
|
|
185
332
|
return lines.join('\n');
|
|
186
333
|
}
|
|
@@ -295,17 +442,23 @@ function syncSchemaBaseline(changeDir, archDir, globalArchDir, changeName) {
|
|
|
295
442
|
|
|
296
443
|
/* ============ 生成式产物:PHYSICAL-MODEL / DATABASE / API-INDEX ============ */
|
|
297
444
|
|
|
298
|
-
/** Step 3: PHYSICAL-MODEL.md
|
|
299
|
-
function generatePhysicalModel(globalArchDir, schemaPath) {
|
|
445
|
+
/** Step 3: PHYSICAL-MODEL.md 生成式。O8:registry 非 null → 从 registry.tables 出(R-b 改源);null → legacy 从 schema-baseline.sql。 */
|
|
446
|
+
function generatePhysicalModel(globalArchDir, schemaPath, registry = null) {
|
|
300
447
|
const dstPath = join(globalArchDir, 'PHYSICAL-MODEL.md');
|
|
301
448
|
const header = '# Physical Model(generated by arch-merge, do not edit)\n\n';
|
|
302
|
-
|
|
449
|
+
let tables;
|
|
450
|
+
let sourceNote;
|
|
451
|
+
if (registry) {
|
|
452
|
+
tables = registry.tables.filter(t => t.status !== 'retired').map(t => t.name);
|
|
453
|
+
sourceNote = '> 从 registry.tables[] 自动生成(O8 改源,§5.4-5;source/生命周期为权威)。';
|
|
454
|
+
} else if (!existsSync(schemaPath)) {
|
|
303
455
|
writeFile(dstPath, header + '> schema-baseline.sql 不存在,暂无物理模型。\n', 'utf-8');
|
|
304
456
|
return { generated: true, file: 'PHYSICAL-MODEL.md', tables: 0 };
|
|
457
|
+
} else {
|
|
458
|
+
tables = extractTablesFromSql(readFileSync(schemaPath, 'utf-8'));
|
|
459
|
+
sourceNote = '> 从 schema-baseline.sql 自动生成(P2 机器管结构,DDL 为事实源;legacy 路径)。';
|
|
305
460
|
}
|
|
306
|
-
const
|
|
307
|
-
const tables = extractTablesFromSql(sql);
|
|
308
|
-
const lines = [header, '> 从 schema-baseline.sql 自动生成(P2 机器管结构,DDL 为事实源)。\n'];
|
|
461
|
+
const lines = [header, sourceNote + '\n'];
|
|
309
462
|
for (const t of tables) {
|
|
310
463
|
lines.push(`### ${t}`, '', '| 字段 | 类型 | 约束 |', '|------|------|------|', '', '');
|
|
311
464
|
}
|
|
@@ -313,20 +466,29 @@ function generatePhysicalModel(globalArchDir, schemaPath) {
|
|
|
313
466
|
return { generated: true, file: 'PHYSICAL-MODEL.md', tables: tables.length };
|
|
314
467
|
}
|
|
315
468
|
|
|
316
|
-
/** Step 3b: DATABASE.md
|
|
317
|
-
function generateDatabase(globalArchDir, schemaPath) {
|
|
469
|
+
/** Step 3b: DATABASE.md 生成式。O8:registry 非 null → 从 registry.tables 出;null → legacy。 */
|
|
470
|
+
function generateDatabase(globalArchDir, schemaPath, registry = null) {
|
|
318
471
|
const dstPath = join(globalArchDir, 'DATABASE.md');
|
|
319
472
|
const header = '# Database(generated by arch-merge, do not edit)\n\n';
|
|
320
|
-
|
|
473
|
+
let tables;
|
|
474
|
+
let sourceCol;
|
|
475
|
+
let sourceNote;
|
|
476
|
+
if (registry) {
|
|
477
|
+
tables = registry.tables.filter(t => t.status !== 'retired').map(t => t);
|
|
478
|
+
sourceCol = 'registry.tables[]';
|
|
479
|
+
sourceNote = '> 从 registry.tables[] 再生成(O8 改源,§5.4-5;schema-baseline.sql 仍单向 append 作外部兼容,doctor 校验漂移)。';
|
|
480
|
+
} else if (!existsSync(schemaPath)) {
|
|
321
481
|
writeFile(dstPath, header + '> schema-baseline.sql 不存在,暂无库表说明。\n', 'utf-8');
|
|
322
482
|
return { generated: true, file: 'DATABASE.md', tables: 0 };
|
|
483
|
+
} else {
|
|
484
|
+
tables = extractTablesFromSql(readFileSync(schemaPath, 'utf-8')).map(n => ({ name: n }));
|
|
485
|
+
sourceCol = 'schema-baseline.sql';
|
|
486
|
+
sourceNote = '> 从 schema-baseline.sql + PHYSICAL-MODEL.md + changelog/ 再生成(v0.14 §62.2,构造上消除漂移;legacy 路径)。';
|
|
323
487
|
}
|
|
324
|
-
const
|
|
325
|
-
const tables = extractTablesFromSql(sql);
|
|
326
|
-
const lines = [header, '> 从 schema-baseline.sql + PHYSICAL-MODEL.md + changelog/ 再生成(v0.14 §62.2,构造上消除漂移)。\n'];
|
|
488
|
+
const lines = [header, sourceNote + '\n'];
|
|
327
489
|
lines.push('| 表名 | 来源 | 状态 |', '|------|------|------|');
|
|
328
490
|
for (const t of tables) {
|
|
329
|
-
lines.push(`| ${t} |
|
|
491
|
+
lines.push(`| ${t.name} | ${sourceCol} | 实际落地 |`);
|
|
330
492
|
}
|
|
331
493
|
writeFile(dstPath, lines.join('\n'), 'utf-8');
|
|
332
494
|
return { generated: true, file: 'DATABASE.md', tables: tables.length };
|
|
@@ -357,7 +519,7 @@ function isChangeClosed(changeRoot) {
|
|
|
357
519
|
* ③ 产出量对账(§104.2.2)——生成器原只判「源文件在不在」,从不判「吃进去多少 / 原文多少」,
|
|
358
520
|
* 形态失配因此长期隐性。
|
|
359
521
|
*/
|
|
360
|
-
function generateApiIndex(projectRoot, globalArchDir, changeName) {
|
|
522
|
+
function generateApiIndex(projectRoot, globalArchDir, changeName, registry = null) {
|
|
361
523
|
const changesDir = join(projectRoot, 'changes');
|
|
362
524
|
const dstPath = join(globalArchDir, 'API-INDEX.md');
|
|
363
525
|
const header = '# API Index(generated by arch-merge, do not edit)\n\n';
|
|
@@ -422,6 +584,50 @@ function generateApiIndex(projectRoot, globalArchDir, changeName) {
|
|
|
422
584
|
return { generated: false, reason: 'endpoint-parse-mismatch', failures, warnings, file: 'API-INDEX.md', endpoints: endpoints.length };
|
|
423
585
|
}
|
|
424
586
|
|
|
587
|
+
// ---- O8 registry 路径(§5.4-5 改源 + 物理事实机械补种)----
|
|
588
|
+
if (registry) {
|
|
589
|
+
// P3 M-2:registry 与扫描**双空**且既有文件存在 → 不覆写(对齐 legacy 空保护意图——
|
|
590
|
+
// 防「新机制把历史端点表清零」;registry 单边空而扫描有货则照常补种渲染)
|
|
591
|
+
if (registry.endpoints.filter(e => e.status !== 'retired').length === 0
|
|
592
|
+
&& endpoints.length === 0 && existsSync(dstPath)) {
|
|
593
|
+
return {
|
|
594
|
+
generated: false,
|
|
595
|
+
reason: 'empty-registry-skip-overwrite',
|
|
596
|
+
warnings: [...warnings, 'registry 端点与扫描结果均为空,已保留既有 API-INDEX.md 未被覆盖(P3 M-2 空保护)'],
|
|
597
|
+
file: 'API-INDEX.md',
|
|
598
|
+
endpoints: 0,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// 机械补种:扫描到(物理事实 = api.md)而 registry 缺失的端点 → 补 + WARN(不静默;
|
|
602
|
+
// 与表的 DDL 补种对称——delta 的 endpoint 条目负责富化/retire 等语义意图)
|
|
603
|
+
const missing = [];
|
|
604
|
+
for (const e of endpoints) {
|
|
605
|
+
const key = `${e.method}:${e.path}`;
|
|
606
|
+
if (!registry.endpoints.some(x => `${x.method}:${x.path}` === key)) {
|
|
607
|
+
// P3 C-1/I-7:kind 归一化进 schema 枚举——e.kind 是章节 kind(Command/Read/Query…),
|
|
608
|
+
// 非 feign 一律 server;feign 生产者未实现(B10 登记,见 §12 I-7 处置)
|
|
609
|
+
registry.endpoints.push({
|
|
610
|
+
method: e.method, path: e.path, kind: e.kind === 'feign' ? 'feign' : 'server',
|
|
611
|
+
change: e.source, status: 'active', source: `change:${e.source}`,
|
|
612
|
+
});
|
|
613
|
+
missing.push(key);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (missing.length > 0) {
|
|
617
|
+
warnings.push(`delta/api 未登记端点,已按扫描结果机械补种:${missing.slice(0, 10).join(', ')}${missing.length > 10 ? ' …' : ''}`);
|
|
618
|
+
}
|
|
619
|
+
const regEps = [...registry.endpoints]
|
|
620
|
+
.filter(e => e.status !== 'retired')
|
|
621
|
+
.sort((a, b) => `${a.method}${a.path}`.localeCompare(`${b.method}${b.path}`));
|
|
622
|
+
const lines = [header, '> 从 registry.endpoints[] 生成(O8 改源 §5.4-5;source 含 base/change 归属,kind 分流 = server|feign 审计 B10)。\n'];
|
|
623
|
+
lines.push('| 方法 | 路径 | 分流 | 来源 |', '|------|------|------|------|');
|
|
624
|
+
for (const e of regEps) {
|
|
625
|
+
lines.push(`| ${e.method} | ${e.path} | ${e.kind || 'server'} | ${e.source || '—'} |`);
|
|
626
|
+
}
|
|
627
|
+
writeFile(dstPath, lines.join('\n'), 'utf-8');
|
|
628
|
+
return { generated: true, file: 'API-INDEX.md', endpoints: regEps.length, warnings, failures: [] };
|
|
629
|
+
}
|
|
630
|
+
|
|
425
631
|
// 空结果拒绝覆盖(数据丢失保护,**保留**)+ 可见化(§104 R4:不得静默)
|
|
426
632
|
if (endpoints.length === 0 && existsSync(dstPath)) {
|
|
427
633
|
return {
|
|
@@ -433,7 +639,7 @@ function generateApiIndex(projectRoot, globalArchDir, changeName) {
|
|
|
433
639
|
};
|
|
434
640
|
}
|
|
435
641
|
|
|
436
|
-
const lines = [header, '> 扫描 changes/**/api.md + domains/* 归一化端点重建(v0.14 §62.4
|
|
642
|
+
const lines = [header, '> 扫描 changes/**/api.md + domains/* 归一化端点重建(v0.14 §62.4;legacy 路径)。\n'];
|
|
437
643
|
lines.push('| 方法 | 路径 | 分流 | 来源 |', '|------|------|------|------|');
|
|
438
644
|
for (const e of endpoints) {
|
|
439
645
|
// method 恒非空(§105.2.4「无方法即跳过」保证),故不再写 `—` 占位——
|
|
@@ -536,28 +742,36 @@ function conflictCheck(changeDir, archDir, globalArchDir, changeName) {
|
|
|
536
742
|
/* ============ INDEX 确定性统计 ============ */
|
|
537
743
|
|
|
538
744
|
/** Step 7: INDEX.md 确定性统计(表数解析 schema-baseline,端点数解析 API-INDEX 结构,非垃圾进垃圾出)。 */
|
|
539
|
-
function updateIndex(globalArchDir, changeName) {
|
|
745
|
+
function updateIndex(globalArchDir, changeName, registry = null) {
|
|
540
746
|
const indexPath = join(globalArchDir, 'INDEX.md');
|
|
541
747
|
const date = new Date().toISOString().slice(0, 10);
|
|
542
748
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
749
|
+
// O8 改源(§5.4-5:updateIndex 是 R-b 的隐蔽传播路径,必须同批改源——
|
|
750
|
+
// 否则出现「PHYSICAL-MODEL 修好了、INDEX 还写着 1 表」的自相矛盾)
|
|
751
|
+
const bcCount = registry
|
|
752
|
+
? new Set(registry.aggregates.filter(a => a.status !== 'retired').map(a => a.bc || a.context).filter(Boolean)).size
|
|
753
|
+
: (() => {
|
|
754
|
+
const p = join(globalArchDir, 'ARCHITECTURE.md');
|
|
755
|
+
if (!existsSync(p)) return null;
|
|
756
|
+
const agg = extractAggregates(readFileSync(p, 'utf-8'));
|
|
757
|
+
return new Set(agg.map(a => a.context)).size;
|
|
758
|
+
})();
|
|
759
|
+
|
|
760
|
+
const tableCount = registry
|
|
761
|
+
? registry.tables.filter(t => t.status !== 'retired').length
|
|
762
|
+
: (() => {
|
|
763
|
+
const p = join(globalArchDir, 'schema-baseline.sql');
|
|
764
|
+
if (!existsSync(p)) return null;
|
|
765
|
+
return extractTablesFromSql(readFileSync(p, 'utf-8')).length;
|
|
766
|
+
})();
|
|
767
|
+
|
|
768
|
+
const endpointCount = registry
|
|
769
|
+
? registry.endpoints.filter(e => e.status !== 'retired').length
|
|
770
|
+
: (() => {
|
|
771
|
+
const p = join(globalArchDir, 'API-INDEX.md');
|
|
772
|
+
if (!existsSync(p)) return null;
|
|
773
|
+
return extractEndpoints(readFileSync(p, 'utf-8')).length;
|
|
774
|
+
})();
|
|
561
775
|
|
|
562
776
|
const lines = [
|
|
563
777
|
`# Architecture Index`,
|
|
@@ -584,16 +798,29 @@ function updateIndex(globalArchDir, changeName) {
|
|
|
584
798
|
|
|
585
799
|
/* ============ Step 1: 预检 ============ */
|
|
586
800
|
|
|
587
|
-
function preCheck(changeDir, archDir, projectRoot) {
|
|
801
|
+
function preCheck(changeDir, archDir, projectRoot, light = false) {
|
|
588
802
|
const conflicts = [];
|
|
589
803
|
if (!existsSync(archDir)) {
|
|
590
804
|
return { pass: false, conflicts: ['architecture/ directory not found in change'] };
|
|
591
805
|
}
|
|
592
|
-
|
|
806
|
+
// v0.64.0(§4 D8,--light):light 模式持久源只要求 architecture.md(聚合注册行 +
|
|
807
|
+
// 演进日志段 = marker 区投影唯一输入);api.md/database.md 触及时才要求(调用方按
|
|
808
|
+
// touched surface 判定,此处仅对已存在文件做非空校验——缺失即该 sink 无增量,
|
|
809
|
+
// 生成式重建会安全保留旧表而非抹除)。full 模式维持三件套硬要求。
|
|
810
|
+
const required = light ? ['architecture.md'] : ['architecture.md', 'database.md', 'api.md'];
|
|
811
|
+
for (const f of required) {
|
|
593
812
|
const fp = join(archDir, f);
|
|
594
813
|
if (!existsSync(fp)) conflicts.push(`architecture/${f} missing`);
|
|
595
814
|
else if (readFileSync(fp, 'utf-8').trim().length === 0) conflicts.push(`architecture/${f} empty`);
|
|
596
815
|
}
|
|
816
|
+
if (light) {
|
|
817
|
+
for (const f of ['database.md', 'api.md']) {
|
|
818
|
+
const fp = join(archDir, f);
|
|
819
|
+
if (existsSync(fp) && readFileSync(fp, 'utf-8').trim().length === 0) {
|
|
820
|
+
conflicts.push(`architecture/${f} empty (present but blank — delete it or fill the delta)`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
597
824
|
return { pass: conflicts.length === 0, conflicts };
|
|
598
825
|
}
|
|
599
826
|
|
|
@@ -705,6 +932,7 @@ export function run(args = {}) {
|
|
|
705
932
|
for (let i = 0; i < args.length; i++) {
|
|
706
933
|
if (args[i] === '--project-root' && args[i + 1] && !args[i + 1].startsWith('--')) parsed.projectRoot = args[++i];
|
|
707
934
|
else if (args[i] === '--dry-run') parsed.dryRun = true;
|
|
935
|
+
else if (args[i] === '--light') parsed.light = true;
|
|
708
936
|
else if (!args[i].startsWith('--')) parsed._.push(args[i]);
|
|
709
937
|
}
|
|
710
938
|
args = parsed;
|
|
@@ -712,8 +940,9 @@ export function run(args = {}) {
|
|
|
712
940
|
|
|
713
941
|
const changeDir = args._?.[0] || args.changeDir;
|
|
714
942
|
const dryRun = args.dryRun || false;
|
|
943
|
+
const light = args.light || false; // v0.64.0 §4 D8:planned 轻回写入口
|
|
715
944
|
if (!changeDir) {
|
|
716
|
-
console.error('Usage: tf arch-merge <change-dir> [--project-root <path>] [--dry-run]');
|
|
945
|
+
console.error('Usage: tf arch-merge <change-dir> [--project-root <path>] [--dry-run] [--light]');
|
|
717
946
|
process.exit(1);
|
|
718
947
|
}
|
|
719
948
|
|
|
@@ -733,6 +962,7 @@ export function run(args = {}) {
|
|
|
733
962
|
console.log(` Source: ${archDir}`);
|
|
734
963
|
console.log(` Target: ${globalArchDir}`);
|
|
735
964
|
if (dryRun) console.log(` Mode: DRY-RUN`);
|
|
965
|
+
if (light) console.log(` Mode: LIGHT(minimal persistent source: architecture.md + present deltas;同 sink 同生成器)`);
|
|
736
966
|
|
|
737
967
|
// 写锁(硬门禁 3)
|
|
738
968
|
const lockPath = acquireLock(projectRoot);
|
|
@@ -740,7 +970,7 @@ export function run(args = {}) {
|
|
|
740
970
|
if (!existsSync(globalArchDir)) makeDir(globalArchDir);
|
|
741
971
|
|
|
742
972
|
// Step 1: 预检
|
|
743
|
-
const preCheckResult = preCheck(changeDir, archDir, projectRoot);
|
|
973
|
+
const preCheckResult = preCheck(changeDir, archDir, projectRoot, light);
|
|
744
974
|
if (!preCheckResult.pass) {
|
|
745
975
|
console.error(' Pre-check FAILED:');
|
|
746
976
|
for (const c of preCheckResult.conflicts) console.error(` - ${c}`);
|
|
@@ -770,28 +1000,139 @@ export function run(args = {}) {
|
|
|
770
1000
|
};
|
|
771
1001
|
}
|
|
772
1002
|
|
|
773
|
-
// Step
|
|
774
|
-
|
|
1003
|
+
// ---- Step 1c(O8 · §5.4-7 软着陆双路径):.arch-delta.json 存在 → registry 路径 ----
|
|
1004
|
+
// seed 先于 delta(R3-3):首跑 seed 基线(closed changes + 快照 + 既有 DDL/API-INDEX),
|
|
1005
|
+
// 再消费 delta;缺失 → legacy 重算路径(软着陆期合法,v0.66.0 硬切)。
|
|
1006
|
+
const deltaPath = join(archDir, '.arch-delta.json');
|
|
1007
|
+
let registry = null;
|
|
1008
|
+
const registryWarnings = [];
|
|
1009
|
+
if (existsSync(deltaPath)) {
|
|
1010
|
+
const seedRes = seedRegistryFromLegacy(globalArchDir, projectRoot, changeName);
|
|
1011
|
+
registry = seedRes.registry;
|
|
1012
|
+
if (!seedRes.existed && seedRes.seeded > 0) {
|
|
1013
|
+
console.log(` [registry] 首跑 seed ${seedRes.seeded} 条基线(schema 冻结版含 events/context_map 预留)`);
|
|
1014
|
+
}
|
|
1015
|
+
let delta;
|
|
1016
|
+
try {
|
|
1017
|
+
delta = JSON.parse(readFileSync(deltaPath, 'utf-8'));
|
|
1018
|
+
} catch (e) {
|
|
1019
|
+
abort(`.arch-delta.json 解析失败(${e.message})——语义段制品损坏,禁止静默继续`);
|
|
1020
|
+
}
|
|
1021
|
+
const dvErrs = validateDelta(delta, { changeDir });
|
|
1022
|
+
const structural = dvErrs.filter(e => !e.startsWith('EVIDENCE:'));
|
|
1023
|
+
const evidenceIssues = dvErrs.filter(e => e.startsWith('EVIDENCE:'));
|
|
1024
|
+
if (structural.length > 0) {
|
|
1025
|
+
abort(`.arch-delta.json 结构校验失败(结构坏无法安全消费):\n - ${structural.join('\n - ')}\n(A8 机械层;evidence 内容正确性另属人审)`);
|
|
1026
|
+
}
|
|
1027
|
+
// P3 I-2:软着陆期 evidence 缺失/引用文件不存在 → WARN + 条目照常消费(设计 §5.4-4c 与
|
|
1028
|
+
// ①-pre SKILL 的契约);v0.66.0 硬切时此处改 abort。结构错误与 evidence 错误严格分流。
|
|
1029
|
+
if (evidenceIssues.length > 0) {
|
|
1030
|
+
registryWarnings.push(...evidenceIssues.map(e =>
|
|
1031
|
+
`${changeName}:[软着陆] ${e.replace(/^EVIDENCE:\s*/, '')} — 条目照常消费(v0.66.0 起将 abort)`));
|
|
1032
|
+
}
|
|
1033
|
+
const applyRes = applyDelta(registry, delta, { changeName, hard: false });
|
|
1034
|
+
if (applyRes.violations.length > 0) {
|
|
1035
|
+
abort(`delta 违约:\n - ${applyRes.violations.join('\n - ')}`);
|
|
1036
|
+
}
|
|
1037
|
+
registry = applyRes.registry;
|
|
1038
|
+
registry.updated_at = new Date().toISOString();
|
|
1039
|
+
registryWarnings.push(...applyRes.warnings.map(w => `${changeName}:${w}`));
|
|
1040
|
+
console.log(` [registry] delta 应用 ${applyRes.applied.length} 条(软着陆 hard=false;warnings=${applyRes.warnings.length})`);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Step 2: ARCHITECTURE.md(当前态 upsert)——registry 非 null 时 marker 从 registry 渲染
|
|
1044
|
+
const archResult = mergeArchitecture(changeDir, archDir, globalArchDir, changeName, projectRoot, registry);
|
|
775
1045
|
|
|
776
1046
|
// Step 4: schema-baseline.sql(幂等追加)——先同步 DDL,供 PHYSICAL-MODEL/DATABASE 生成消费
|
|
777
1047
|
const schemaResult = syncSchemaBaseline(changeDir, archDir, globalArchDir, changeName);
|
|
778
1048
|
|
|
779
|
-
//
|
|
780
|
-
|
|
1049
|
+
// O8 · 表的物理事实机械补种(红牌 12:seed 语义 = 既有 append 语义的等价物):
|
|
1050
|
+
// 本次 change 的 DDL 表若 registry 缺失 → 补入(source=change:<name>)+ WARN。
|
|
1051
|
+
// delta 的 table 条目负责富化/retire 等语义意图;DDL 是物理事实,不依赖 LLM 枚举。
|
|
1052
|
+
if (registry) {
|
|
1053
|
+
const ddlDir = join(archDir, 'sql', 'ddl');
|
|
1054
|
+
if (existsSync(ddlDir)) {
|
|
1055
|
+
const names = [];
|
|
1056
|
+
for (const f of readdirSync(ddlDir).filter(x => x.endsWith('.sql'))) {
|
|
1057
|
+
names.push(...extractTablesFromSql(readFileSync(join(ddlDir, f), 'utf-8')));
|
|
1058
|
+
}
|
|
1059
|
+
const before = registry.tables.length;
|
|
1060
|
+
seedRegistry(registry, { tables: names.map(n => ({ name: n, ddl_ref: `changes/${changeName}/architecture/sql/ddl` })) }, `change:${changeName}`);
|
|
1061
|
+
const added = registry.tables.length - before;
|
|
1062
|
+
if (added > 0) {
|
|
1063
|
+
registryWarnings.push(`${changeName}:DDL 补种 ${added} 张表进 registry(delta 未登记的物理事实——请语义段核对 .arch-delta.json)`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
781
1067
|
|
|
782
|
-
// Step
|
|
783
|
-
const
|
|
1068
|
+
// Step 3: PHYSICAL-MODEL.md(生成式;registry 模式从 registry.tables,legacy 从 schema-baseline.sql)
|
|
1069
|
+
const pmResult = generatePhysicalModel(globalArchDir, schemaPath, registry);
|
|
1070
|
+
|
|
1071
|
+
// Step 3b: DATABASE.md(生成式;同上双源)
|
|
1072
|
+
const dbResult = generateDatabase(globalArchDir, schemaPath, registry);
|
|
784
1073
|
|
|
785
1074
|
// Step 5: 归档
|
|
786
1075
|
const archiveResult = archiveChangeScripts(changeDir, archDir, globalArchDir, changeName);
|
|
787
1076
|
|
|
788
|
-
// Step 6: API-INDEX.md
|
|
789
|
-
const apiResult = generateApiIndex(projectRoot, globalArchDir, changeName);
|
|
1077
|
+
// Step 6: API-INDEX.md(生成式;registry 模式从 registry.endpoints + 扫描补种)
|
|
1078
|
+
const apiResult = generateApiIndex(projectRoot, globalArchDir, changeName, registry);
|
|
1079
|
+
|
|
1080
|
+
// Step 7: INDEX.md(O8 改源:registry 模式计数取 registry,防「PM 修好 INDEX 还写 1 表」)
|
|
1081
|
+
const indexResult = updateIndex(globalArchDir, changeName, registry);
|
|
1082
|
+
|
|
1083
|
+
// ---- A6/§5.4-4 守恒断言(分级:C2 B级·在册下界 / C3 半级;红牌 9 —— B/C 级不冒充 A 级护栏)----
|
|
1084
|
+
// C4(A 级)依赖 scan-code + doctor,归属 §7 的 P3 存量迁移批(M1 rebuild 第 4 级取源,须 LT 授权),
|
|
1085
|
+
// 不在本批运行时接线;A1 的模块级可达性由 tests 断言(scanCode → seedRegistry 组合)。
|
|
1086
|
+
if (registry && registryWarnings /* 数组恒存在,仅借位标识 registry 模式 */) {
|
|
1087
|
+
// C2 scope(R3-2③):已关闭 | closing | 当前 change 的手写 DDL
|
|
1088
|
+
const ddlTables = new Set();
|
|
1089
|
+
const changesRoot = join(projectRoot, 'changes');
|
|
1090
|
+
if (existsSync(changesRoot)) {
|
|
1091
|
+
for (const name of readdirSync(changesRoot)) {
|
|
1092
|
+
if (name !== changeName) {
|
|
1093
|
+
const r = join(changesRoot, name);
|
|
1094
|
+
if (!isChangeClosed(r)) continue;
|
|
1095
|
+
}
|
|
1096
|
+
const d = join(changesRoot, name, 'architecture', 'sql', 'ddl');
|
|
1097
|
+
if (!existsSync(d)) continue;
|
|
1098
|
+
for (const f of readdirSync(d).filter(x => x.endsWith('.sql'))) {
|
|
1099
|
+
for (const t of extractTablesFromSql(readFileSync(join(d, f), 'utf-8'))) ddlTables.add(t);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const c2Missing = [...ddlTables].filter(n => !registry.tables.some(t => t.name === n)).sort();
|
|
1104
|
+
if (c2Missing.length > 0) {
|
|
1105
|
+
registryWarnings.push(`${changeName}:[C2·B级下界] registry 缺手写 DDL 表 ${c2Missing.join(', ')}——检出后须人工核对 delta/补种链(C2 仅下界,对无 DDL change 空集恒真,不得当 A 级护栏)`);
|
|
1106
|
+
}
|
|
1107
|
+
// C3(半级):registry.endpoints 对 api.md 扫描集——generateApiIndex 已机械补种,
|
|
1108
|
+
// 此处仅对账「registry 是否含扫描集」(正常恒真;改坏补种链时变红——正向存在性)
|
|
1109
|
+
const apiC3Missing = [];
|
|
1110
|
+
if (existsSync(changesRoot)) {
|
|
1111
|
+
for (const name of readdirSync(changesRoot)) {
|
|
1112
|
+
if (name !== changeName) {
|
|
1113
|
+
const r = join(changesRoot, name);
|
|
1114
|
+
if (!isChangeClosed(r)) continue;
|
|
1115
|
+
}
|
|
1116
|
+
const p = join(changesRoot, name, 'architecture', 'api.md');
|
|
1117
|
+
if (!existsSync(p)) continue;
|
|
1118
|
+
for (const e of extractEndpointsDetailed(readFileSync(p, 'utf-8')).endpoints) {
|
|
1119
|
+
if (!registry.endpoints.some(x => x.method === e.method && x.path === e.path)) {
|
|
1120
|
+
apiC3Missing.push(`${e.method}:${e.path}`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (apiC3Missing.length > 0) {
|
|
1126
|
+
registryWarnings.push(`${changeName}:[C3·半级] registry 缺 api.md 端点 ${apiC3Missing.slice(0, 8).join(', ')}——api.md 由 LLM 写且计数受解析影响,仅作半级对账,须人工核对`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
790
1129
|
|
|
791
|
-
//
|
|
792
|
-
|
|
1130
|
+
// O8 · registry 落盘(唯一持久化点;DRY_RUN 零副作用,红牌 10)
|
|
1131
|
+
if (registry && !DRY_RUN) {
|
|
1132
|
+
saveRegistry(globalArchDir, registry);
|
|
1133
|
+
}
|
|
793
1134
|
|
|
794
|
-
// Step 8: 白名单 git commit
|
|
1135
|
+
// Step 8: 白名单 git commit(O8-2:追加 .registry/ 产出文件——LT 裁定归属本批)
|
|
795
1136
|
const touchedFiles = [
|
|
796
1137
|
join(globalArchDir, 'ARCHITECTURE.md'),
|
|
797
1138
|
join(globalArchDir, 'PHYSICAL-MODEL.md'),
|
|
@@ -799,6 +1140,7 @@ export function run(args = {}) {
|
|
|
799
1140
|
join(globalArchDir, 'schema-baseline.sql'),
|
|
800
1141
|
join(globalArchDir, 'API-INDEX.md'),
|
|
801
1142
|
join(globalArchDir, 'INDEX.md'),
|
|
1143
|
+
...(registry ? [registryPath(globalArchDir)] : []), // O8:registry 进提交(登记不 commit = AT-2 修复)
|
|
802
1144
|
...(archiveResult.files || []), // v0.36.3:changelog/ 复制文件入白名单
|
|
803
1145
|
].filter(p => existsSync(p));
|
|
804
1146
|
const dirty = detectUntouchedDirtyFiles(projectRoot, globalArchDir, touchedFiles);
|
|
@@ -815,6 +1157,11 @@ export function run(args = {}) {
|
|
|
815
1157
|
const warnings = [
|
|
816
1158
|
...(conflictResult.warnings || []),
|
|
817
1159
|
...(apiResult.warnings || []),
|
|
1160
|
+
// O8 · 软着陆登记(§5.4-7:复用既有登记族语义,不新增 DEPRECATED-* 告警族)
|
|
1161
|
+
...(registry === null
|
|
1162
|
+
? [`${changeName}:.arch-delta.json 缺失 → legacy 回写路径(软着陆期合法,v0.66.0 起硬切;如需登记理由按 arch_merge_skip_reason 同族补记)`]
|
|
1163
|
+
: []),
|
|
1164
|
+
...registryWarnings,
|
|
818
1165
|
...(archResult.reason === 'no-aggregate-delta'
|
|
819
1166
|
? [`${changeName}:本 change 无聚合增量(aggregate_action: none),marker 区按已合并 change 全集投影,未新增聚合行`]
|
|
820
1167
|
: []),
|
|
@@ -893,6 +1240,9 @@ export function scaffoldGlobalLedger(projectRoot) {
|
|
|
893
1240
|
// 空 marker 区骨架(与 buildCurrentStateSection 同源)。
|
|
894
1241
|
// 带 H1 与内容归属说明(v0.53.0 §115.6,P4 反馈 m2 / 问题8):同批其他制品均有 H1,
|
|
895
1242
|
// 且需澄清「As-Is 叙述写哪」——arch-merge 只重写 marker 区**之间**,区外内容保留。
|
|
1243
|
+
// O8 / D-6 复核(P0-B''):scaffold **不产 `.registry/`**——M6 冷启动语义 = registry 由
|
|
1244
|
+
// 首次带 delta 的 arch-merge 自动 seed 建立(红牌 12);空 marker 骨架与 registry 渲染
|
|
1245
|
+
// 输出同构(buildCurrentStateSection 同源),首 merge 替换即接管,无需本函数改动。
|
|
896
1246
|
writeIfAbsent('ARCHITECTURE.md', () => [
|
|
897
1247
|
'# Architecture',
|
|
898
1248
|
'',
|