@tbox.cn/app-toolkit 0.4.0 → 0.5.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 (39) hide show
  1. package/dist/chunk-WUULQDOV.js +1 -0
  2. package/dist/contracts-resolver-PS2UH2G6.js +1 -0
  3. package/dist/index.d.ts +315 -144
  4. package/dist/index.js +9 -9
  5. package/package.json +2 -2
  6. package/src/assembly/provider-catalog.ts +1 -1
  7. package/src/assembly/resolve-key.ts +75 -0
  8. package/src/assembly/walk-manifests.ts +138 -85
  9. package/src/core/contracts-expected.ts +2 -2
  10. package/src/core/contracts-resolver.ts +12 -1
  11. package/src/core/module-schema.ts +87 -14
  12. package/src/dto.ts +48 -33
  13. package/src/factory.ts +62 -18
  14. package/src/index.ts +14 -7
  15. package/src/integrations/mock-bindings.ts +1 -1
  16. package/src/integrations/module-binding.ts +60 -0
  17. package/src/integrations/predicate-io.ts +26 -5
  18. package/src/integrations/predicate.ts +97 -57
  19. package/src/integrations/prune-bindings.ts +33 -14
  20. package/src/integrations/write.ts +43 -27
  21. package/src/views/app.ts +4 -4
  22. package/src/views/context.ts +11 -12
  23. package/src/views/integration-schemas.ts +61 -17
  24. package/src/views/modules.ts +29 -23
  25. package/src/views/providers.ts +5 -5
  26. package/src/views/service-detail.ts +1 -1
  27. package/src/views/service-resolutions.ts +14 -13
  28. package/tests/credentials-view.test.ts +2 -2
  29. package/tests/demo-app.ts +11 -6
  30. package/tests/dev-manifest-catalog.test.ts +14 -14
  31. package/tests/file-cache.test.ts +5 -5
  32. package/tests/module-schema.test.ts +106 -20
  33. package/tests/resolve-key.test.ts +154 -0
  34. package/tests/vendor-schema-keywords.test.ts +74 -0
  35. package/tests/views.test.ts +296 -166
  36. package/tests/write-core.test.ts +83 -0
  37. package/dist/chunk-KPD3LUH4.js +0 -1
  38. package/dist/contracts-resolver-E4SECBOG.js +0 -1
  39. package/src/integrations/domain-binding.ts +0 -81
@@ -9,7 +9,7 @@ import { readAppManifestFull, type NpmModuleEntry, type AppManifestFull } from '
9
9
  *
10
10
  * manifest 候选路径探测(packages/<id>/tbox.module.json 优先 → node_modules/<pkg>/tbox.module.json;
11
11
  * tbox-app dev 真身经 .tbox-dev.local/dev-manifest.json redirects 独立 catalog 通道直达)一次遍历同时产出:
12
- * 1. modules:已装模块清单(declared;demand meta title/description 透传)
12
+ * 1. modules:已装模块清单(declared;demand meta name/description 透传)
13
13
  * 2. catalog(富形态):providers 嵌套(vendor→impl→entry,per-service {implementation, credentialType}
14
14
  * 保真——同厂商异构/多实现合法,D25/H1)+ 需求侧聚合
15
15
  * 3. serviceSchemas:per-service schema 复合键表——键 `${provider}\u0000${service}`(H2,同一服务
@@ -20,10 +20,12 @@ import { readAppManifestFull, type NpmModuleEntry, type AppManifestFull } from '
20
20
  * ③ 候选自身 {service-slots.json, src/service-slots.json}(local/codegen 全量副本——
21
21
  * mall 生成应用主形态,不依赖 install);零缓存枚举,与 manifest 探测同机制;
22
22
  * 多行业 contracts-* 天然扩展 F8);缺席 → [](退化 demanded ∪ configured)
