@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
@@ -15,19 +15,24 @@
15
15
  * IMPL_WITHOUT_PROVIDER #5/#13 error impl-only(implementation 缺 provider 绑定组头)
16
16
  * INLINE_PROVIDER_INVALID #5 error 内联厂商声明超受控形状(槽级/实例级)
17
17
  * SCHEMA_FILE_MISSING #6 error credentialType schema 引用不存在
18
- * DOMAIN_CONFIG_INVALID #7 error root/domains 死配置(键 ∉ 引用厂商 schema properties;
18
+ * DOMAIN_CONFIG_INVALID #7 error root/modules 死配置(键 ∉ 引用厂商 schema properties;
19
19
  * api.md DOMAIN_CONFIG_UNUSED 行预留 warning 级,
20
20
  * 现行为 error 恒等旧谓词——doctor 门控等价)
21
21
  * INSTANCE_KEY_NOT_REGISTERED #8 error ByInstance 键/实例键 ∉ 注册表白名单
22
22
  * REALM_SWITCH_HINT #9 warning 实现覆盖跨厂商(凭据域切换确认)
23
23
  * WILDCARD_ALIGNMENT #10 warning 通配形态齐步提示
24
24
  * MULTI_INSTANCE_NO_DEFAULT v4 warning 多实例无 defaultInstanceId(无 scope 访问落全局会话)
25
- * VENDOR_NOT_INSTALLED #11 error root/domains 厂商未命中已装供给
25
+ * VENDOR_NOT_INSTALLED #11 error root/modules 厂商未命中已装供给
26
26
  * BINDING_UNRESOLVED #12 error 绑定不可解析(resolveEffectiveService 求值失败)
27
- * DOMAIN_KEY_UNMATCHED #14 warning domains 匹配域集(typo)
27
+ * IMPL_WITHOUT_PROVIDER #5/#13 error impl-only(implementation provider 绑定组头)
28
28
  * RESOURCE_TYPE_UNKNOWN #15 error modules 资源节 type ∉ 词汇表
29
29
  * RESOURCE_NOT_DECLARED #16 warning 资源键/资源 id ∉ 模块声明集
30
+ * MODULE_CONFIG_DEAD v6 error modules[m](未安装 ∨ owns=0)携带 provider/config
31
+ * (统一吸收原「#16 升级」特例——P2 一码一义)
32
+ * GLOBAL_FIELD_UNDECLARED #17 warning 全局层键过 #7 但 ∉ 引用厂商 x-tbox-global 并集(G 批;
33
+ * 零声明厂商整体跳过——#7 同遍历双判定,P4)
30
34
  * ORPHANED_CREDENTIAL 写后对账 warning 凭据文件不再被引用(toolkit 写路径产出,非本谓词)
35
+ * (v6 退役:DOMAIN_KEY_UNMATCHED #14——domains 键随归属模型退场)
31
36
  *
32
37
  * 输入形状(调用方负责装载):
33
38
  * - integrations:config/integrations.json 解析产物(宽松 Record 透传——严格 zod 校验归模板 loader)
@@ -44,28 +49,31 @@
44
49
  * 继承绑定(槽级 provider 缺席)经 effective 求值取厂商——supplies 在场时)
45
50
  * #5 实例 impl-only 禁止 + 覆盖命中供给(含槽级内联形状;provider-only = 派生位,合法)
46
51
  * #6 manifest configSchema/credentialSchema 引用文件存在性(内联与 manifest 绑定同查)
47
- * #7 root/domains 死配置:root.config/configByInstance/domains[d].config 键 ⊆ 引用厂商(并集)
48
- * 各槽 schema properties——引用面 = root.provider ∪ domains.*.provider ∪ 槽级(terse 化防退化)
52
+ * #7 root/modules 死配置:root.config/configByInstance/modules[m].config 键 ⊆ 引用厂商(并集)
53
+ * 各槽 schema properties——引用面 = root.provider ∪ modules.*.provider ∪ 槽级(terse 化防退化);
54
+ * modules 层 per-module 生效厂商 = resolveModuleBinding 单源(≡ modules[m].provider ?? root.provider)
49
55
  * #8 ByInstance/实例键 ∈ malls 白名单(v4:源 = root instances 注册表 ids——参考表在场时
50
- * 激活;'**' 通配键豁免;注册表缺席 → 跳过;domains 键 = 域前缀非 instanceId,不适用)
56
+ * 激活;'**' 通配键豁免;注册表缺席 → 跳过;modules 键 = install id 非 instanceId,不适用)
51
57
  * #9 实现覆盖跨厂商提示(realm switch——凭据域切换确认)
52
- * #11 root/domains 厂商未命中已装供给(vendor 集合 = suppliedProviders 键前缀展开)
58
+ * #11 root/modules 厂商未命中已装供给(vendor 集合 = suppliedProviders 键前缀展开)
53
59
  * #12 绑定可解析性:逐 (service, instanceId) 经 contracts resolveEffectiveService 求值
54
- * (级联单源复用,谓词零级联实现),binding-unresolved → error(supplies 缺席 → 跳过)
60
+ * (级联单源复用,谓词零级联实现;v6 ownerOf=ownership 显式参透传),binding-unresolved → error
55
61
  * #13 槽级/实例级 impl-only(implementation 需伴随 provider——provider 是绑定组头;
56
62
  * zod 之外的第二道防线——doctor 读裸 JSON)
