@xulthekl/team-flow 0.64.0 → 0.67.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.
Files changed (77) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor-plugin/marketplace.json +1 -1
  6. package/.cursor-plugin/plugin.json +1 -1
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/CHANGELOG.md +60 -0
  9. package/GEMINI.md +1 -1
  10. package/INSTALL.md +1 -1
  11. package/README.md +1 -1
  12. package/agents/architecture-design.md +1 -1
  13. package/agents/architecture-reviewer.md +6 -2
  14. package/agents/prd-completeness-reviewer.md +10 -0
  15. package/agents/prd-writer.md +1 -0
  16. package/agents/release-archivist.md +2 -0
  17. package/docs/README_en.md +1 -1
  18. 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" +8 -6
  19. package/gemini-extension.json +1 -1
  20. package/hooks/session-start +2 -2
  21. package/llms.txt +1 -1
  22. package/package.json +1 -1
  23. package/plugin.json +1 -1
  24. package/prd/v1/prd.md +1 -1
  25. package/scripts/guard/checks/arch-gate-exemptions.mjs +5 -3
  26. package/scripts/guard/checks/arch-readiness.mjs +1 -1
  27. package/scripts/guard/checks/arch-snapshot.mjs +5 -3
  28. package/scripts/guard/checks/history-risk.mjs +132 -0
  29. package/scripts/guard/checks/prd-clarity-state.mjs +41 -0
  30. package/scripts/guard/checks/prd-clarity.mjs +176 -0
  31. package/scripts/guard/guard.mjs +16 -8
  32. package/scripts/infer-workflow.mjs +20 -0
  33. package/scripts/lib/arch-merge.mjs +384 -50
  34. package/scripts/lib/arch-parse.mjs +5 -2
  35. package/scripts/lib/arch-registry.mjs +523 -0
  36. package/scripts/lib/arch-scan-code.mjs +518 -0
  37. package/scripts/lib/cmd-arch.mjs +9 -1
  38. package/scripts/lib/cmd-doctor.mjs +3 -3
  39. package/scripts/lib/cmd-prd.mjs +84 -1
  40. package/scripts/lib/cmd-solutions.mjs +3 -0
  41. package/scripts/lib/cmd-state.mjs +31 -1
  42. package/scripts/lib/config-loader.mjs +20 -0
  43. package/scripts/lib/solutions-capture.mjs +5 -0
  44. package/scripts/lib/solutions-index-gen.mjs +33 -3
  45. package/scripts/lib/solutions-inject.mjs +34 -9
  46. package/scripts/lib/state-loader.mjs +13 -0
  47. package/scripts/team-flow.mjs +3 -0
  48. package/skills/architecture-design/SKILL.md +29 -11
  49. package/skills/architecture-design/chapters/ch04-entity-to-aggregate.md +18 -7
  50. package/skills/architecture-design/chapters/ch06-integration.md +12 -3
  51. package/skills/architecture-design/glossary.md +5 -1
  52. package/skills/architecture-design/references/adr-templates.md +56 -0
  53. package/skills/architecture-design/references/context-map-8.md +47 -0
  54. package/skills/architecture-design/references/ddd-evented-playbook.md +41 -0
  55. package/skills/architecture-design/references/s3.5-architecture-template.md +36 -4
  56. package/skills/architecture-design/references/s3.5-loading-protocol.md +5 -4
  57. package/skills/architecture-design/references/s3.5-product-architecture.md +6 -6
  58. package/skills/architecture-design/templates/architecture.md +20 -0
  59. package/skills/ce-brainstorm/SKILL.md +3 -3
  60. package/skills/ce-brainstorm/references/grounding.md +1 -1
  61. package/skills/ce-brainstorm/references/prd-84-authoring-spec.md +26 -3
  62. package/skills/ce-brainstorm/references/prototype-loop.md +8 -0
  63. package/skills/ce-compound/references/concepts-vocabulary.md +1 -1
  64. package/skills/ce-compound/references/full-mode-workflow.md +2 -2
  65. package/skills/ce-compound/references/lightweight-mode.md +1 -1
  66. package/skills/ce-compound/references/promotion-rules.md +1 -1
  67. package/skills/ce-compound/references/three-tier-index.md +1 -1
  68. package/skills/ce-plan/references/research-workflow.md +1 -1
  69. package/skills/jarvis/references/protocols.md +1 -0
  70. package/skills/release-archivist/SKILL.md +21 -0
  71. package/skills/release-archivist/references/closing-procedures.md +1 -1
  72. package/skills/workflow-orchestrator/SKILL.md +8 -4
  73. package/skills/workflow-orchestrator/references/s1-path-router.md +7 -0
  74. package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +16 -2
  75. package/skills/workflow-orchestrator/references/s3-plan-pipeline.md +1 -1
  76. package/skills/workflow-start/SKILL.md +1 -1
  77. package/templates/prd.md +9 -1