23
- * 5. slotMeta:词汇 meta 节(per-service {title,description} 展示元数据——消费端兜底链
23
+ * 5. slotMeta:词汇 meta 节(per-service {name,description} 展示元数据——消费端兜底链
24
24
  * 词汇 > demand > id,views/context resolveServiceDisplay 单源);多包发现序首声明胜出
25
25
  * (与 catalog addSlots 同语义);contracts-mall 词汇锁保证 mall 域全量覆盖;缺席 → {}
26
- * 6. domainKeyIssues:跨模块域键冲突 issue(两模块派生同一 domains 键——P13)
26
+ * 6. ownership:service → moduleId 归属表(v6——declare.owns 聚合;install id 键控;
27
+ * 双 owner 冲突 → declarationIssues OWNERSHIP_CONFLICT + 先声明胜出)
28
+ * 7. declarationIssues:声明面诊断(OWNERSHIP_CONFLICT / OLD_MANIFEST_KEY——P3 通道)
27
29
  *
28
30
  * **约束(D26;FX-3/F7 边界)**:目录枚举零缓存——readdir / node_modules 候选探测 / `.tbox/app.json` 读取 /
29
31
  * dev-manifest 读取每次执行(枚举输入保持 fresh——agent 装卸模块下一笔即见);manifest/service-slots
@@ -32,6 +34,11 @@ import { readAppManifestFull, type NpmModuleEntry, type AppManifestFull } from '
32
34
  * 宽松解析语义(坏文件静默 + 重复键首声明保留)零改动。
33
35
  */
34
36
 
37
+ /** 声明面诊断(v6 P3 通道——OWNERSHIP_CONFLICT / OLD_MANIFEST_KEY;error 级) */
38
+ export type DeclarationIssue =
39
+ | { severity: 'error'; code: 'OWNERSHIP_CONFLICT'; message: string; module?: string }
40
+ | { severity: 'error'; code: 'OLD_MANIFEST_KEY'; message: string; module?: string };
41
+
35
42
  export interface WalkManifestsResult {
36
43
  /** declared 产物(install id + descriptor;demand meta 透传) */
37
44
  modules: WalkModuleInfo[];
@@ -41,15 +48,18 @@ export interface WalkManifestsResult {
41
48
  serviceSchemas: Record<string, { configSchema?: string; credentialSchema?: string; packageDir: string }>;
42
49
  /** 服务词汇 = 所有携带 service-slots.json 的已装契约包 `.slots` 键并集(缺席 → []) */
43
50
  vocabulary: string[];
44
- /** 词汇 meta 节:per-service {title,description} 展示元数据(多包首声明胜出;缺席 → {}) */
51
+ /** 词汇 meta 节:per-service {name,description} 展示元数据(多包首声明胜出;缺席 → {}) */
45
52
  slotMeta: Record<string, SlotMeta>;
46
- /** 跨模块域键冲突(P13——两模块的 service 集派生同一 domains 键) */
47
- domainKeyIssues: Array<{ severity: 'warning'; code: 'DOMAIN_KEY_CONFLICT'; message: string; module?: string }>;
53
+ /** v6 服务归属表:service → moduleId(install id 键控——integrations modules 节键同轴;
54
+ * 双 owner 冲突 → declarationIssues + 先声明胜出;dev 通道以 manifest id 参与归属) */
55
+ ownership: Record<string, string>;
56
+ /** 声明面诊断(OWNERSHIP_CONFLICT / OLD_MANIFEST_KEY) */
57
+ declarationIssues: DeclarationIssue[];
48
58
  }
49
59
 
50
- /** 槽位展示元数据(service-slots.json meta 节元素——宽松提取,字段皆可选) */
60
+ /** 槽位展示元数据(service-slots.json meta 节元素——宽松提取,字段皆可选;v7 词汇统一 title→name) */
51
61
  export interface SlotMeta {
52
- title?: string;
62
+ name?: string;
53
63
  description?: string;
54
64
  }
55
65
 
@@ -69,11 +79,11 @@ function parseSlotsFile(raw: string): SlotsFile | null {
69
79
  if (parsed.meta && typeof parsed.meta === 'object' && !Array.isArray(parsed.meta)) {
70
80
  for (const [svc, entry] of Object.entries(parsed.meta as Record<string, unknown>)) {
71
81
  if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
72
- const { title, description } = entry as { title?: unknown; description?: unknown };
82
+ const { name, description } = entry as { name?: unknown; description?: unknown };
73
83
  const m: SlotMeta = {};
74
- if (typeof title === 'string' && title.length > 0) m.title = title;
84
+ if (typeof name === 'string' && name.length > 0) m.name = name;
75
85
  if (typeof description === 'string' && description.length > 0) m.description = description;
76
- if (m.title !== undefined || m.description !== undefined) meta[svc] = m;
86
+ if (m.name !== undefined || m.description !== undefined) meta[svc] = m;
77
87
  }
78
88
  }
79
89
  return { slots, meta };
@@ -94,15 +104,15 @@ export interface WalkModuleInfo {
94
104
  }
95
105
 
96
106
  /** 富形态供给条目:per-service {implementation, credentialType} 恒数组保真(H1);
97
- * title/description = 厂商展示元数据(strict manifest descriptor 透传——多包同厂商
98
- * 首声明胜出,与 owner 同语义;B1 walk 层传播) */
107
+ * name/description = 厂商展示元数据(strict manifest descriptor 透传——多包同厂商
108
+ * 首声明胜出,与 owner 同语义;B1 walk 层传播;v7 词汇统一 title→name) */
99
109
  export interface WalkProviderEntry {
100
110
  provider: string;
101
111
  implementation: string;
102
112
  credentialType: string;
103
113
  owner: string;
104
- /** 厂商展示名(manifest 模块 meta——多包首声明;缺席 = undefined) */
105
- title?: string;
114
+ /** 厂商展示名(manifest name——多包首声明;缺席 = undefined) */
115
+ name?: string;
106
116
  description?: string;
107
117
  local?: boolean;
108
118
  configSchema?: string;
@@ -117,8 +127,8 @@ export interface WalkCatalog {
117
127
  {
118
128
  optional: boolean;
119
129
  defaults: Array<{ provider: string; implementation: string }>;
120
- /** demand meta(title/description 首声明保留——模块 manifest demand 侧 additive) */
121
- title?: string;
130
+ /** demand meta(name/description 首声明保留——模块 manifest demand 侧 additive;v7 title→name) */
131
+ name?: string;
122
132
  description?: string;
123
133
  owners: string[];
124
134
  }
@@ -126,25 +136,28 @@ export interface WalkCatalog {
126
136
  credentialTypes: Record<string, { owner: string; packageDir: string; credentialSchema?: string }>;
127
137
  }
128
138
 
129
- /** loose 允许的 contributes 局部结构(本地声明——解跨模块类型身份耦合,TS2719 防线) */
130
- interface LooseContributes {
139
+ /** loose 允许的 declare 局部结构(本地声明——解跨模块类型身份耦合,TS2719 防线) */
140
+ interface LooseDeclare {
131
141
  providers?: { slots?: ProviderSlotContrib[] };
132
- services?: Array<{
142
+ consumes?: Array<{
133
143
  service: string;
134
144
  optional?: boolean;
135
145
  defaults?: Array<{ provider: string; implementation: string }>;
136
- title?: string;
146
+ name?: string;
137
147
  description?: string;
138
148
  }>;
139
149
  }
140
150
 
141
- /** manifest 读取结果(判别联合——strict = 完整 descriptor;loose = 部分形态宽容提取) */
151
+ /** manifest 读取结果(判别联合——strict = 完整 descriptor;loose = 部分形态宽容提取;
152
+ * legacy = 旧键(v6 声明面 / v7 词汇统一,parseModuleDescriptor legacy 标志透传)——
153
+ * 拒 loose 静默兜底,定向 issue)。 */
142
154
  type ReadManifestResult =
143
- | { kind: 'strict'; name: string; version: string; descriptor: ModuleDescriptor; contributes: ModuleDescriptor['contributes'] }
144
- | { kind: 'loose'; name?: string; contributes: LooseContributes };
155
+ | { kind: 'strict'; id: string; version: string; descriptor: ModuleDescriptor; declare: ModuleDescriptor['declare'] }
156
+ | { kind: 'loose'; id?: string; declare: LooseDeclare }
157
+ | { kind: 'legacy'; detail: string };
145
158
 
146
- /** 读单文件 manifest(坏文件 → null;宽松——strict 解析失败但有 contributes → loose 兜底,
147
- * 保留旧 SDK loadProviderCatalog 对部分形态 manifest 的容忍语义,C3-5 桥等价性)。
159
+ /** 读单文件 manifest(坏文件 → null;宽松——strict 解析失败但为新形态 declare → loose 兜底;
160
+ * 旧键 → legacy(定向 issue,静默兜底防线——单源 = parseModuleDescriptor legacy 标志)。
148
161
  * FX-3(F7 边界):**文件内容**经 file-cache(stat 指纹——外部写下一笔自愈);目录枚举仍零缓存。 */
149
162
  function readManifestFile(file: string, cache?: FileCache): ReadManifestResult | null {
150
163
  const parse = (raw: string): ReadManifestResult | null => {
@@ -152,17 +165,19 @@ function readManifestFile(file: string, cache?: FileCache): ReadManifestResult |
152
165
  const parsedRaw: unknown = JSON.parse(raw);
153
166
  const parsed = parseModuleDescriptor(parsedRaw);
154
167
  if (parsed.ok) {
155
- return { kind: 'strict', name: parsed.value.name, version: parsed.value.version, descriptor: parsed.value, contributes: parsed.value.contributes };
168
+ return { kind: 'strict', id: parsed.value.id, version: parsed.value.version, descriptor: parsed.value, declare: parsed.value.declare };
156
169
  }
157
- // loose 兜底:strict 拒(部分形态 manifest——如仅 contributes)但有 contributes → 宽松提取
158
- if (parsedRaw && typeof parsedRaw === 'object' && 'contributes' in parsedRaw) {
159
- const obj = parsedRaw as { name?: unknown; contributes?: ModuleDescriptor['contributes'] };
170
+ // 旧键(v6/v7):parseModuleDescriptor 已定向检测(legacy 标志)——拒 loose 静默兜底
171
+ if (parsed.legacy) return { kind: 'legacy', detail: parsed.errors.join('; ') };
172
+ // loose 兜底:strict 拒(部分形态 manifest——如仅 declare)但有新形态 declare → 宽松提取
173
+ if (parsedRaw && typeof parsedRaw === 'object' && 'declare' in parsedRaw) {
174
+ const obj = parsedRaw as { id?: unknown; declare?: LooseDeclare };
160
175
  return {
161
176
  kind: 'loose',
162
- ...(typeof obj.name === 'string' ? { name: obj.name } : {}),
163
- contributes: {
164
- providers: (obj.contributes as LooseContributes | undefined)?.providers,
165
- services: (obj.contributes as LooseContributes | undefined)?.services,
177
+ ...(typeof obj.id === 'string' ? { id: obj.id } : {}),
178
+ declare: {
179
+ providers: obj.declare?.providers,
180
+ consumes: obj.declare?.consumes,
166
181
  },
167
182
  };
168
183
  }
@@ -283,10 +298,9 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
283
298
  const modules: WalkModuleInfo[] = [];
284
299
  const catalog: WalkCatalog = { providers: {}, services: {}, credentialTypes: {} };
285
300
  const serviceSchemas: WalkManifestsResult['serviceSchemas'] = {};
286
- const domainKeyIssues: WalkManifestsResult['domainKeyIssues'] = [];
301
+ const declarationIssues: WalkManifestsResult['declarationIssues'] = [];
302
+ const ownership: Record<string, string> = {};
287
303
  const seen = new Set<string>();
288
- /** 域键 → 首个声明模块(P13 冲突检测) */
289
- const domainOwners = new Map<string, string>();
290
304
 
291
305
  const addSlots = (slots: readonly ProviderSlotContrib[], owner: string, packageDir: string, modMeta?: SlotMeta): void => {
292
306
  for (const slot of slots) {
@@ -301,7 +315,7 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
301
315
  implementation: slot.implementation,
302
316
  credentialType: slot.credentialType,
303
317
  owner,
304
- ...(modMeta?.title ? { title: modMeta.title } : {}),
318
+ ...(modMeta?.name ? { name: modMeta.name } : {}),
305
319
  ...(modMeta?.description ? { description: modMeta.description } : {}),
306
320
  ...(slot.local ? { local: true } : {}),
307
321
  ...(slot.configSchema ? { configSchema: slot.configSchema } : {}),
@@ -328,10 +342,48 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
328
342
 
329
343
  /** 厂商展示元数据(strict manifest descriptor 透传——loose 形态无 descriptor → undefined) */
330
344
  const modMetaOf = (manifest: ReadManifestResult): SlotMeta | undefined =>
331
- manifest.kind === 'strict' ? { title: manifest.descriptor.title, description: manifest.descriptor.description } : undefined;
345
+ manifest.kind === 'strict' ? { name: manifest.descriptor.name, description: manifest.descriptor.description } : undefined;
346
+
347
+ /** v6 归属聚合:declare.owns → ownership 表(先声明胜出;冲突 → OWNERSHIP_CONFLICT)。
348
+ * owner 键 = install id(integrations modules 节键同轴——npmModules 通道 = entry.id、
349
+ * packages/dev 通道 = manifest id)。 */
350
+ const addOwns = (services: readonly string[], owner: string): void => {
351
+ for (const service of services) {
352
+ const first = ownership[service];
353
+ if (first === undefined) {
354
+ ownership[service] = owner;
355
+ } else if (first !== owner) {
356
+ declarationIssues.push({
357
+ severity: 'error',
358
+ code: 'OWNERSHIP_CONFLICT',
359
+ module: owner,
360
+ message: `服务 ${service} 归属冲突:${first} 与 ${owner} 均声明 owns(先声明 ${first} 胜出)——修正 manifest declare.owns 互斥`,
361
+ });
362
+ }
363
+ }
364
+ };
332
365
 
333
- const addServiceNeed = (
334
- need: { service: string; optional?: boolean; defaults?: Array<{ provider: string; implementation: string }>; title?: string; description?: string },
366
+ /** 旧键 legacy manifest → 定向 issue(N1:拒 loose 静默兜底)。 */
367
+ const pushLegacyIssue = (manifest: ReadManifestResult, module: string): void => {
368
+ if (manifest.kind !== 'legacy') return;
369
+ declarationIssues.push({
370
+ severity: 'error',
371
+ code: 'OLD_MANIFEST_KEY',
372
+ module,
373
+ message: `${module}:${manifest.detail}`,
374
+ });
375
+ };
376
+
377
+ /** declare 提取(strict/loose 双形态统一视图;legacy → null) */
378
+ const declareOf = (manifest: ReadManifestResult): LooseDeclare | null =>
379
+ manifest.kind === 'strict'
380
+ ? manifest.declare
381
+ : manifest.kind === 'loose'
382
+ ? manifest.declare
383
+ : null;
384
+
385
+ const addServiceConsume = (
386
+ need: { service: string; optional?: boolean; defaults?: Array<{ provider: string; implementation: string }>; name?: string; description?: string },
335
387
  owner: string,
336
388
  ): void => {
337
389
  const optional = need.optional !== false;
@@ -353,22 +405,10 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
353
405
  catalog.services[need.service] = {
354
406
  optional,
355
407
  defaults: [...defaults],
356
- ...(need.title ? { title: need.title } : {}),
408
+ ...(need.name ? { name: need.name } : {}),
357
409
  ...(need.description ? { description: need.description } : {}),
358
410
  owners: [owner],
359
411
  };
360
- // P13 域键冲突检测(首声明占用;跨模块冲突 → issue)
361
- const domain = need.service.includes('.') ? need.service.slice(0, need.service.indexOf('.')) : need.service;
362
- const firstOwner = domainOwners.get(domain);
363
- if (firstOwner === undefined) domainOwners.set(domain, owner);
364
- else if (firstOwner !== owner) {
365
- domainKeyIssues.push({
366
- severity: 'warning',
367
- code: 'DOMAIN_KEY_CONFLICT',
368
- module: owner,
369
- message: `域键 ${domain} 被多模块声明(${firstOwner} 与 ${owner})——domains 换算歧义(P13)`,
370
- });
371
- }
372
412
  };
373
413
 
374
414
  // 通道 1:npmModules 登记(packages/<id> 本地优先 → node_modules/<pkg>)
@@ -377,36 +417,46 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
377
417
  const local = join(appDir, 'packages', entry.id);
378
418
  const nm = join(appDir, 'node_modules', ...entry.package.split('/'));
379
419
  const manifest = readManifestFile(join(local, 'tbox.module.json'), cache) ?? readManifestFile(join(nm, 'tbox.module.json'), cache);
420
+ if (manifest?.kind === 'legacy') {
421
+ pushLegacyIssue(manifest, entry.id);
422
+ seen.add(entry.id); // 防重复报
423
+ continue;
424
+ }
380
425
  if (!manifest) continue;
381
426
  seen.add(entry.id);
382
427
  const dir = existsSync(join(local, 'tbox.module.json')) ? local : nm;
383
- const contributes = manifest.contributes;
428
+ const declare = declareOf(manifest);
384
429
  // loose 形态(部分 manifest)不进 modules 清单(descriptor 缺名不可装配化)——仅 catalog 聚合(旧语义等价)
385
430
  if (manifest.kind === 'strict') {
386
431
  modules.push({ id: entry.id, dir, pkg: entry.mode === 'sdk' ? entry.package : `@app/${entry.id}`, descriptor: manifest.descriptor });
387
432
  }
388
- const slots = contributes.providers?.slots ?? [];
433
+ const slots = declare?.providers?.slots ?? [];
389
434
  if (slots.length) addSlots(slots, entry.package, dir, modMetaOf(manifest));
390
- const needs = contributes.services ?? [];
391
- if (needs.length) {
392
- for (const need of needs) {
393
- addServiceNeed(need, entry.package);
394
- }
395
- }
435
+ const consumes = declare?.consumes ?? [];
436
+ for (const need of consumes) addServiceConsume(need, entry.package);
437
+ // v6 归属:owner 键 = install id(integrations modules 节同轴)
438
+ const owns = manifest.kind === 'strict' ? manifest.descriptor.declare.owns : [];
439
+ if (owns.length) addOwns(owns.map((o) => o.service), entry.id);
396
440
  }
397
441
 
398
442
  // 通道 1.5:tbox-app dev manifest 真身(catalog-only——聚合宽/声明窄不变量:dev 模块
399
- // 永不进 modules 清单,防 sync 污染受跟踪装配文件;登记真源优先(seen 按模块名去重))
443
+ // 永不进 modules 清单,防 sync 污染受跟踪装配文件;登记真源优先(seen 按模块名去重))。
444
+ // v6:归属以 manifest id 参与(dev 会话的 modules 集成位键同轴——N3 聚合宽/声明窄)。
400
445
  for (const { pkg, dir } of readDevManifestRedirects(appDir)) {
401
446
  const manifest = readManifestFile(join(dir, 'tbox.module.json'), cache);
402
- if (!manifest?.name || seen.has(manifest.name)) continue;
403
- seen.add(manifest.name);
404
- const slots = manifest.contributes.providers?.slots ?? [];
405
- if (slots.length) addSlots(slots, pkg, dir, modMetaOf(manifest));
406
- const needs = manifest.contributes.services ?? [];
407
- if (needs.length) {
408
- for (const need of needs) addServiceNeed(need, pkg);
447
+ if (manifest?.kind === 'legacy') {
448
+ pushLegacyIssue(manifest, pkg);
449
+ continue;
409
450
  }
451
+ if (!manifest?.id || seen.has(manifest.id)) continue;
452
+ seen.add(manifest.id);
453
+ const declare = declareOf(manifest);
454
+ const slots = declare?.providers?.slots ?? [];
455
+ if (slots.length) addSlots(slots, pkg, dir, modMetaOf(manifest));
456
+ const consumes = declare?.consumes ?? [];
457
+ for (const need of consumes) addServiceConsume(need, pkg);
458
+ const owns = manifest.kind === 'strict' ? manifest.descriptor.declare.owns : [];
459
+ if (owns.length) addOwns(owns.map((o) => o.service), manifest.id);
410
460
  }
411
461
 
412
462
  // 通道 2:packages/* 约定发现(本地模块——npmModules 未登记形态,如手工放入的本地模块)
@@ -422,22 +472,25 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
422
472
  if (seen.has(name)) continue;
423
473
  const dir = join(packagesDir, name);
424
474
  const manifest = readManifestFile(join(dir, 'tbox.module.json'), cache);
475
+ if (manifest?.kind === 'legacy') {
476
+ pushLegacyIssue(manifest, name);
477
+ seen.add(name);
478
+ continue;
479
+ }
425
480
  if (!manifest) continue;
426
- const modName = manifest.name ?? name;
427
- if (seen.has(modName)) continue;
428
- seen.add(modName);
429
- const contributes = manifest.contributes;
481
+ const modId = manifest.id ?? name;
482
+ if (seen.has(modId)) continue;
483
+ seen.add(modId);
484
+ const declare = declareOf(manifest);
430
485
  if (manifest.kind === 'strict') {
431
- modules.push({ id: modName, dir, pkg: `@app/${modName}`, descriptor: manifest.descriptor });
432
- }
433
- const slots = contributes.providers?.slots ?? [];
434
- if (slots.length) addSlots(slots, `@app/${modName}`, dir, modMetaOf(manifest));
435
- const needs = contributes.services ?? [];
436
- if (needs.length) {
437
- for (const need of needs) {
438
- addServiceNeed(need, `@app/${modName}`);
439
- }
486
+ modules.push({ id: modId, dir, pkg: `@app/${modId}`, descriptor: manifest.descriptor });
440
487
  }
488
+ const slots = declare?.providers?.slots ?? [];
489
+ if (slots.length) addSlots(slots, `@app/${modId}`, dir, modMetaOf(manifest));
490
+ const consumes = declare?.consumes ?? [];
491
+ for (const need of consumes) addServiceConsume(need, `@app/${modId}`);
492
+ const owns = manifest.kind === 'strict' ? manifest.descriptor.declare.owns : [];
493
+ if (owns.length) addOwns(owns.map((o) => o.service), modId);
441
494
  }
442
495
  }
443
496
 
@@ -452,7 +505,7 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
452
505
  }
453
506
  }
454
507
 
455
- return { modules, catalog, serviceSchemas, vocabulary: [...vocabulary].sort(), slotMeta, domainKeyIssues };
508
+ return { modules, catalog, serviceSchemas, vocabulary: [...vocabulary].sort(), slotMeta, ownership, declarationIssues };
456
509
  }
457
510
 
458
511
  // ── CLI collectDeclared 迁入(C5——声明集合 + 双端入口探测 + declaration.moduleId 提取)──
@@ -514,7 +567,7 @@ export function collectDeclaredModules(appDir: string): DeclaredModuleInfo[] {
514
567
  for (const mod of walk.modules) {
515
568
  const mode = modeById.get(mod.id) ?? 'local';
516
569
  const hasServerEntry = probeModuleObjectExport(mod.dir, 'server', 'serverModule');
517
- const c = mod.descriptor.contributes;
570
+ const c = mod.descriptor.declare;
518
571
  declared.push({
519
572
  id: mod.id,
520
573
  pkg: mode === 'sdk' ? mod.pkg : `@app/${mod.id}`,
@@ -13,11 +13,11 @@ import type {
13
13
  */
14
14
 
15
15
  export interface EvaluationExports {
16
- /** 求值单源(TIER0;v3 显式激活 vs v4 通配语义随应用版本) */
16
+ /** 求值单源(TIER0;v3 显式激活 vs v4 通配语义随应用版本;v6 第五参 ownerOf) */
17
17
  resolveEffectiveService: typeof resolveEffectiveService;
18
18
  /** 供给查表派生(谓词 #12 与视图供给轴共用输入) */
19
19
  buildSupplyLookup: typeof buildSupplyLookup;
20
- /** service → 首点段域前缀(谓词 #14 域键匹配) */
20
+ /** service → 首点段域前缀(v6 deprecated——级联键控退役,仅词汇组织消费) */
21
21
  moduleDomainOf: typeof moduleDomainOf;
22
22
  /** 部署实例注册表归一(默认实例委托——null → '*' 通配口径) */
23
23
  normalizeInstances: typeof normalizeInstances;
@@ -38,13 +38,16 @@ export interface ResolvedContracts {
38
38
  evaluation: boolean;
39
39
  /** 严格 zod 面可用(parseIntegrationsConfig 在场——≥0.9 门控判据) */
40
40
  strictValidation: boolean;
41
+ /** v6 形状标记在场(OWNERSHIP_BINDING_V6——modules[owner] 绑定形状支持;标记门 P1:
42
+ * v5 contracts + v6 toolkit 偏斜 → 503 第四分支,防静默忽略 ownerOf) */
43
+ ownershipBindingV6: boolean;
41
44
  /** 动态 import 产物(命名导出子集——Partial<EvaluationExports>) */
42
45
  module: ResolvedModule;
43
46
  /** 解析诊断(N1 构建指引 message 三分支的输入:未安装 / 版本 <0.9 / 源码态未构建) */
44
47
  notes: string[];
45
48
  }
46
49
 
47
- /** 求值面必需具名导出(在场 → evaluation: true) */
50
+ /** 求值面必需具名导出(在场 → evaluation: true;moduleDomainOf v6 deprecated 但仍在发布面) */
48
51
  const EVALUATION_EXPORT_NAMES = [
49
52
  'resolveEffectiveService',
50
53
  'buildSupplyLookup',
@@ -56,10 +59,14 @@ const EVALUATION_EXPORT_NAMES = [
56
59
  /** 严格校验面具名导出(在场 → strictValidation: true;0.9+) */
57
60
  const STRICT_EXPORT_NAME = 'parseIntegrationsConfig';
58
61
 
62
+ /** v6 形状标记导出(在场 → ownershipBindingV6: true;判据先例 = STRICT_EXPORT_NAME 出口在场性) */
63
+ const OWNERSHIP_MARKER_NAME = 'OWNERSHIP_BINDING_V6';
64
+
59
65
  const UNRESOLVED: ResolvedContracts = {
60
66
  version: null,
61
67
  evaluation: false,
62
68
  strictValidation: false,
69
+ ownershipBindingV6: false,
63
70
  module: {},
64
71
  notes: [],
65
72
  };
@@ -203,10 +210,14 @@ async function probeCandidate(
203
210
  const mod = (await import(pathToFileURL(probeFile).href + versionQuery)) as Record<string, unknown>;
204
211
  const evaluation = EVALUATION_EXPORT_NAMES.every((name) => mod[name] !== undefined);
205
212
  const strictValidation = mod[STRICT_EXPORT_NAME] !== undefined;
213
+ // v6 形状标记(出口在场性——判据先例 STRICT_EXPORT_NAME);缺席时不在此 push note
214
+ // (503 文案归 factory assertContractsUsable 门控单点——P1)
215
+ const ownershipBindingV6 = mod[OWNERSHIP_MARKER_NAME] === true;
206
216
  return {
207
217
  version,
208
218
  evaluation,
209
219
  strictValidation,
220
+ ownershipBindingV6,
210
221
  module: mod as ResolvedModule,
211
222
  notes: evaluation
212
223
  ? []
@@ -1,16 +1,30 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  /**
4
- * tbox.module.json schema v1(001 §3.6 字段名,003 §4.1 schema v1 定版)。
4
+ * tbox.module.json schema v1(001 §3.6 字段名,003 §4.1 schema v1 定版;v6 声明面翻转)。
5
5
  * app-toolkit 单一校验源(自 CLI module-schema.ts 迁入;CLI 经 re-export 消费)。
6
6
  *
7
7
  * 迁移改造(ai-build C2-5):
8
8
  * - resourceNeedContribSchema 的 type 由 `z.enum(RESOURCE_TYPE_IDS)` 放宽为 `z.string()`——
9
9
  * 词汇校验移谓词、经 contracts 动态解析取 RESOURCE_TYPE_IDS(比静态 enum 更忠实应用版本);
10
- * - demand meta additive:serviceNeedContribSchema 增 title?/description?(服务展示元数据,
10
+ * - demand meta additive:serviceConsumeContribSchema 增 name?/description?(服务展示元数据,
11
11
  * manifest demand 侧 additive 扩展,catalog 聚合顺带携带——api.md ServiceDemand 对齐);
12
12
  * - 安全钉子(H9b):provider 名与 instance id 进凭据 stem 派生,字符集 pattern
13
13
  * `/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/` 为第一道防线(stem 派生处另有断言防线——纵深防御)。
14
+ *
15
+ * v6 声明面翻转(integrations v6 所有权模型):
16
+ * - `contributes` → `declare`(声明/行为分离——owns/consumes 语义对仗);
17
+ * - `declare.services` → `declare.consumes`(无向"服务" → 消费关系,多对多);
18
+ * - += `declare.owns`:薄数组(strictObject 单键——互斥事实无元数据;元数据恒在
19
+ * consumes 防双数组漂移)。服务归属唯一真源;双 owner = walk OWNERSHIP_CONFLICT;
20
+ * - 旧键 `contributes` / `declare.services`:parseModuleDescriptor 定向检测(N1——
21
+ * 报错含三路径升级指引,拒 loose 静默兜底)。
22
+ *
23
+ * v7 词汇统一(标识/展示名分轴,对齐实例轴 DeploymentInstance {id, name}):
24
+ * - 根键 `name` → `id`(标识键:唯一、寻址);根键 `title` → `name`(展示名:中文、可选);
25
+ * - `declare.consumes[].title` → `name`(demand meta 同步跟随);
26
+ * - 旧键 parseModuleDescriptor 定向检测(legacy? 标志——walk 据此拒 loose 兜底、
27
+ * CLI sync 据此拒装配改写)。
14
28
  */
15
29
 
16
30
  /** 输入字符集(安全钉子①):provider 名 / instanceId 恒经此 pattern(进凭据 stem 派生的输入) */
@@ -39,7 +53,7 @@ export const moduleDependencySchema = z.object({
39
53
  required: z.boolean().default(true),
40
54
  });
41
55
 
42
- /** provider 供给槽声明(integrations v2:contributes.providers.slots 元素;
56
+ /** provider 供给槽声明(integrations v2:declare.providers.slots 元素;
43
57
  * provider = 厂商名(catalog 嵌套第一轴)、implementation = 'id@version'(第二轴)) */
44
58
  export const providerSlotContribSchema = z.object({
45
59
  service: z.string(),
@@ -51,8 +65,8 @@ export const providerSlotContribSchema = z.object({
51
65
  credentialSchema: z.string().optional(),
52
66
  });
53
67
 
54
- /** 服务需求槽声明(contributes.services 元素;业务模块声明消费——catalog 需求侧聚合源) */
55
- export const serviceNeedContribSchema = z.object({
68
+ /** 服务消费槽声明(declare.consumes 元素;业务模块声明消费——catalog 需求侧聚合源) */
69
+ export const serviceConsumeContribSchema = z.object({
56
70
  service: z.string(),
57
71
  optional: z.boolean().default(true),
58
72
  defaults: z
@@ -64,11 +78,19 @@ export const serviceNeedContribSchema = z.object({
64
78
  )
65
79
  .default([]),
66
80
  // demand meta additive(api.md ServiceDemand):服务展示元数据,缺席 UI 回退 service id
67
- title: z.string().optional(),
81
+ // (v7 词汇统一:title → name)
82
+ name: z.string().optional(),
68
83
  description: z.string().optional(),
69
84
  });
70
85
 
71
- /** 资源需求声明(contributes.resources 元素;type 放宽为 string——词汇校验移谓词经 resolver 取词表) */
86
+ /** 服务归属声明(declare.owns 元素;v6 薄形状——互斥事实无元数据,strict 单键防双数组漂移) */
87
+ export const serviceOwnContribSchema = z
88
+ .object({
89
+ service: z.string(),
90
+ })
91
+ .strict();
92
+
93
+ /** 资源需求声明(declare.resources 元素;type 放宽为 string——词汇校验移谓词经 resolver 取词表) */
72
94
  export const resourceNeedContribSchema = z.object({
73
95
  id: z.string(),
74
96
  type: z.string(),
@@ -77,9 +99,10 @@ export const resourceNeedContribSchema = z.object({
77
99
  export const moduleDescriptorSchema = z
78
100
  .object({
79
101
  schemaVersion: z.number().default(1),
80
- name: z.string(),
81
- // 模块级 meta additive(B1):展示名/描述——回退归视图层(title ?? name / ''),schema 不灌值
82
- title: z.string().optional(),
102
+ // v7 词汇统一(标识/展示名分轴):id = 标识键(唯一、寻址,键 = install id 规约形态);
103
+ // name = 展示名(中文、可选)——回退归视图层(name ?? id),schema 不灌值
104
+ id: z.string(),
105
+ name: z.string().optional(),
83
106
  description: z.string().optional(),
84
107
  version: z.string().default('0.0.0'),
85
108
  kind: z.enum(['platform', 'business', 'third-party']).default('business'),
@@ -94,7 +117,7 @@ export const moduleDescriptorSchema = z
94
117
  defaultMode: z.enum(['hybrid', 'sdk', 'codegen', 'local']).default('codegen'),
95
118
  })
96
119
  .default({}),
97
- contributes: z
120
+ declare: z
98
121
  .object({
99
122
  handlers: z.array(z.string()).default([]),
100
123
  tools: z.array(z.string()).default([]),
@@ -107,7 +130,8 @@ export const moduleDescriptorSchema = z
107
130
  slots: z.array(providerSlotContribSchema).default([]),
108
131
  })
109
132
  .default({}),
110
- services: z.array(serviceNeedContribSchema).default([]),
133
+ owns: z.array(serviceOwnContribSchema).default([]),
134
+ consumes: z.array(serviceConsumeContribSchema).default([]),
111
135
  resources: z.array(resourceNeedContribSchema).default([]),
112
136
  })
113
137
  .default({}),
@@ -123,14 +147,63 @@ export const moduleDescriptorSchema = z
123
147
  export type ModuleDescriptor = z.infer<typeof moduleDescriptorSchema>;
124
148
  export type CardContrib = z.infer<typeof cardContribSchema>;
125
149
  export type ProviderSlotContrib = z.infer<typeof providerSlotContribSchema>;
126
- export type ServiceNeedContrib = z.infer<typeof serviceNeedContribSchema>;
150
+ export type ServiceConsumeContrib = z.infer<typeof serviceConsumeContribSchema>;
151
+ export type ServiceOwnContrib = z.infer<typeof serviceOwnContribSchema>;
127
152
  export type ResourceNeedContrib = z.infer<typeof resourceNeedContribSchema>;
128
153
 
129
154
  export type ParseModuleResult =
130
155
  | { ok: true; value: ModuleDescriptor }
131
- | { ok: false; errors: string[] };
156
+ | { ok: false; errors: string[]; legacy?: true };
157
+
158
+ /** 旧键检测(N1):命中 → 定向错误(含三路径升级指引),不走 loose 静默兜底。
159
+ * v6 分支:声明面翻转键(contributes / declare.services);v7 分支:词汇统一翻转键
160
+ * (标识 `name`→`id`、展示名 `title`→`name`)。doctor descriptor-schema 规则以错误文本
161
+ * 「已更名」嗅探透传 OLD_MANIFEST_KEY——新增分支消息必须保持该词。 */
162
+ function legacyManifestKeys(raw: unknown): string[] {
163
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return [];
164
+ const errors: string[] = [];
165
+ const obj = raw as Record<string, unknown>;
166
+ if ('contributes' in obj) {
167
+ errors.push(
168
+ '(root): manifest 旧键 `contributes` 已更名 `declare`(v6 声明面)——' +
169
+ 'codegen 副本经 `module update --source` 重新生成 / sdk 模式升级模块包 / 手改两键',
170
+ );
171
+ }
172
+ const declare = obj.declare;
173
+ if (declare && typeof declare === 'object' && !Array.isArray(declare)
174
+ && 'services' in (declare as Record<string, unknown>)) {
175
+ errors.push(
176
+ 'declare: manifest 旧键 `declare.services` 已更名 `declare.consumes`(消费关系定向化)——' +
177
+ '同上三路径升级',
178
+ );
179
+ }
180
+ // v7 词汇统一(标识/展示名分轴):`name`→`id`(标识)、`title`→`name`(展示)。
181
+ // 判据以 `id` 在场性消解同名键歧义:无 id 有 name = v6 旧态;有 id 有 title = 半迁移。
182
+ if (!('id' in obj) && 'name' in obj) {
183
+ errors.push(
184
+ '(root): manifest 标识键 `name` 已更名 `id`(v7 词汇统一)——' +
185
+ 'codegen 副本经 `module update --source` 重新生成 / sdk 模式升级模块包 / 手改两键',
186
+ );
187
+ }
188
+ const declareObj = declare && typeof declare === 'object' && !Array.isArray(declare)
189
+ ? (declare as Record<string, unknown>)
190
+ : undefined;
191
+ const consumeHasTitle = Array.isArray(declareObj?.consumes)
192
+ && (declareObj.consumes as unknown[]).some(
193
+ (c) => c !== null && typeof c === 'object' && 'title' in (c as Record<string, unknown>),
194
+ );
195
+ if ('title' in obj || consumeHasTitle) {
196
+ errors.push(
197
+ '(root): manifest 展示名键 `title` 已更名 `name`(v7 词汇统一;declare.consumes[].title 同步改 name)——'
198
+ + '同上三路径升级',
199
+ );
200
+ }
201
+ return errors;
202
+ }
132
203
 
133
204
  export function parseModuleDescriptor(raw: unknown): ParseModuleResult {
205
+ const legacy = legacyManifestKeys(raw);
206
+ if (legacy.length > 0) return { ok: false, errors: legacy, legacy: true };
134
207
  const result = moduleDescriptorSchema.safeParse(raw);
135
208
  if (result.success) return { ok: true, value: result.data };
136
209
  return {