57
- * #14 domains 匹配域集(已配置槽 需求声明的 moduleDomainOf 并集;typo → warning)
58
- * #15-#16 modules 资源节对账(v5):type RESOURCE_TYPE_IDS error(裸 JSON 预启动反馈);
59
- * 已装模块 id / 资源 id模块 manifest contributes.resources 声明集(声明非空时)
60
- * warning(moduleResourceNeeds 缺席 需求面检查跳过,M3 降级先例)
63
+ * MODULE_CONFIG_DEAD:modules[m] 携带 provider/config 且(m installedModuleIds ownership
64
+ * 无任何 service 归属 m)→ error(键级 #16 warning 与本检查可同节点共发——键级 vs 值级不同断言面)
65
+ * #17(G 批):与 #7 同遍历双判定(P4)——键 schema properties x-tbox-global 并集
66
+ * warning;引用厂商零全局声明(集空/缺席)→ 该厂商键整体跳过;检查序在 MODULE_CONFIG_DEAD
67
+ * #15-#16 modules 资源节对账(v5 延续):type ∉ RESOURCE_TYPE_IDS → error;键 ∉ 已装模块 id 集 /
68
+ * 资源 id ∉ 模块 manifest declare.resources 声明集 → warning
61
69
  */
62
70
  import type { EvaluationExports } from '../core/contracts-expected.js';
63
- import { resolveDomainBinding } from './domain-binding.js';
71
+ import { resolveModuleBinding } from './module-binding.js';
64
72
 
65
73
  /** contracts 注入面(resolver 装配产物运行时视图 = EvaluationExports 求值子集) */
66
74
  export type PredicateContracts = Pick<
67
75
  EvaluationExports,
68
- 'resolveEffectiveService' | 'buildSupplyLookup' | 'moduleDomainOf' | 'normalizeInstances' | 'RESOURCE_TYPE_IDS'
76
+ 'resolveEffectiveService' | 'buildSupplyLookup' | 'normalizeInstances' | 'RESOURCE_TYPE_IDS'
69
77
  >;
70
78
 
71
79
  /** 派生查表形状(contracts buildSupplyLookup 产物;结构等价 TIER0 SupplyLookup——谓词零 contracts 静态依赖) */
@@ -74,7 +82,6 @@ export type SupplyLookup = Record<string, readonly string[]>;
74
82
  export interface IntegrationServicePredicateInput {
75
83
  integrations: {
76
84
  provider?: string;
77
- domains?: Record<string, { provider?: string; config?: Record<string, unknown> }>;
78
85
  config?: Record<string, unknown>;
79
86
  configByInstance?: Record<string, Record<string, unknown>>;
80
87
  credentialRef?: string;
@@ -93,8 +100,8 @@ export interface IntegrationServicePredicateInput {
93
100
  instances?: Array<{ id: string; name?: string; attributes?: Record<string, unknown> }>;
94
101
  /** v4 默认实例(normalizeInstances 解析语义:显式 ?? 单候选唯一解) */
95
102
  defaultInstanceId?: string | null;
96
- /** v5 模块资源绑定(modules 节——宽松透传;严格 zod 归模板 loader) */
97
- modules?: Record<string, { resources?: Record<string, { type?: unknown }> }>;
103
+ /** v6 模块集成位(modules 节三键——宽松透传;严格 zod 归模板 loader) */
104
+ modules?: Record<string, { provider?: string; config?: Record<string, unknown>; resources?: Record<string, { type?: unknown }> }>;
98
105
  };
99
106
  /** 已声明供给(`${vendor}\u0000${implementation}` → 槽集合;credentialType 供 #2/#6 绑定侧对齐) */
100
107
  suppliedProviders: Record<string, { services: string[]; owner: string; credentialType?: string }>;
@@ -102,12 +109,20 @@ export interface IntegrationServicePredicateInput {
102
109
  demandedServices: Record<string, { optional: boolean; owners: string[] }>;
103
110
  /** 派生查表(buildSupplyLookup 产物;缺席 → #12 跳过 + #2 继承绑定不查 credentialType) */
104
111
  supplies?: SupplyLookup;
112
+ /** v6 服务归属表(walk.ownership——#12 ownerOf 透传 + MODULE_CONFIG_DEAD owns=0 判据;
113
+ * 缺席 = 归属面不可判 → MODULE_CONFIG_DEAD 退化为仅「未安装」分支) */
114
+ ownership?: Record<string, string>;
115
+ /** v6 已装模块 id 集(.tbox/app.json entry.id ∪ packages/ 目录名;缺席 → 「未安装」分支跳过) */
116
+ installedModuleIds?: ReadonlySet<string>;
105
117
  /** malls 白名单(deployments 控制面 extract;缺席 → ByInstance 键检查跳过) */
106
118
  malls?: string[];
107
119
  /** credentialType → schema 文件引用缺失(manifest 声明指向缺失文件;#6) */
108
120
  credentialSchemaMissing?: Record<string, boolean>;
109
121
  /** 厂商 → 槽 schema properties 键集(#7 用;调用方从 manifest configSchema 引用解析) */
110
122
  vendorConfigKeys?: Record<string, Set<string>>;
123
+ /** G 批:厂商 → x-tbox-global 标记键集(#17 用;#7 同循环收集——零新增 IO);
124
+ * 缺席/空集 = 该厂商零声明 → #17 对其键整体跳过 */
125
+ vendorGlobalConfigKeys?: Record<string, Set<string>>;
111
126
  /** 厂商 → configSchema 引用文件缺失(#7 跳过该厂商——无 schema 不能判死配置) */
112
127
  vendorConfigSchemaMissing?: Record<string, boolean>;
113
128
  /** 凭据文件缺失(#1:credentialRef stem → 档内 credentials/<stem>.json 在场;域中立注入) */
@@ -116,7 +131,7 @@ export interface IntegrationServicePredicateInput {
116
131
  credentialFileType?: Record<string, string | undefined>;
117
132
  /** #9:实现覆盖跨厂商提示位(CLI 侧组装) */
118
133
  realmSwitchHints?: string[];
119
- /** v5:已装模块 id → manifest contributes.resources 声明 id 集(值空集 = 无声明,资源级跳过;
134
+ /** v5:已装模块 id → manifest declare.resources 声明 id 集(值空集 = 无声明,资源级跳过;
120
135
  * 缺席 = 无需求面信息——键未知/资源未声明检查跳过,type 检查恒跑) */
121
136
  moduleResourceNeeds?: Record<string, ReadonlySet<string>>;
122
137
  }
@@ -189,7 +204,7 @@ export function evaluateIntegrationServices(
189
204
  ): IntegrationServiceIssue[] {
190
205
  const issues: IntegrationServiceIssue[] = [];
191
206
  const contracts = input.contracts;
192
- const { integrations, suppliedProviders, demandedServices, supplies, malls, credentialSchemaMissing, vendorConfigKeys, vendorConfigSchemaMissing, credentialFileMissing, credentialFileType, realmSwitchHints, moduleResourceNeeds } = input;
207
+ const { integrations, suppliedProviders, demandedServices, supplies, malls, credentialSchemaMissing, vendorConfigKeys, vendorGlobalConfigKeys, vendorConfigSchemaMissing, credentialFileMissing, credentialFileType, realmSwitchHints, moduleResourceNeeds, ownership, installedModuleIds } = input;
193
208
 
194
209
  // #8 白名单索引(malls 缺席 → 跳过 ByInstance 键检查)
195
210
  const mallsIndex = malls ? new Set(malls) : undefined;
@@ -230,10 +245,11 @@ export function evaluateIntegrationServices(
230
245
  }
231
246
  const ct = (service.provider as Record<string, unknown> | undefined)?.credentialType;
232
247
  if (typeof ct === 'string') return ct;
233
- // v3 继承绑定:effective 求值取 (provider, implementation)(supplies 在场才可判)
248
+ // v3 继承绑定:effective 求值取 (provider, implementation)(supplies 在场才可判;
249
+ // v6 ownerOf=ownership 显式参——modules 层绑定纳入求值)
234
250
  if (!supplies) return undefined;
235
251
  const sk = instanceId ?? Object.keys(service.instances ?? {})[0] ?? '';
236
- const r = contracts.resolveEffectiveService(integrations as never, serviceId, sk, supplies);
252
+ const r = contracts.resolveEffectiveService(integrations as never, serviceId, sk, supplies, ownership);
237
253
  if (r.status !== 'ok' || typeof r.effective.provider !== 'string') return undefined;
238
254
  return suppliedProviders[providerKey(r.effective.provider, r.effective.implementation)]?.credentialType;
239
255
  };
@@ -415,18 +431,18 @@ export function evaluateIntegrationServices(
415
431
  }
416
432
  }
417
433
 
418
- // #11:root/domains 厂商存在性(继承层声明必须命中已装供给——vendor 集合 = 供给键前缀展开;
434
+ // #11:root/modules 厂商存在性(继承层声明必须命中已装供给——vendor 集合 = 供给键前缀展开;
419
435
  // 槽级内联声明不在本检查范围——F5 先声明后实施闭环归 #2/#6 形状检查)
420
436
  for (const [where, vendor] of [
421
437
  ['root', integrations.provider],
422
- ...Object.entries(integrations.domains ?? {}).map(([d, cfg]): [string, string | undefined] => [`domains.${d}`, cfg?.provider]),
438
+ ...Object.entries(integrations.modules ?? {}).map(([m, cfg]): [string, string | undefined] => [`modules.${m}`, cfg?.provider]),
423
439
  ] as Array<[string, string | undefined]>) {
424
440
  if (typeof vendor !== 'string' || vendor.length === 0) continue;
425
441
  if (!installedVendors.has(vendor)) {
426
442
  issues.push({
427
443
  severity: 'error',
428
444
  code: 'VENDOR_NOT_INSTALLED',
429
- module: where.startsWith('domains.') ? where.slice('domains.'.length) : undefined,
445
+ module: where.startsWith('modules.') ? where.slice('modules.'.length) : undefined,
430
446
  message: `integrations: ${where} provider ${vendor} 未命中已装供给——装对应 provider 包或 provider use 改绑`,
431
447
  });
432
448
  }
@@ -434,12 +450,13 @@ export function evaluateIntegrationServices(
434
450
 
435
451
  // #12:绑定可解析性(级联单源复用 contracts resolveEffectiveService——谓词零级联实现;
436
452
  // supplies 缺席 → 整体跳过,M3 降级兼容 F5 页面无 catalog 形态。
437
- // v4:instances 键缺席槽 = 通配激活——以 '' 探测求值(继承链可解析性仍需覆盖))
453
+ // v4:instances 键缺席槽 = 通配激活——以 '' 探测求值(继承链可解析性仍需覆盖);
454
+ // v6:ownership 作 ownerOf 显式参透传——modules 层绑定/配置纳入求值与 fail-visible)
438
455
  if (supplies) {
439
456
  for (const [serviceId, service] of Object.entries(integrations.services)) {
440
457
  const instanceIds = service.instances ? Object.keys(service.instances) : [''];
441
458
  for (const instanceId of instanceIds) {
442
- const r = contracts.resolveEffectiveService(integrations as never, serviceId, instanceId, supplies);
459
+ const r = contracts.resolveEffectiveService(integrations as never, serviceId, instanceId, supplies, ownership);
443
460
  if (r.status === 'binding-unresolved') {
444
461
  issues.push({ severity: 'error', code: 'BINDING_UNRESOLVED', service: serviceId, instance: instanceId || undefined, message: `integrations: ${r.message}` });
445
462
  }
@@ -459,30 +476,40 @@ export function evaluateIntegrationServices(
459
476
  }
460
477
  }
461
478
 
462
- // #14:domains 匹配域集(已配置槽 需求声明的 moduleDomainOf 并集;
463
- // typo = 永不生效的静默死配置 warning 级,staged 域(先行声明)不告警)
464
- const matchedDomains = new Set<string>([
465
- ...Object.keys(integrations.services).map((s) => contracts.moduleDomainOf(s)),
466
- ...Object.keys(demandedServices).map((s) => contracts.moduleDomainOf(s)),
467
- ]);
468
- for (const d of Object.keys(integrations.domains ?? {})) {
469
- if (!matchedDomains.has(d)) {
479
+ // MODULE_CONFIG_DEAD(v6):modules[m] 携带 provider/config 且(未安装 owns=0)→ error。
480
+ // 统一定义吸收原「#16 升级」特例(P2 一码一义);与 #16 可同节点共发(键级 vs 值级不同断言面)。
481
+ // 依赖面双保险:installedModuleIds/ownership 缺席 对应分支跳过(谓词纯函数——面信息由调用方喂)。
482
+ for (const [moduleId, cfg] of Object.entries(integrations.modules ?? {})) {
483
+ const carriesBinding = cfg?.provider !== undefined || cfg?.config !== undefined;
484
+ if (!carriesBinding) continue;
485
+ const notInstalled = installedModuleIds ? !installedModuleIds.has(moduleId) : false;
486
+ let ownsCount = 0;
487
+ if (ownership) {
488
+ for (const owner of Object.values(ownership)) if (owner === moduleId) ownsCount += 1;
489
+ }
490
+ const ownsUnknown = ownership === undefined;
491
+ if (notInstalled || (!ownsUnknown && ownsCount === 0)) {
492
+ const why = notInstalled ? '未安装(键 = install id)' : '未声明任何 owns(无模块级配置位)';
470
493
  issues.push({
471
- severity: 'warning',
472
- code: 'DOMAIN_KEY_UNMATCHED',
473
- module: d,
474
- message: `integrations: domains.${d} 无对应槽位或需求声明(域前缀拼写错误?——键 = 槽位 id 点分前缀)`,
494
+ severity: 'error',
495
+ code: 'MODULE_CONFIG_DEAD',
496
+ module: moduleId,
497
+ message: `integrations: modules.${moduleId} 携带 provider/config 但模块${why}(MODULE_CONFIG_DEAD——模块级集成位作用于 declare.owns 服务,改 declare.owns 或删除该节点)`,
475
498
  });
476
499
  }
477
500
  }
478
501
 
479
- // #7:root/domains 死配置 —— root.config/configByInstance/domains[d].config 键 ⊆ 引用厂商
502
+ // #7:root/modules 死配置 —— root.config/configByInstance/modules[m].config 键 ⊆ 引用厂商
480
503
  // (跨厂商取并集)各槽 schema properties 键集。并集语义:混布部署中属 A 厂商的 root 键
481
- // 不应被 B 厂商判死。引用面(v3)= root.provider ∪ domains.*.provider ∪ 槽级(含内联
482
- // .provider)——terse 化后槽级双字段退场,引用面不上移即静默漏检(N2)。
504
+ // 不应被 B 厂商判死。引用面(v3 延续 + v6 面)= root.provider ∪ modules.*.provider ∪
505
+ // 槽级(含内联 .provider)——terse 化后槽级双字段退场,引用面不上移即静默漏检(N2)。
483
506
  // 保守跳过条件(无法确证即不判死——误报会阻断部署):任一引用厂商 configSchema 文件缺失,
484
507
  // 或被绑定但零 configSchema 声明(如 local 供给未声明 configSchema——无 schema 知识。
485
- // 注:provider-mock 0.3.0 起全槽带 schema,mock 绑定下本检查已生效)。
508
+ // 注:provider-mock 0.5.0 起零 configSchema(保守跳过);config 检查对 mocktest 绑定
509
+ // 生效(全槽 schema——2026-09-20 拆分迁入 provider-mocktest)。
510
+ // #17(G 批,与 #7 同遍历双判定 P4):键 ∈ schema properties 但 ∉ 该层生效厂商
511
+ // x-tbox-global 并集 → GLOBAL_FIELD_UNDECLARED warning;生效厂商零声明(集空/缺席)→
512
+ // 整体跳过(零声明厂商不告警)。检查序:#7 判定先行,过 #7 才进 #17(N5)。
486
513
  if (vendorConfigKeys) {
487
514
  const referenced = new Set<string>();
488
515
  let schemaIncomplete = false;
@@ -494,7 +521,7 @@ export function evaluateIntegrationServices(
494
521
  if (vendorConfigSchemaMissing?.[vendor]) schemaIncomplete = true;
495
522
  };
496
523
  noteVendor(integrations.provider);
497
- for (const cfg of Object.values(integrations.domains ?? {})) noteVendor(cfg?.provider);
524
+ for (const cfg of Object.values(integrations.modules ?? {})) noteVendor(cfg?.provider);
498
525
  for (const slot of Object.values(integrations.services)) {
499
526
  const inline = slot.provider as Record<string, unknown> | undefined;
500
527
  noteVendor(typeof slot.provider === 'string' ? slot.provider : inline?.provider as string | undefined);
@@ -502,7 +529,10 @@ export function evaluateIntegrationServices(
502
529
  if (referenced.size > 0 && !schemaIncomplete) {
503
530
  const allowed = new Set<string>();
504
531
  for (const v of referenced) for (const k of vendorConfigKeys[v] ?? []) allowed.add(k);
505
- const checkLayer = (layer: Record<string, unknown> | undefined, where: string, allowedKeys?: Set<string>): void => {
532
+ // 全局并集(root 层):引用厂商 x-tbox-global 键并集;空 = 全体零声明 root #17 跳过
533
+ const globalUnion = new Set<string>();
534
+ for (const v of referenced) for (const k of vendorGlobalConfigKeys?.[v] ?? []) globalUnion.add(k);
535
+ const checkLayer = (layer: Record<string, unknown> | undefined, where: string, allowedKeys?: Set<string>, globalKeys?: Set<string>): void => {
506
536
  for (const [k] of Object.entries(layer ?? {})) {
507
537
  if (!(allowedKeys ?? allowed).has(k)) {
508
538
  issues.push({
@@ -510,24 +540,34 @@ export function evaluateIntegrationServices(
510
540
  code: 'DOMAIN_CONFIG_INVALID',
511
541
  message: `integrations: ${where} 键 ${k} 不在引用厂商(${[...referenced].join('/')})任何槽 schema properties 内(死配置)`,
512
542
  });
543
+ } else if (globalKeys && globalKeys.size > 0 && !globalKeys.has(k)) {
544
+ // #17:过 #7 但未声明全局——仅表单收窄提示(已配置值全链保全,解析不拒)
545
+ issues.push({
546
+ severity: 'warning',
547
+ code: 'GLOBAL_FIELD_UNDECLARED',
548
+ message: `integrations: ${where} 键 ${k} 过 schema 校验但未声明 x-tbox-global(不进应用/模块全局表单;已配置值照常生效——如需全局编辑请在 provider schema 标记)`,
549
+ });
513
550
  }
514
551
  }
515
552
  };
516
- checkLayer(integrations.config, 'root.config');
517
- for (const [k, v] of Object.entries(integrations.configByInstance ?? {})) checkLayer(v, `root.configByInstance[${k}]`);
518
- // 域级 config:键 ⊆ 该域生效厂商 schema(resolveDomainBinding 单源——v4.4 F1 等价重构,
519
- // 语义 ≡ 现状 domains[d].provider ?? root.provider)——生效厂商缺席(全走槽级覆盖)→
520
- // 用全局并集;生效厂商 schema 缺失 → 跳过该域
521
- for (const [d, cfg] of Object.entries(integrations.domains ?? {})) {
553
+ checkLayer(integrations.config, 'root.config', undefined, globalUnion);
554
+ for (const [k, v] of Object.entries(integrations.configByInstance ?? {})) {
555
+ checkLayer(v, `root.configByInstance[${k}]`, undefined, globalUnion);
556
+ }
557
+ // 模块层 config:键 ⊆ 该模块生效厂商 schema(resolveModuleBinding 单源——v6 语义
558
+ // modules[m].provider ?? root.provider)——生效厂商缺席(全走槽级覆盖)→
559
+ // 用全局并集;生效厂商 schema 缺失 → 跳过该模块
560
+ for (const [m, cfg] of Object.entries(integrations.modules ?? {})) {
522
561
  if (!cfg?.config) continue;
523
- const domainVendor = resolveDomainBinding(integrations, d).provider ?? undefined;
524
- if (domainVendor === undefined) {
525
- checkLayer(cfg.config, `domains.${d}.config`);
562
+ const moduleVendor = resolveModuleBinding(integrations as never, m).provider ?? undefined;
563
+ if (moduleVendor === undefined) {
564
+ checkLayer(cfg.config, `modules.${m}.config`, undefined, globalUnion);
526
565
  continue;
527
566
  }
528
- const vendorKeys = vendorConfigKeys[domainVendor];
529
- if (!vendorKeys || vendorConfigSchemaMissing?.[domainVendor]) continue;
530
- checkLayer(cfg.config, `domains.${d}.config`, new Set(vendorKeys));
567
+ const vendorKeys = vendorConfigKeys[moduleVendor];
568
+ if (!vendorKeys || vendorConfigSchemaMissing?.[moduleVendor]) continue;
569
+ const vendorGlobals = vendorGlobalConfigKeys?.[moduleVendor];
570
+ checkLayer(cfg.config, `modules.${m}.config`, new Set(vendorKeys), vendorGlobals);
531
571
  }
532
572
  }
533
573
  }
@@ -585,7 +625,7 @@ export function evaluateIntegrationServices(
585
625
  severity: 'warning',
586
626
  code: 'RESOURCE_NOT_DECLARED',
587
627
  module: moduleId,
588
- message: `integrations: modules.${moduleId}.resources.${resourceId} 未被模块 manifest contributes.resources 声明`,
628
+ message: `integrations: modules.${moduleId}.resources.${resourceId} 未被模块 manifest declare.resources 声明`,
589
629
  });
590
630
  }
591
631
  }
@@ -6,7 +6,6 @@ import { expandEnvVars } from '../core/env-expansion.js';
6
6
  import { writeAtomic } from '../core/fskit.js';
7
7
  import { walkManifests } from '../assembly/walk-manifests.js';
8
8
  import { readIntegrationsConfig } from './read.js';
9
- import { domainOfService } from './domain-binding.js';
10
9
  import { isControlledInlineProvider } from './predicate.js';
11
10
  import { reconcileOrphanCredentials } from './credentials.js';
12
11
  import { evaluateAppIntegrationIssues } from './evaluate-app.js';
@@ -16,14 +15,16 @@ import { evaluateAppIntegrationIssues } from './evaluate-app.js';
16
15
  *
17
16
  * 不变式:**删除判据 ≡ doctor integration-services 报错判据(构造性同源)**——消费 doctor 同一
18
17
  * 共享求值器 evaluateAppIntegrationIssues 的结构化 issue,code 白名单
19
- * {VENDOR_NOT_INSTALLED, BINDING_UNRESOLVED} + 守卫(绑定引用链候选 provider ∈ installedVendors
20
- * 即保留,排除活厂商错配)→ 删除域严格 = "供给载体消亡"。
18
+ * {VENDOR_NOT_INSTALLED, BINDING_UNRESOLVED, MODULE_CONFIG_DEAD} + 守卫(绑定引用链候选
19
+ * provider ∈ installedVendors 即保留,排除活厂商错配)→ 删除域严格 = "供给载体消亡"。
20
+ * v6:模块卸载 → modules[m] **整节点确定性清理**(provider+config+resources——D4:
21
+ * 模块消亡清理 vs 集成位编辑保全不矛盾;MODULE_CONFIG_DEAD 为判据源)。
21
22
  *
22
23
  * 由此推论(均已实证):需求侧孤儿绑定(demand 消失、供给在场)保留——doctor 判其合法;用户
23
24
  * 预配置绑定受保护;一次 remove 自愈全文件历史死债;凭据类 issue(CREDENTIAL_FILE_MISSING 等)
24
25
  * 刻意不进白名单,原样保留。
25
26
  *
26
- * 写侧三防线(镜像 write core):N1 归一化(空模块节点删 domains 键)→ parseIntegrationsConfig
27
+ * 写侧三防线(镜像 write core):归一化(空节点删键)→ parseIntegrationsConfig
27
28
  * 严格校验(失败弃写)→ writeAtomic。凭据孤儿只报告不清理(D9——与 DELETE 管线现行为一致)。
28
29
  * 全空(无任何存活绑定)→ 删文件——ensureMockBindings(缺席即生成)在重装供给时再生闭环。
29
30
  *
@@ -43,7 +44,6 @@ export interface PruneResult {
43
44
  /** 宽松形状(谓词输入同族子集——doctor 读裸 JSON 先例,严格 zod 归模板 loader) */
44
45
  interface PruneDoc {
45
46
  provider?: string;
46
- domains?: Record<string, { provider?: string; config?: Record<string, unknown> }>;
47
47
  config?: Record<string, unknown>;
48
48
  configByInstance?: Record<string, Record<string, unknown>>;
49
49
  credentialRef?: string;
@@ -102,7 +102,9 @@ export async function pruneIntegrationBindings(appDir: string): Promise<PruneRes
102
102
  return { removed: [], orphanCredentials: [], skipped: 'predicate-unavailable' };
103
103
  }
104
104
 
105
- const installedVendors = new Set(Object.keys(walkManifests(appDir).catalog.providers));
105
+ const walk = walkManifests(appDir);
106
+ const installedVendors = new Set(Object.keys(walk.catalog.providers));
107
+ const walkOwnership = walk.ownership;
106
108
  const next = structuredClone(raw) as PruneDoc;
107
109
  next.services ??= {};
108
110
  const removed: string[] = [];
@@ -111,24 +113,42 @@ export async function pruneIntegrationBindings(appDir: string): Promise<PruneRes
111
113
  if (issue.severity !== 'error') continue;
112
114
  if (issue.code === 'VENDOR_NOT_INSTALLED') {
113
115
  if (issue.module) {
114
- const d = next.domains?.[issue.module];
115
- if (d?.provider) {
116
- removed.push(`domains.${issue.module}.provider=${d.provider}`);
117
- delete d.provider;
118
- if (Object.keys(d).length === 0) delete next.domains![issue.module]; // N1
116
+ // v6:modules[owner].provider 死厂商 → 整节点删除前提下的 provider 键清理;
117
+ // provider(无 config/resources)→ 节点删除(N1 归一)
118
+ const modulesNode = (next.modules ?? {}) as Record<string, Record<string, unknown>>;
119
+ const node = modulesNode[issue.module];
120
+ if (node?.provider) {
121
+ removed.push(`modules.${issue.module}.provider=${String(node.provider)}`);
122
+ delete node.provider;
123
+ if (Object.keys(node).length === 0) delete next.modules![issue.module]; // 归一
119
124
  }
120
125
  } else if (next.provider) {
121
126
  removed.push(`root.provider=${next.provider}`);
122
127
  delete next.provider;
123
128
  }
129
+ } else if (issue.code === 'MODULE_CONFIG_DEAD' && issue.module) {
130
+ // v6 D4:模块卸载 → modules[m] 整节点确定性清理(provider+config+resources)
131
+ const modulesNode = (next.modules ?? {}) as Record<string, Record<string, unknown>>;
132
+ if (modulesNode[issue.module]) {
133
+ const node = modulesNode[issue.module];
134
+ const parts = [
135
+ node.provider !== undefined ? 'provider' : undefined,
136
+ node.config !== undefined ? 'config' : undefined,
137
+ node.resources !== undefined ? 'resources' : undefined,
138
+ ].filter(Boolean);
139
+ removed.push(`modules.${issue.module}(${parts.join('+')} 整节点)`);
140
+ delete next.modules![issue.module];
141
+ }
124
142
  } else if (issue.code === 'BINDING_UNRESOLVED' && issue.service) {
125
143
  const s = next.services[issue.service];
126
144
  if (!s) continue;
127
- // 守卫:引用链任一候选 ∈ installedVendors → 活厂商错配,保留(doctor 报错语义不变)
145
+ // 守卫:引用链任一候选 ∈ installedVendors → 活厂商错配,保留(doctor 报错语义不变;
146
+ // v6 引用链 = 实例级/槽级/modules[ownerOf]/root 四层)
147
+ const owner = walkOwnership[issue.service];
128
148
  const candidates = [
129
149
  issue.instance ? providerOf(s.instances?.[issue.instance]?.provider) : undefined,
130
150
  providerOf(s.provider),
131
- providerOf(next.domains?.[domainOfService(issue.service)]?.provider),
151
+ owner !== undefined ? providerOf((next.modules as Record<string, { provider?: unknown }> | undefined)?.[owner]?.provider) : undefined,
132
152
  providerOf(next.provider),
133
153
  ].filter((v): v is string => typeof v === 'string');
134
154
  if (candidates.some((v) => installedVendors.has(v))) continue;
@@ -151,7 +171,6 @@ export async function pruneIntegrationBindings(appDir: string): Promise<PruneRes
151
171
  // 全空判定先行(**先于严格校验**——空文档 `{}` 缺必填 services 键必然被 zod 拒;删文件免校验)
152
172
  const empty =
153
173
  !next.provider &&
154
- Object.keys(next.domains ?? {}).length === 0 &&
155
174
  Object.keys(next.services ?? {}).length === 0 &&
156
175
  Object.keys(next.config ?? {}).length === 0 &&
157
176
  Object.keys(next.configByInstance ?? {}).length === 0 &&
@@ -19,8 +19,9 @@ import {
19
19
  import type { IntegrationServiceIssue } from './predicate.js';
20
20
  import { checkInstanceKeys } from './predicate.js';
21
21
  import { isKnownService } from './read.js';
22
- import { displayOwnerName, moduleDomainWritable } from './domain-binding.js';
22
+ import { moduleOwnershipWritable } from './module-binding.js';
23
23
  import { AppToolkitError } from '../core/errors.js';
24
+ import { MODULE_INTEGRATION_NODE_KEYS } from '../dto.js';
24
25
 
25
26
  /**
26
27
  * 写核心(C3-4 write;FX-1c 管线重排——F3 九步定序 + D-B2 语义 + F4 输入扩位):
@@ -231,7 +232,8 @@ export interface WriteCore {
231
232
  warmup(): Promise<void>;
232
233
  /** PUT /app/integration(root 节点全集) */
233
234
  applyApp(node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
234
- /** PUT /modules/:module/integration(domains 换算;N1 {} 删域键;v4.4 D34 写守卫单源) */
235
+ /** PUT /modules/:module/integration(v6 modules[owner] 集成位;键集纪律见 MODULE_INTEGRATION_NODE_KEYS;
236
+ * N1 {} 清 provider/config 保 resources;moduleOwnershipWritable 写守卫单源) */
235
237
  applyModule(moduleId: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
236
238
  /** PUT /services/:service/integration(整节点含 instances map;N3 {} 不归一化;v4.4 A2 身份判据) */
237
239
  applyService(service: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
@@ -336,7 +338,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
336
338
  message: 'config/integrations.json 缺席——已物化骨架(写时自动初始化;R13 语义翻转提示)',
337
339
  });
338
340
  }
339
- // root 全量替换(D14 无例外语义):缺席根键删除(services/domains/modules 域不在根键集——随 base 保留),
341
+ // root 全量替换(D14 无例外语义):缺席根键删除(services/modules 域不在根键集——随 base 保留;v6 domains 键已退役),
340
342
  // node 全键置入(未知键交严格 zod 拒——fail-visible;B10:原实现 spread+merge 残留 stale 根键)
341
343
  const next: Record<string, unknown> = { ...base };
342
344
  for (const k of APP_INTEGRATION_NODE_KEYS) if (!(k in node)) delete next[k];
@@ -370,37 +372,27 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
370
372
 
371
373
  applyModule(moduleId: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
372
374
  assertSafeNames(node, 'root');
373
- // D34 写守卫(v4.4,moduleDomainWritable 单源):404(模块在册)→ 400(多域/零域/共享键
374
- // 三分支)→ 既有管线。守卫先于 no-op 短路——多域/零域/共享键模块 PUT {} 清域同 400
375
- // (「写路径拒绝歧义操作」含清除;P13/P17)。
376
- const { modules, catalog } = walkManifests(appDir, cache);
375
+ // v6 写守卫(moduleOwnershipWritable 单源):404(模块在册)→ 400(zero-ownership——
376
+ // 模块级集成位作用于 declare.owns 服务,零归属模块无配置位)→ 既有管线。
377
+ // 守卫先于 no-op 短路——零归属模块 PUT {} 清位同 400(「写路径拒绝歧义操作」含清除)。
378
+ const { modules, catalog, ownership } = walkManifests(appDir, cache);
377
379
  const mod = modules.find((m) => m.id === moduleId);
378
380
  if (!mod) {
379
381
  throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
380
382
  }
381
- const w = moduleDomainWritable({ modules, catalog }, mod);
383
+ const w = moduleOwnershipWritable({ modules, catalog, ownership }, mod);
382
384
  if (!w.writable) {
383
- if (w.reason === 'multi-domain') {
384
- throw new AppToolkitError(
385
- 'VALIDATION_FAILED',
386
- 400,
387
- `模块 ${moduleId} 跨 ${w.domainKeys.length} 个域(${w.domainKeys.join('、')})——模块级统一配置仅支持单域模块,请按服务级配置(编辑各服务行)`,
388
- );
389
- }
390
- if (w.reason === 'zero-domain') {
391
- throw new AppToolkitError('VALIDATION_FAILED', 400, `模块 ${moduleId} 无服务需求声明,无域配置位`);
392
- }
393
385
  throw new AppToolkitError(
394
386
  'VALIDATION_FAILED',
395
387
  400,
396
- `域键 ${w.domainKey} ${w.coOwners.map((o) => displayOwnerName(modules, o)).join('、')} 共用——写通道拒绝歧义操作,请按服务级配置`,
388
+ `模块 ${moduleId} 未声明任何 declare.owns——无模块级配置位(模块级 provider/config 作用于归属服务)`,
397
389
  );
398
390
  }
399
- const domainKey = w.domainKey;
400
391
  const issues: IntegrationServiceIssue[] = [];
401
392
  const current = loadConfig(appDir, cache);
393
+ // N2 语义(v6):空节点 = 清 provider/config 两键,resources 随既有节点保全;
394
+ // 文件缺席 + 空节点 = no-op(幂等)
402
395
  if (current === null && Object.keys(node).length === 0) {
403
- // 空文件清空域 = no-op(N1 幂等)
404
396
  return { written: false, restartScheduled: false, files: [], credentials: [], issues };
405
397
  }
406
398
  if (current === null) {
@@ -411,15 +403,39 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
411
403
  });
412
404
  }
413
405
  const base: Record<string, unknown> = current ?? { services: {} };
414
- const domains = { ...((base.domains as Record<string, unknown>) ?? {}) };
415
- const next: Record<string, unknown> = { ...base, domains };
416
- // N1:模块节点 {} domains[键](清域);其余 = 域键全量替换
417
- if (Object.keys(node).length === 0) delete domains[domainKey];
418
- else domains[domainKey] = { ...node };
406
+ const modulesNode = { ...((base.modules as Record<string, unknown>) ?? {}) };
407
+ const existing = { ...((modulesNode[moduleId] as Record<string, unknown> | undefined) ?? {}) };
408
+ // 键集纪律:node 内仅接受 MODULE_INTEGRATION_NODE_KEYS 成员(未知键交严格 zod 拒——fail-visible)
409
+ for (const k of Object.keys(node)) {
410
+ if (!(MODULE_INTEGRATION_NODE_KEYS as readonly string[]).includes(k)) {
411
+ throw new AppToolkitError(
412
+ 'VALIDATION_FAILED',
413
+ 400,
414
+ `模块集成位仅接受 ${MODULE_INTEGRATION_NODE_KEYS.join('/')} 键(收到 ${k})`,
415
+ [k],
416
+ );
417
+ }
418
+ }
419
+ // 全量替换(键集内):缺席键删除;空对象/空串值 = 删除(≡ 缺席——模块节点不留空键);
420
+ // PUT {} = 清 provider/config,resources 恒随 existing 保全(N2:清集成位不杀资源绑定)
421
+ const nextNode: Record<string, unknown> = { ...existing };
422
+ for (const k of MODULE_INTEGRATION_NODE_KEYS) {
423
+ const v = node[k];
424
+ const isEmptyish = v === undefined || v === null || v === ''
425
+ || (typeof v === 'object' && !Array.isArray(v) && Object.keys(v as object).length === 0);
426
+ if (isEmptyish) delete nextNode[k];
427
+ else nextNode[k] = v;
428
+ }
429
+ if (Object.keys(nextNode).length === 0) {
430
+ delete modulesNode[moduleId];
431
+ } else {
432
+ modulesNode[moduleId] = nextNode;
433
+ }
434
+ const next: Record<string, unknown> = { ...base, modules: modulesNode };
419
435
  if (deepEqual(current, next)) {
420
436
  return { written: false, restartScheduled: false, files: [], credentials: [], issues };
421
437
  }
422
- runStrictValidation(next, issues, `domains.${domainKey}.`);
438
+ runStrictValidation(next, issues, `modules.${moduleId}.`);
423
439
  issues.push(...registryCheck(next));
424
440
  return finalize(next, [], issues, writeOpts.dryRun);
425
441
  },
package/src/views/app.ts CHANGED
@@ -5,21 +5,21 @@ import { moduleOccupiedServices, resolveServiceDisplay, type ViewSnapshot } from
5
5
  /**
6
6
  * 应用静态投影(views/app;C4;FX-1c B10——root 节点投影修;v4.4 D29——services 双端点同型)。
7
7
  * /app = 平台服务 + root 节点原值(纯静态):services = 服务词汇 − 模块占用
8
- * (ServiceDemand[]——平台条目 required 恒 false、title/description 缺席,D29);
8
+ * (ServiceDemand[]——平台条目 required 恒 false、name/description 缺席,D29);
9
9
  * 平台服务状态不在此(→ /service-resolutions);模块服务不在此(→ /modules/:m)。
10
10
  * integration = **root 节点投影**(APP_INTEGRATION_NODE_KEYS——D21/L1:GET 预填 ≡ PUT body;
11
- * 文件另含 services/domains/modules 域键,不进 root 节点投影——原实现整文件透传,
11
+ * 文件另含 services/modules 域键,不进 root 节点投影——原实现整文件透传,
12
12
  * PUT 回会级联写回域键);文件缺席 = null,文件在场 → 投影对象(可为空)。
13
13
  */
14
14
 
15
15
  export function loadAppView(snapshot: ViewSnapshot): AppView {
16
16
  const occupied = moduleOccupiedServices(snapshot.walk);
17
- // 平台条目展示名走链(B1):词汇 meta > demand meta > id——title 恒发(id 兜底内聚)
17
+ // 平台条目展示名走链(B1):词汇 meta > demand meta > id——name 恒发(id 兜底内聚)
18
18
  const services: ServiceDemand[] = snapshot.walk.vocabulary
19
19
  .filter((s) => !occupied.has(s))
20
20
  .map((s) => {
21
21
  const display = resolveServiceDisplay(snapshot.walk, s);
22
- return { service: s, title: display.title, ...(display.description !== undefined ? { description: display.description } : {}), required: false };
22
+ return { service: s, name: display.name, ...(display.description !== undefined ? { description: display.description } : {}), required: false };
23
23
  });
24
24
  const config = snapshot.integrations;
25
25
  let integration: AppView['integration'] = null;