@@ -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
- const allAggregates = collectAllAggregates(projectRoot, changeName, aggregates);
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
- return [...collected.values()];
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
- lines.push(`| ${a.id} | ${a.context || '—'} | ${a.root || '—'} | ${a.source || '—'} | 已落地 | ${a.invariants || '—'} |`);
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 生成式(从 schema-baseline.sql 反向生成,禁改)。 */
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
- if (!existsSync(schemaPath)) {
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 sql = readFileSync(schemaPath, 'utf-8');
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 生成式(从 schema-baseline + PHYSICAL-MODEL 再生成,禁改)。 */
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
- if (!existsSync(schemaPath)) {
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 sql = readFileSync(schemaPath, 'utf-8');
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} | schema-baseline.sql | 实际落地 |`);
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)。\n'];
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
- const bcCount = (() => {
544
- const p = join(globalArchDir, 'ARCHITECTURE.md');
545
- if (!existsSync(p)) return null;
546
- const agg = extractAggregates(readFileSync(p, 'utf-8'));
547
- return new Set(agg.map(a => a.context)).size;
548
- })();
549
-
550
- const tableCount = (() => {
551
- const p = join(globalArchDir, 'schema-baseline.sql');
552
- if (!existsSync(p)) return null;
553
- return extractTablesFromSql(readFileSync(p, 'utf-8')).length;
554
- })();
555
-
556
- const endpointCount = (() => {
557
- const p = join(globalArchDir, 'API-INDEX.md');
558
- if (!existsSync(p)) return null;
559
- return extractEndpoints(readFileSync(p, 'utf-8')).length;
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`,
@@ -786,28 +1000,139 @@ export function run(args = {}) {
786
1000
  };
787
1001
  }
788
1002
 
789
- // Step 2: ARCHITECTURE.md(当前态 upsert)
790
- const archResult = mergeArchitecture(changeDir, archDir, globalArchDir, changeName, projectRoot);
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);
791
1045
 
792
1046
  // Step 4: schema-baseline.sql(幂等追加)——先同步 DDL,供 PHYSICAL-MODEL/DATABASE 生成消费
793
1047
  const schemaResult = syncSchemaBaseline(changeDir, archDir, globalArchDir, changeName);
794
1048
 
795
- // Step 3: PHYSICAL-MODEL.md(生成式,从 schema-baseline.sql)
796
- const pmResult = generatePhysicalModel(globalArchDir, schemaPath);
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
+ }
797
1067
 
798
- // Step 3b: DATABASE.md(生成式,从 schema-baseline.sql)
799
- const dbResult = generateDatabase(globalArchDir, schemaPath);
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);
800
1073
 
801
1074
  // Step 5: 归档
802
1075
  const archiveResult = archiveChangeScripts(changeDir, archDir, globalArchDir, changeName);
803
1076
 
804
- // Step 6: API-INDEX.md(生成式)
805
- 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
+ }
806
1129
 
807
- // Step 7: INDEX.md
808
- const indexResult = updateIndex(globalArchDir, changeName);
1130
+ // O8 · registry 落盘(唯一持久化点;DRY_RUN 零副作用,红牌 10)
1131
+ if (registry && !DRY_RUN) {
1132
+ saveRegistry(globalArchDir, registry);
1133
+ }
809
1134
 
810
- // Step 8: 白名单 git commit
1135
+ // Step 8: 白名单 git commit(O8-2:追加 .registry/ 产出文件——LT 裁定归属本批)
811
1136
  const touchedFiles = [
812
1137
  join(globalArchDir, 'ARCHITECTURE.md'),
813
1138
  join(globalArchDir, 'PHYSICAL-MODEL.md'),
@@ -815,6 +1140,7 @@ export function run(args = {}) {
815
1140
  join(globalArchDir, 'schema-baseline.sql'),
816
1141
  join(globalArchDir, 'API-INDEX.md'),
817
1142
  join(globalArchDir, 'INDEX.md'),
1143
+ ...(registry ? [registryPath(globalArchDir)] : []), // O8:registry 进提交(登记不 commit = AT-2 修复)
818
1144
  ...(archiveResult.files || []), // v0.36.3:changelog/ 复制文件入白名单
819
1145
  ].filter(p => existsSync(p));
820
1146
  const dirty = detectUntouchedDirtyFiles(projectRoot, globalArchDir, touchedFiles);
@@ -831,6 +1157,11 @@ export function run(args = {}) {
831
1157
  const warnings = [
832
1158
  ...(conflictResult.warnings || []),
833
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,
834
1165
  ...(archResult.reason === 'no-aggregate-delta'
835
1166
  ? [`${changeName}:本 change 无聚合增量(aggregate_action: none),marker 区按已合并 change 全集投影,未新增聚合行`]
836
1167
  : []),
@@ -909,6 +1240,9 @@ export function scaffoldGlobalLedger(projectRoot) {
909
1240
  // 空 marker 区骨架(与 buildCurrentStateSection 同源)。
910
1241
  // 带 H1 与内容归属说明(v0.53.0 §115.6,P4 反馈 m2 / 问题8):同批其他制品均有 H1,
911
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 替换即接管,无需本函数改动。
912
1246
  writeIfAbsent('ARCHITECTURE.md', () => [
913
1247
  '# Architecture',
914
1248
  '',
@@ -264,9 +264,12 @@ export function extractEndpoints(apiMd) {
264
264
  * 见 §106.2:由表头决定列语义,不再由「第二列的值」决定。
265
265
  */
266
266
  const AGG_HEADER_MAP = [
267
- [['聚合id', 'aggregateid', 'aggregate', '聚合名称', '聚合'], 'id'],
267
+ // `聚合id(候选)`/`子域`:产品级快照表头(emp-auth iterations/v0 实样,provenance 反推节)。
268
+ // P0-B'-①(ddd-purity-and-arch-merge-design v1.4 §5.7)起快照进入投影域——缺这两词条会让
269
+ // 快照表走位置兜底,invariants 错取「事务边界」列(R-c′:污染 > 缺失),故按实样补入。
270
+ [['聚合id', 'aggregateid', 'aggregate', '聚合名称', '聚合', '聚合id(候选)'], 'id'],
268
271
  [['聚合根', '根实体', 'root', '聚合根实体'], 'root'],
269
- [['所属bc', '所属限界上下文', '所属上下文', '上下文', 'context', 'bc'], 'context'],
272
+ [['所属bc', '所属限界上下文', '所属上下文', '上下文', 'context', 'bc', '子域'], 'context'],
270
273
  [['不变量', '关键不变量', 'invariant', 'invariants'], 'invariants'],
271
274
  [['来源', 'source'], 'source'],
272
275
  ];