@tbox.cn/app-toolkit 0.4.0 → 0.6.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 (40) 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 +374 -150
  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 +90 -14
  12. package/src/dto.ts +80 -33
  13. package/src/factory.ts +78 -26
  14. package/src/index.ts +16 -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 +47 -27
  21. package/src/views/app.ts +4 -4
  22. package/src/views/context.ts +15 -14
  23. package/src/views/integration-schemas.ts +76 -18
  24. package/src/views/modules.ts +93 -25
  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-resources.test.ts +148 -0
  33. package/tests/module-schema.test.ts +120 -20
  34. package/tests/resolve-key.test.ts +154 -0
  35. package/tests/vendor-schema-keywords.test.ts +74 -0
  36. package/tests/views.test.ts +585 -247
  37. package/tests/write-core.test.ts +117 -0
  38. package/dist/chunk-KPD3LUH4.js +0 -1
  39. package/dist/contracts-resolver-E4SECBOG.js +0 -1
  40. package/src/integrations/domain-binding.ts +0 -81
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { createAppToolkit, type AppToolkit } from '../src/factory.js';
6
6
  import { invalidateContractsResolver } from '../src/core/contracts-resolver.js';
7
7
  import { walkManifests, type WalkManifestsResult } from '../src/assembly/walk-manifests.js';
8
- import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas } from '../src/views/integration-schemas.js';
8
+ import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas, loadServiceFieldRegistry, loadServiceIntegrationSchemas } from '../src/views/integration-schemas.js';
9
9
  import { resolveServiceDisplay } from '../src/views/context.js';
10
10
  import type { ViewSnapshot } from '../src/views/context.js';
11
11
  import { URL_METHOD_MAP } from './naming-alignment.test.js';
@@ -42,11 +42,12 @@ function harnessWithContracts(
42
42
  'packages/module-parking/tbox.module.json',
43
43
  JSON.stringify({
44
44
  schemaVersion: 1,
45
- name: 'module-parking',
45
+ id: 'module-parking',
46
46
  version: '0.1.0',
47
47
  kind: 'business',
48
- contributes: {
49
- services: [{ service: 'parking.query', optional: false, title: '车位查询' }],
48
+ declare: {
49
+ owns: [{ service: 'parking.query' }],
50
+ consumes: [{ service: 'parking.query', optional: false, name: '车位查询' }],
50
51
  },
51
52
  dependencies: { modules: [] },
52
53
  env: [],
@@ -62,7 +63,7 @@ function harnessWithContracts(
62
63
  { id: 'mall-sh-01', name: '上海店' },
63
64
  ],
64
65
  defaultInstanceId: 'mall-bj-01',
65
- domains: { parking: { provider: 'wanda' } },
66
+ modules: { 'module-parking': { provider: 'wanda' } },
66
67
  services: {
67
68
  'parking.query': { provider: { provider: 'my-custom-api', credentialType: '-' }, implementation: 'my-custom-parking@1' },
68
69
  'auth.alipay-login': {},
@@ -104,9 +105,12 @@ function layerHarness(): {
104
105
  ].join(' '),
105
106
  });
106
107
  linkContracts(app.appDir, store, { subdir: 'apps/server' });
108
+ // G 批:fixture 全字段标 x-tbox-global(表单收窄过滤后 E1/E2/E5 字段面逐字不变);
109
+ // wanda parking.json 另含未标记键 parkLotId(P16 负例——layer.properties 在场、registry.fields 不收)
110
+ const g = (sub: Record<string, unknown>): Record<string, unknown> => ({ ...sub, 'x-tbox-global': true });
107
111
  const schemas = {
108
- query: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
109
- parking: { type: 'object', properties: { mallId: { type: 'string' } } },
112
+ query: { type: 'object', properties: { baseUrl: g({ type: 'string', format: 'uri' }) }, required: ['baseUrl'] },
113
+ parking: { type: 'object', properties: { mallId: g({ type: 'string' }), parkLotId: { type: 'string', title: '车场组' } } },
110
114
  };
111
115
  // v4.6 joycity 代数 fixture:R1(baseUrl 双无 enum / env 同集异序 / envMixed 混布)/
112
116
  // R2(options 深相等 / meta 异构)/ R3(lotId string≠integer / timeout number≠integer / untyped 无 type)/
@@ -115,14 +119,14 @@ function layerHarness(): {
115
119
  query: {
116
120
  type: 'object',
117
121
  properties: {
118
- baseUrl: { type: 'string', format: 'uri' },
119
- lotId: { type: 'string' },
120
- env: { type: 'string', enum: ['dev', 'staging', 'prod'] },
121
- envMixed: { type: 'string', enum: ['a', 'b'] },
122
- meta: { type: 'object', properties: { region: { type: 'string' } } },
123
- options: { type: 'object', properties: { x: { type: 'string' } } },
124
- untyped: { description: 'x' },
125
- timeout: { type: 'number' },
122
+ baseUrl: { 'x-tbox-global': true, type: 'string', format: 'uri' },
123
+ lotId: { 'x-tbox-global': true, type: 'string' },
124
+ env: { 'x-tbox-global': true, type: 'string', enum: ['dev', 'staging', 'prod'] },
125
+ envMixed: { 'x-tbox-global': true, type: 'string', enum: ['a', 'b'] },
126
+ meta: { 'x-tbox-global': true, type: 'object', properties: { region: { 'x-tbox-global': true, type: 'string' } } },
127
+ options: { 'x-tbox-global': true, type: 'object', properties: { x: { 'x-tbox-global': true, type: 'string' } } },
128
+ untyped: { 'x-tbox-global': true, description: 'x' },
129
+ timeout: { 'x-tbox-global': true, type: 'number' },
126
130
  },
127
131
  required: ['baseUrl', 'lotId'],
128
132
  },
@@ -130,15 +134,15 @@ function layerHarness(): {
130
134
  type: 'object',
131
135
  additionalProperties: false,
132
136
  properties: {
133
- apiKey: { type: 'string' },
134
- baseUrl: { type: 'string' },
135
- lotId: { type: 'integer' },
136
- env: { type: 'string', enum: ['prod', 'dev', 'staging'] },
137
- envMixed: { type: 'string' },
138
- meta: { type: 'object', properties: { zone: { type: 'number' } } },
139
- options: { type: 'object', properties: { x: { type: 'string' } } },
140
- untyped: { type: 'string' },
141
- timeout: { type: 'integer' },
137
+ apiKey: { 'x-tbox-global': true, type: 'string' },
138
+ baseUrl: { 'x-tbox-global': true, type: 'string' },
139
+ lotId: { 'x-tbox-global': true, type: 'integer' },
140
+ env: { 'x-tbox-global': true, type: 'string', enum: ['prod', 'dev', 'staging'] },
141
+ envMixed: { 'x-tbox-global': true, type: 'string' },
142
+ meta: { 'x-tbox-global': true, type: 'object', properties: { zone: { 'x-tbox-global': true, type: 'number' } } },
143
+ options: { 'x-tbox-global': true, type: 'object', properties: { x: { 'x-tbox-global': true, type: 'string' } } },
144
+ untyped: { 'x-tbox-global': true, type: 'string' },
145
+ timeout: { 'x-tbox-global': true, type: 'integer' },
142
146
  },
143
147
  required: ['apiKey', 'options'],
144
148
  },
@@ -148,10 +152,10 @@ function layerHarness(): {
148
152
  'packages/provider-w/tbox.module.json',
149
153
  JSON.stringify({
150
154
  schemaVersion: 1,
151
- name: 'provider-w',
155
+ id: 'provider-w',
152
156
  version: '0.1.0',
153
157
  kind: 'business',
154
- contributes: {
158
+ declare: {
155
159
  providers: {
156
160
  slots: [
157
161
  { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
@@ -171,10 +175,10 @@ function layerHarness(): {
171
175
  'packages/provider-j/tbox.module.json',
172
176
  JSON.stringify({
173
177
  schemaVersion: 1,
174
- name: 'provider-j',
178
+ id: 'provider-j',
175
179
  version: '0.1.0',
176
180
  kind: 'business',
177
- contributes: {
181
+ declare: {
178
182
  providers: {
179
183
  slots: [
180
184
  { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
@@ -194,10 +198,10 @@ function layerHarness(): {
194
198
  'packages/provider-p/tbox.module.json',
195
199
  JSON.stringify({
196
200
  schemaVersion: 1,
197
- name: 'provider-p',
201
+ id: 'provider-p',
198
202
  version: '0.1.0',
199
203
  kind: 'business',
200
- contributes: {
204
+ declare: {
201
205
  providers: {
202
206
  slots: [{ service: 'parking.payment', provider: 'partial', implementation: 'partial-payment@1', credentialType: 'partial-c' }],
203
207
  },
@@ -211,10 +215,13 @@ function layerHarness(): {
211
215
  'packages/module-parking/tbox.module.json',
212
216
  JSON.stringify({
213
217
  schemaVersion: 1,
214
- name: 'module-parking',
218
+ id: 'module-parking',
215
219
  version: '0.1.0',
216
220
  kind: 'business',
217
- contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'parking.payment', optional: true }] },
221
+ declare: {
222
+ owns: [{ service: 'parking.query' }, { service: 'parking.payment' }],
223
+ consumes: [{ service: 'parking.query', optional: false }, { service: 'parking.payment', optional: true }],
224
+ },
218
225
  dependencies: { modules: [] },
219
226
  env: [],
220
227
  }),
@@ -226,13 +233,103 @@ function layerHarness(): {
226
233
  provider: 'wanda',
227
234
  instances: [{ id: 'mall-bj-01', name: '北京店' }],
228
235
  defaultInstanceId: 'mall-bj-01',
229
- domains: { parking: { provider: 'wanda' } },
236
+ modules: { 'module-parking': { provider: 'wanda' } },
230
237
  services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
231
238
  }),
232
239
  );
233
240
  return { app, tk: createAppToolkit(app.appDir), schemas, joycity };
234
241
  }
235
242
 
243
+ /** E 系列基座(v4.7 D38 引入;v5.1 D39 提升至模块层共用——原 D38 describe 内定义):wanda 双槽
244
+ * (layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json(O15 形态)+
245
+ * module-parking demand。schema 内容互异防断言串扰:
246
+ * wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
247
+ * 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。
248
+ * 供给矩阵:wanda 供给 {parking.query, parking.payment};joycity 仅供给 {parking.query}
249
+ * (provider-multi 双 impl 皆 query)——D39 用例 (厂商 × 服务) 选型须对照本矩阵。 */
250
+ function eHarness(): {
251
+ app: TempApp;
252
+ tk: AppToolkit;
253
+ schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
254
+ } {
255
+ const app = createTempApp('tbox-d38-');
256
+ invalidateContractsResolver();
257
+ const store = join(app.appDir, 'contracts');
258
+ writeFakeContractsPackage(store, { version: '0.9.0' });
259
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
260
+ const schemas = {
261
+ wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
262
+ wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
263
+ joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
264
+ joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
265
+ ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
266
+ };
267
+ writeAppFile(
268
+ app.appDir,
269
+ 'packages/provider-w/tbox.module.json',
270
+ JSON.stringify({
271
+ schemaVersion: 1,
272
+ id: 'provider-w',
273
+ version: '0.1.0',
274
+ kind: 'business',
275
+ declare: {
276
+ providers: {
277
+ slots: [
278
+ { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
279
+ { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
280
+ ],
281
+ },
282
+ },
283
+ dependencies: { modules: [] },
284
+ env: [],
285
+ }),
286
+ );
287
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
288
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
289
+ writeAppFile(
290
+ app.appDir,
291
+ 'packages/provider-multi/tbox.module.json',
292
+ JSON.stringify({
293
+ schemaVersion: 1,
294
+ id: 'provider-multi',
295
+ version: '0.1.0',
296
+ kind: 'business',
297
+ declare: {
298
+ providers: {
299
+ slots: [
300
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
301
+ { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
302
+ ],
303
+ },
304
+ },
305
+ dependencies: { modules: [] },
306
+ env: [],
307
+ }),
308
+ );
309
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
310
+ writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
311
+ writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
312
+ writeAppFile(
313
+ app.appDir,
314
+ 'packages/module-parking/tbox.module.json',
315
+ JSON.stringify({
316
+ schemaVersion: 1,
317
+ id: 'module-parking',
318
+ version: '0.1.0',
319
+ kind: 'business',
320
+ declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
321
+ dependencies: { modules: [] },
322
+ env: [],
323
+ }),
324
+ );
325
+ return { app, tk: createAppToolkit(app.appDir), schemas };
326
+ }
327
+
328
+ /** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
329
+ function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
330
+ writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
331
+ }
332
+
236
333
  describe('views/工厂(O 矩阵核心族)', () => {
237
334
  let h: Harness;
238
335
 
@@ -259,8 +356,9 @@ describe('views/工厂(O 矩阵核心族)', () => {
259
356
  expect(Object.keys(appView)).toEqual(['services', 'integration']);
260
357
  const mod = await h.tk.loadModule('module-parking');
261
358
  // v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
262
- // B1 additive:模块 meta title/description 前置(恒发——兜底 name/''
263
- expect(Object.keys(mod)).toEqual(['title', 'description', 'domainKeys', 'integration', 'providerBindings', 'configEditable', 'services']);
359
+ // v7 词汇统一:模块展示名 name 前置(恒发——兜底 id
360
+ // resources 只读镜像(N2 读半边——基座 integrations modules 节无 resources → null 仍恒发)
361
+ expect(Object.keys(mod)).toEqual(['name', 'description', 'ownedServices', 'integration', 'resources', 'binding', 'configEditable', 'services']);
264
362
  const svc = await h.tk.loadService('parking.query');
265
363
  expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
266
364
  // 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
@@ -513,8 +611,8 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
513
611
  'packages/provider-multi/tbox.module.json',
514
612
  JSON.stringify({
515
613
  schemaVersion: 1,
516
- name: 'provider-multi',
517
- contributes: {
614
+ id: 'provider-multi',
615
+ declare: {
518
616
  providers: {
519
617
  slots: [
520
618
  { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
@@ -585,8 +683,11 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
585
683
  // api.md §5 表
586
684
  'CREDENTIAL_FILE_MISSING', 'CREDENTIAL_TYPE_MISMATCH', 'IMPL_WITHOUT_PROVIDER', 'INSTANCE_SUPPLY_UNMATCHED',
587
685
  'SCHEMA_FILE_MISSING', 'DOMAIN_CONFIG_UNUSED', 'DOMAIN_CONFIG_INVALID', 'INSTANCE_KEY_NOT_REGISTERED',
588
- 'REALM_SWITCH_HINT', 'WILDCARD_ALIGNMENT', 'VENDOR_NOT_INSTALLED', 'BINDING_UNRESOLVED', 'DOMAIN_KEY_UNMATCHED',
686
+ 'REALM_SWITCH_HINT', 'WILDCARD_ALIGNMENT', 'VENDOR_NOT_INSTALLED', 'BINDING_UNRESOLVED',
589
687
  'RESOURCE_TYPE_UNKNOWN', 'RESOURCE_NOT_DECLARED',
688
+ // v6 additive(api.md §5 表已收口):MODULE_CONFIG_DEAD / #17 GLOBAL_FIELD_UNDECLARED;
689
+ // v6 退役:DOMAIN_KEY_UNMATCHED(#14——domains 键退场)
690
+ 'MODULE_CONFIG_DEAD', 'GLOBAL_FIELD_UNDECLARED',
590
691
  // 谓词头映射表 additive(检查族编号映射:#3/#4/#5 内联/#13/v4)
591
692
  'SLOT_NOT_DECLARED', 'SLOT_NOT_CONFIGURED', 'INLINE_PROVIDER_INVALID', 'MULTI_INSTANCE_NO_DEFAULT', 'INSTANCE_DISABLED',
592
693
  ]);
@@ -636,18 +737,18 @@ describe('A2 错误面(P-4 notes 优先 / P-5 畸形包 503)', () => {
636
737
  });
637
738
  });
638
739
 
639
- describe('D34 多域模块钉死(v4.4 F3)', () => {
640
- it('D1 单域回归:domainKeys/providerBindings/configEditable 装配 + integration 原值', async () => {
740
+ describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单数重写)', () => {
741
+ it('D1 单槽回归:ownedServices/binding/configEditable 装配 + integration 集成位原值', async () => {
641
742
  const h2 = harnessWithContracts();
642
743
  const mod = await h2.tk.loadModule('module-parking');
643
- expect(mod.domainKeys).toEqual(['parking']);
744
+ expect(mod.ownedServices).toEqual(['parking.query']);
644
745
  expect(mod.configEditable).toBe(true);
645
- expect(mod.providerBindings).toEqual([{ domain: 'parking', effective: 'wanda', layer: 'module' }]);
746
+ expect(mod.binding).toEqual({ provider: 'wanda', layer: 'module' });
646
747
  expect(mod.integration).toEqual({ provider: 'wanda' });
647
748
  h2.app.dispose();
648
749
  });
649
750
 
650
- it('D2 多域模块:GET integration null + providerBindings 双条 + PUT 400 分支①(含 {} 清域)', async () => {
751
+ it('D2 多域归属合法化(v6 正例化——跨域 owns 单键落位):binding 单值作用全 owned + PUT 全量替换', async () => {
651
752
  const app = createTempApp('tbox-d2-');
652
753
  invalidateContractsResolver();
653
754
  const store = joinStore(app.appDir, 'contracts');
@@ -658,33 +759,30 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
658
759
  'packages/module-multi/tbox.module.json',
659
760
  JSON.stringify({
660
761
  schemaVersion: 1,
661
- name: 'module-multi',
762
+ id: 'module-multi',
662
763
  version: '0.1.0',
663
764
  kind: 'business',
664
- contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
765
+ declare: { owns: [{ service: 'parking.query' }, { service: 'member.account' }], consumes: [{ service: 'parking.query', optional: false }] },
665
766
  dependencies: { modules: [] },
666
767
  env: [],
667
768
  }),
668
769
  );
669
770
  const tk = createAppToolkit(app.appDir);
670
771
  const mod = await tk.loadModule('module-multi');
671
- expect(mod.domainKeys).toEqual(['member', 'parking']);
772
+ expect(mod.ownedServices).toEqual(['member.account', 'parking.query']);
672
773
  expect(mod.integration).toBeNull();
673
- expect(mod.configEditable).toBe(false);
674
- expect(mod.providerBindings).toEqual([
675
- { domain: 'member', effective: null, layer: null },
676
- { domain: 'parking', effective: null, layer: null },
677
- ]);
678
- await expect(tk.writeModuleIntegration('module-multi', { provider: 'wanda' })).rejects.toMatchObject({
679
- code: 'VALIDATION_FAILED',
680
- httpStatus: 400,
681
- message: expect.stringContaining('跨 2 个域(member、parking)'),
682
- });
683
- await expect(tk.writeModuleIntegration('module-multi', {})).rejects.toMatchObject({ code: 'VALIDATION_FAILED' });
774
+ expect(mod.configEditable).toBe(true);
775
+ expect(mod.binding).toEqual({ provider: null, layer: null });
776
+ // v6 写通道:多域归属合法——模块级 provider 单值落 modules[module-multi]
777
+ const r = await tk.writeModuleIntegration('module-multi', { provider: 'wanda' });
778
+ expect(r.written).toBe(true);
779
+ const mod2 = await tk.loadModule('module-multi');
780
+ expect(mod2.integration).toEqual({ provider: 'wanda' });
781
+ expect(mod2.binding).toEqual({ provider: 'wanda', layer: 'module' });
684
782
  app.dispose();
685
783
  });
686
784
 
687
- it('D3 零域模块:domainKeys [] + PUT 400 分支②', async () => {
785
+ it('D3 零归属模块:ownedServices [] + configEditable false + PUT 400(zero-ownership)', async () => {
688
786
  const app = createTempApp('tbox-d3-');
689
787
  invalidateContractsResolver();
690
788
  const store = joinStore(app.appDir, 'contracts');
@@ -695,63 +793,95 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
695
793
  'packages/module-zero/tbox.module.json',
696
794
  JSON.stringify({
697
795
  schemaVersion: 1,
698
- name: 'module-zero',
796
+ id: 'module-zero',
699
797
  version: '0.1.0',
700
798
  kind: 'business',
701
- contributes: {},
799
+ declare: {},
702
800
  dependencies: { modules: [] },
703
801
  env: [],
704
802
  }),
705
803
  );
706
804
  const tk = createAppToolkit(app.appDir);
707
805
  const mod = await tk.loadModule('module-zero');
708
- expect(mod.domainKeys).toEqual([]);
709
- expect(mod.providerBindings).toEqual([]);
806
+ expect(mod.ownedServices).toEqual([]);
807
+ expect(mod.binding).toEqual({ provider: null, layer: null });
710
808
  expect(mod.integration).toBeNull();
711
809
  await expect(tk.writeModuleIntegration('module-zero', { provider: 'x' })).rejects.toMatchObject({
712
810
  code: 'VALIDATION_FAILED',
713
811
  httpStatus: 400,
714
- message: expect.stringContaining('无服务需求声明'),
812
+ message: expect.stringContaining('未声明任何 declare.owns'),
715
813
  });
716
814
  app.dispose();
717
815
  });
718
816
 
719
- it('D4 域键共享(P13):双方 configEditable false + PUT 400 分支③(他方同域异服务——并集计数)', async () => {
817
+ it('D4 v6 互斥归属:双模块 owns 同服务 OWNERSHIP_CONFLICT(先声明胜出)+ 双方可写(归属冲突≠写禁)', async () => {
720
818
  const app = createTempApp('tbox-d4-');
721
819
  invalidateContractsResolver();
722
820
  const store = joinStore(app.appDir, 'contracts');
723
821
  writeFakeContractsPackage(store, { version: '0.9.0' });
724
822
  linkContracts(app.appDir, store, { subdir: 'apps/server' });
725
- const manifest = (name: string, service: string): string =>
823
+ const manifest = (id: string, service: string): string =>
726
824
  JSON.stringify({
727
825
  schemaVersion: 1,
728
- name,
826
+ id,
729
827
  version: '0.1.0',
730
828
  kind: 'business',
731
- contributes: { services: [{ service, optional: false }] },
829
+ declare: { owns: [{ service }], consumes: [{ service, optional: false }] },
732
830
  dependencies: { modules: [] },
733
831
  env: [],
734
832
  });
735
833
  writeAppFile(app.appDir, 'packages/module-a/tbox.module.json', manifest('module-a', 'parking.alpha'));
736
- writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b', 'parking.beta'));
834
+ writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b', 'parking.alpha'));
737
835
  const tk = createAppToolkit(app.appDir);
836
+ const walk = tk.walkManifests();
837
+ // 先声明胜出(packages/ 目录字母序:module-a 先)
838
+ expect(walk.ownership['parking.alpha']).toBe('module-a');
839
+ // N4 执法分层:冲突 = declarationIssues error(doctor 面向第三方组合态执法;写通道不拒——归属冲突非写禁)
840
+ const conflict = walk.declarationIssues.find((i) => i.code === 'OWNERSHIP_CONFLICT');
841
+ expect(conflict).toBeDefined();
842
+ expect(conflict!.severity).toBe('error');
843
+ expect(conflict!.message).toContain('module-b');
844
+ // 双方 configEditable 均 true(owns>0 唯一判据——v6 无 shared-key 写禁)
738
845
  const a = await tk.loadModule('module-a');
739
846
  const b = await tk.loadModule('module-b');
740
- expect(a.configEditable).toBe(false);
741
- expect(b.configEditable).toBe(false);
742
- await expect(tk.writeModuleIntegration('module-a', { provider: 'x' })).rejects.toMatchObject({
743
- code: 'VALIDATION_FAILED',
744
- httpStatus: 400,
745
- message: expect.stringContaining(' module-a、module-b 共用'),
847
+ expect(a.configEditable).toBe(true);
848
+ expect(b.configEditable).toBe(true);
849
+ app.dispose();
850
+ });
851
+
852
+ it('D6 偏斜态 503 第四分支(P1 标记门):v5 contracts 假件(无 OWNERSHIP_BINDING_V6)+ v6 toolkit → 503', async () => {
853
+ const app = createTempApp('tbox-d6-');
854
+ invalidateContractsResolver();
855
+ const store = joinStore(app.appDir, 'contracts');
856
+ writeFakeContractsPackage(store, { version: '0.9.0', withMarker: false });
857
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
858
+ writeAppFile(app.appDir, 'packages/module-parking/tbox.module.json', JSON.stringify({
859
+ schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
860
+ declare: { owns: [{ service: 'parking.query' }] },
861
+ }));
862
+ const tk = createAppToolkit(app.appDir);
863
+ // 写路径门控(求值轴同此门——assertContractsUsable 单点)
864
+ await expect(tk.writeModuleIntegration('module-parking', { provider: 'wanda' })).rejects.toMatchObject({
865
+ code: 'CONTRACTS_NOT_RESOLVED',
866
+ httpStatus: 503,
867
+ message: expect.stringContaining('OWNERSHIP_BINDING_V6'),
746
868
  });
869
+ await expect(tk.loadServiceResolutions()).rejects.toMatchObject({ code: 'CONTRACTS_NOT_RESOLVED', httpStatus: 503 });
870
+ // 静态端点照常(永不 503——门控 policy at edge 只拦求值/写/migrate)
871
+ const mod = await tk.loadModule('module-parking');
872
+ expect(mod.ownedServices).toEqual(['parking.query']);
747
873
  app.dispose();
748
874
  });
749
875
 
750
- it('D5 死键扫尾:moduleDomainKey/moduleDomainKeyOf 全链删除(源文件内容断言)', () => {
876
+ it('D5 死键扫尾:providerBindings/moduleDomainWritable/domain-binding 全链删除(源文件内容断言)', () => {
751
877
  const read = (rel: string): string => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8');
752
- expect(read('../src/views/modules.ts')).not.toContain('moduleDomainKeyOf');
753
- expect(read('../src/integrations/write.ts')).not.toContain('moduleDomainKey');
754
- expect(read('../src/index.ts')).not.toContain('moduleDomainKeyOf');
878
+ const exists = (rel: string): boolean => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8').length >= 0;
879
+ void exists;
880
+ expect(read('../src/views/modules.ts')).not.toContain('providerBindings:');
881
+ expect(read('../src/integrations/write.ts')).not.toContain('moduleDomain');
882
+ expect(read('../src/index.ts')).not.toContain('domain-binding');
883
+ // v6 新面在场(正向锚)
884
+ expect(read('../src/integrations/module-binding.ts')).toContain('moduleOwnershipWritable');
755
885
  });
756
886
  });
757
887
 
@@ -845,7 +975,7 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
845
975
  const h2 = layerHarness();
846
976
  const mod = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas'] });
847
977
  expect(mod.integrationSchemas).toEqual({
848
- provider: 'wanda', // resolveDomainBinding 单源(domains.parking)
978
+ provider: 'wanda', // resolveModuleBinding 单源(modules['module-parking']
849
979
  services: {
850
980
  'parking.query': { configSchema: h2.schemas.query },
851
981
  'parking.payment': { configSchema: h2.schemas.parking },
@@ -874,7 +1004,7 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
874
1004
  'config/integrations.json',
875
1005
  JSON.stringify({
876
1006
  instances: [{ id: 'mall-bj-01', name: '北京店' }],
877
- domains: { parking: { provider: 'wanda' } },
1007
+ modules: { 'module-parking': { provider: 'wanda' } },
878
1008
  services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
879
1009
  }),
880
1010
  );
@@ -903,10 +1033,10 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
903
1033
  'packages/module-multi/tbox.module.json',
904
1034
  JSON.stringify({
905
1035
  schemaVersion: 1,
906
- name: 'module-multi',
1036
+ id: 'module-multi',
907
1037
  version: '0.1.0',
908
1038
  kind: 'business',
909
- contributes: { services: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
1039
+ declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
910
1040
  dependencies: { modules: [] },
911
1041
  env: [],
912
1042
  }),
@@ -930,10 +1060,10 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
930
1060
  'packages/provider-custom/tbox.module.json',
931
1061
  JSON.stringify({
932
1062
  schemaVersion: 1,
933
- name: 'provider-custom',
1063
+ id: 'provider-custom',
934
1064
  version: '0.1.0',
935
1065
  kind: 'business',
936
- contributes: { providers: { slots: [] }, services: [] },
1066
+ declare: { providers: { slots: [] }, consumes: [], owns: [] },
937
1067
  dependencies: { modules: [] },
938
1068
  env: [],
939
1069
  }),
@@ -951,8 +1081,8 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
951
1081
  join('node_modules', '@tbox.cn', 'app-provider-mock', 'tbox.module.json'),
952
1082
  JSON.stringify({
953
1083
  schemaVersion: 1,
954
- name: 'provider-mock',
955
- contributes: {
1084
+ id: 'provider-mock',
1085
+ declare: {
956
1086
  providers: { slots: [{ service: 'parking.query', provider: 'mock', implementation: 'mock-parking@1', credentialType: '-' }] },
957
1087
  },
958
1088
  }),
@@ -970,11 +1100,11 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
970
1100
  expect(walk.modules.map((m) => m.id).sort()).toEqual(['module-parking', 'provider-custom', 'vendor-mock']);
971
1101
  // 详情 200 保持(装包仍合法——零域空态;写通道零域由 D34 守卫拦)
972
1102
  const custom = await h2.tk.loadModule('provider-custom');
973
- expect(custom.domainKeys).toEqual([]);
1103
+ expect(custom.ownedServices).toEqual([]);
974
1104
  expect(custom.configEditable).toBe(false);
975
1105
  expect(custom.services).toEqual([]);
976
1106
  const mock = await h2.tk.loadModule('vendor-mock');
977
- expect(mock.domainKeys).toEqual([]);
1107
+ expect(mock.ownedServices).toEqual([]);
978
1108
  // 供给轴不受影响:provider-mock 槽位照常进 catalog(清单排除不伤聚合)
979
1109
  const providers = await h2.tk.loadProviders();
980
1110
  expect(providers.providers.map((p) => p.provider)).toContain('mock');
@@ -983,93 +1113,6 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
983
1113
  });
984
1114
 
985
1115
  describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
986
- /** E 系列基座:wanda 双槽(layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json
987
- * (O15 形态)+ module-parking demand。schema 内容互异防断言串扰:
988
- * wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
989
- * 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。 */
990
- function eHarness(): {
991
- app: TempApp;
992
- tk: AppToolkit;
993
- schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
994
- } {
995
- const app = createTempApp('tbox-d38-');
996
- invalidateContractsResolver();
997
- const store = join(app.appDir, 'contracts');
998
- writeFakeContractsPackage(store, { version: '0.9.0' });
999
- linkContracts(app.appDir, store, { subdir: 'apps/server' });
1000
- const schemas = {
1001
- wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
1002
- wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
1003
- joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
1004
- joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
1005
- ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
1006
- };
1007
- writeAppFile(
1008
- app.appDir,
1009
- 'packages/provider-w/tbox.module.json',
1010
- JSON.stringify({
1011
- schemaVersion: 1,
1012
- name: 'provider-w',
1013
- version: '0.1.0',
1014
- kind: 'business',
1015
- contributes: {
1016
- providers: {
1017
- slots: [
1018
- { service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
1019
- { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
1020
- ],
1021
- },
1022
- },
1023
- dependencies: { modules: [] },
1024
- env: [],
1025
- }),
1026
- );
1027
- writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
1028
- writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
1029
- writeAppFile(
1030
- app.appDir,
1031
- 'packages/provider-multi/tbox.module.json',
1032
- JSON.stringify({
1033
- schemaVersion: 1,
1034
- name: 'provider-multi',
1035
- version: '0.1.0',
1036
- kind: 'business',
1037
- contributes: {
1038
- providers: {
1039
- slots: [
1040
- { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
1041
- { service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
1042
- ],
1043
- },
1044
- },
1045
- dependencies: { modules: [] },
1046
- env: [],
1047
- }),
1048
- );
1049
- writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
1050
- writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
1051
- writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
1052
- writeAppFile(
1053
- app.appDir,
1054
- 'packages/module-parking/tbox.module.json',
1055
- JSON.stringify({
1056
- schemaVersion: 1,
1057
- name: 'module-parking',
1058
- version: '0.1.0',
1059
- kind: 'business',
1060
- contributes: { services: [{ service: 'parking.query', optional: false }] },
1061
- dependencies: { modules: [] },
1062
- env: [],
1063
- }),
1064
- );
1065
- return { app, tk: createAppToolkit(app.appDir), schemas };
1066
- }
1067
-
1068
- /** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
1069
- function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
1070
- writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
1071
- }
1072
-
1073
1116
  it('E1 显式 string + catalog schema(分支②)', async () => {
1074
1117
  const h = eHarness();
1075
1118
  writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
@@ -1139,12 +1182,12 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
1139
1182
 
1140
1183
  it('E6 域级联(分支①)+ ghost 域厂商诚实态', async () => {
1141
1184
  const h = eHarness();
1142
- writeIntegrations(h.app.appDir, { domains: { parking: { provider: 'wanda' } }, services: { 'parking.query': {} } });
1185
+ writeIntegrations(h.app.appDir, { modules: { 'module-parking': { provider: 'wanda' } }, services: { 'parking.query': {} } });
1143
1186
  const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1144
1187
  expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
1145
1188
  // ghost 域厂商(catalog 外)→ configSchema null(诊断归谓词 #11——富化非门控)
1146
1189
  const h2 = eHarness();
1147
- writeIntegrations(h2.app.appDir, { domains: { parking: { provider: 'ghost-vendor' } }, services: { 'parking.query': {} } });
1190
+ writeIntegrations(h2.app.appDir, { modules: { 'module-parking': { provider: 'ghost-vendor' } }, services: { 'parking.query': {} } });
1148
1191
  const ghost = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1149
1192
  expect(ghost.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
1150
1193
  h.app.dispose();
@@ -1216,10 +1259,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
1216
1259
  });
1217
1260
 
1218
1261
  it('E14 malformed → 整字段 null(不落域级联——分支回归锚)', async () => {
1219
- // ① 坏对象(缺 credentialType)——配 domains 佐证未走域级联
1262
+ // ① 坏对象(缺 credentialType)——配 modules 佐证未走模块级联
1220
1263
  const h = eHarness();
1221
1264
  writeIntegrations(h.app.appDir, {
1222
- domains: { parking: { provider: 'wanda' } },
1265
+ modules: { 'module-parking': { provider: 'wanda' } },
1223
1266
  services: { 'parking.query': { provider: { provider: 'my-api' } } },
1224
1267
  });
1225
1268
  const bad = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
@@ -1227,7 +1270,7 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
1227
1270
  // ② provider: null(谓词 #5 同判 INLINE_PROVIDER_INVALID——坏形态非缺席)
1228
1271
  const h2 = eHarness();
1229
1272
  writeIntegrations(h2.app.appDir, {
1230
- domains: { parking: { provider: 'wanda' } },
1273
+ modules: { 'module-parking': { provider: 'wanda' } },
1231
1274
  services: { 'parking.query': { provider: null } },
1232
1275
  });
1233
1276
  const nul = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
@@ -1239,7 +1282,7 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
1239
1282
  it('E15 provider 空串 → 走域级联(非 null)', async () => {
1240
1283
  const h = eHarness();
1241
1284
  writeIntegrations(h.app.appDir, {
1242
- domains: { parking: { provider: 'wanda' } },
1285
+ modules: { 'module-parking': { provider: 'wanda' } },
1243
1286
  services: { 'parking.query': { provider: '' } },
1244
1287
  });
1245
1288
  const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
@@ -1248,6 +1291,170 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
1248
1291
  });
1249
1292
  });
1250
1293
 
1294
+ /**
1295
+ * v5.1 D39 测试族(服务轴向假想绑定——?serviceProvider= 表单双投影重定向)。
1296
+ * 供给矩阵(eHarness):wanda 供给 {parking.query, parking.payment};joycity 仅供给
1297
+ * {parking.query}(provider-multi 双 impl 皆 query)——用例 (厂商 × 服务) 选型须对照本矩阵:
1298
+ * catalog-miss 形态(D39-7)与豁免链正例(D39-6)不得混淆供给边界。每用例独立 eHarness
1299
+ * (零 file-cache stat 指纹串扰);D39-8/D39-9 用 layerHarness(x-tbox-global 标记 fixture)。
1300
+ */
1301
+ describe('v5.1 D39(服务轴向假想绑定 serviceProvider)', () => {
1302
+ it('D39-1 假想 happy + I1:双投影切 joycity 且 provider 一致 + 单键边界正例', async () => {
1303
+ const h = eHarness();
1304
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1305
+ const both = ['integrationSchemas', 'integrationFieldRegistry'];
1306
+ const v = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
1307
+ // I1:双投影同源同口径(fields = joycity schema 字段——catalog 复合键声明序首个 = @1 的 joycityUrl)
1308
+ expect(v.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
1309
+ expect(v.integrationFieldRegistry?.provider).toBe('joycity');
1310
+ expect(v.integrationFieldRegistry!.fields.map((f) => f.key)).toEqual(['joycityUrl']);
1311
+ // 单键边界正例:仅 integrationSchemas + 轴参 → 200 假想计算;registry 未请求 = 缺席(S7-1 尾部先例)
1312
+ const only = await h.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
1313
+ expect(only.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
1314
+ expect('integrationFieldRegistry' in only).toBe(false);
1315
+ h.app.dispose();
1316
+ });
1317
+
1318
+ it('D39-2 I2 锁:integration/suppliedBy/defaults 恒实际态', async () => {
1319
+ const h = eHarness();
1320
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda', config: { baseUrl: 'https://w.example' } } } });
1321
+ const base = await h.tk.loadService('parking.query');
1322
+ const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'joycity' });
1323
+ expect(v.integration).toEqual(base.integration); // 文件逐字原值不被重定向(预览不产生「已保存」误导)
1324
+ expect(v.suppliedBy).toEqual(base.suppliedBy); // catalog 静态切片恒实际
1325
+ expect(v.defaults).toEqual(base.defaults); // manifest 预填恒实际
1326
+ expect(v.integrationSchemas?.provider).toBe('joycity'); // 对照:表单层已重定向
1327
+ h.app.dispose();
1328
+ });
1329
+
1330
+ it('D39-3 I3 锁 + 错轴静默忽略:空串 ≡ 缺席 ≡ 实际;appProvider/moduleProvider 被无视', async () => {
1331
+ const h = eHarness();
1332
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1333
+ const empty = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: '' });
1334
+ const absent = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'] });
1335
+ expect(empty.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
1336
+ // 错轴参数(变量绕过多余属性检查——运行时按名读取,appProvider/moduleProvider 被无视;S7-4 先例镜像)
1337
+ const opts = { include: ['integrationFieldRegistry'], serviceProvider: '', appProvider: 'joycity', moduleProvider: 'joycity' };
1338
+ const wrongAxis = await h.tk.loadService('parking.query', opts);
1339
+ expect(wrongAxis.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
1340
+ h.app.dispose();
1341
+ });
1342
+
1343
+ it('D39-4 轴参严检矩阵:400 先于 404(镜像 S7-1 形态)', async () => {
1344
+ const h = eHarness();
1345
+ await expect(h.tk.loadService('parking.query', { serviceProvider: 'x' })).rejects.toMatchObject({
1346
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
1347
+ });
1348
+ await expect(h.tk.loadService('parking.query', { include: [], serviceProvider: 'x' })).rejects.toMatchObject({
1349
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
1350
+ });
1351
+ // 先于 404:ghost 服务 + 无表单键轴参 → 400(非 SERVICE_NOT_FOUND);表单键在场 → 放行至 404
1352
+ await expect(h.tk.loadService('ghost', { serviceProvider: 'x' })).rejects.toMatchObject({
1353
+ code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
1354
+ });
1355
+ await expect(h.tk.loadService('ghost', { include: ['integrationSchemas'], serviceProvider: 'x' })).rejects.toMatchObject({
1356
+ code: 'SERVICE_NOT_FOUND', httpStatus: 404,
1357
+ });
1358
+ h.app.dispose();
1359
+ });
1360
+
1361
+ it('D39-5 同构承诺锁:假想 ≡ PUT 改写 provider 后实读(双投影;S6 写通道先例)', async () => {
1362
+ const h = eHarness();
1363
+ const both = ['integrationSchemas', 'integrationFieldRegistry'];
1364
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1365
+ const hyp = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
1366
+ // 状态演进全经 toolkit 写通道——写后本实例缓存即刻失效,免外部写缓存关注
1367
+ const cur = await h.tk.loadService('parking.query');
1368
+ const w = await h.tk.writeServiceIntegration('parking.query', { ...cur.integration!, provider: 'joycity' });
1369
+ expect(w.written).toBe(true);
1370
+ const act = await h.tk.loadService('parking.query', { include: both });
1371
+ expect(act.integrationSchemas).toEqual(hyp.integrationSchemas);
1372
+ expect(act.integrationFieldRegistry).toEqual(hyp.integrationFieldRegistry);
1373
+ h.app.dispose();
1374
+ });
1375
+
1376
+ it('D39-6 假想豁免链三分支:malformed 不污染 / 内联不读取无 H9 / 未绑定仍非 null', async () => {
1377
+ // ① malformed 节点(E14 形态)+ 假想 → 非 null(短路在节点读取之前——坏形态只影响实际态投影)
1378
+ const h = eHarness();
1379
+ writeIntegrations(h.app.appDir, {
1380
+ modules: { 'module-parking': { provider: 'wanda' } },
1381
+ services: { 'parking.query': { provider: { provider: 'my-api' } } },
1382
+ });
1383
+ const bad = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
1384
+ expect(bad.integrationFieldRegistry).not.toBeNull();
1385
+ expect(bad.integrationFieldRegistry!.provider).toBe('joycity');
1386
+ // 实际态对照:同坏节点无轴参 → 整字段 null(E14 双态互证——预览与坏文件解耦)
1387
+ const badActual = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
1388
+ expect(badActual.integrationSchemas).toBeNull();
1389
+ h.app.dispose();
1390
+ // ② 内联 ref 逃逸节点(E4 形态)+ 假想 → 无 H9 400(短路不读节点内联 ref——换厂商即离开内联)
1391
+ const h2 = eHarness();
1392
+ writeIntegrations(h2.app.appDir, {
1393
+ services: { 'parking.query': { provider: { provider: 'my-api', credentialType: '-', configSchema: 'config/schemas-evil/x.json' } } },
1394
+ });
1395
+ const inline = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
1396
+ expect(inline.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h2.schemas.joycity1 });
1397
+ h2.app.dispose();
1398
+ // ③ 全链未绑定(E8 形态)+ 假想 → 仍非 null(服务轴恒可写——无 module 轴零归属豁免形态);
1399
+ // wanda 供给 payment(供给矩阵)→ mallId 字段在场
1400
+ const h3 = eHarness();
1401
+ writeIntegrations(h3.app.appDir, { services: { 'parking.payment': {} } });
1402
+ const unbound = await h3.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
1403
+ // fields[].schema = 字段子 schema(非整文件 schema——G 批锁 :1777 同型)
1404
+ expect(unbound.integrationFieldRegistry).toEqual({
1405
+ provider: 'wanda', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true,
1406
+ fields: [{ key: 'mallId', schema: { type: 'string' }, sources: [{ service: 'parking.payment', required: false }], global: false }],
1407
+ });
1408
+ h3.app.dispose();
1409
+ });
1410
+
1411
+ it('D39-7 catalog miss:假想未知厂商 → 非 null 空集(schemaless=[service])', async () => {
1412
+ const h = eHarness();
1413
+ writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
1414
+ const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'ghost-vendor' });
1415
+ expect(v.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
1416
+ expect(v.integrationFieldRegistry).toEqual({
1417
+ provider: 'ghost-vendor', schemaless: ['parking.query'], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
1418
+ });
1419
+ h.app.dispose();
1420
+ });
1421
+
1422
+ it('D39-8 global 旗标假想透传:标记位如实(layerHarness——true 正例 + 未标记 false 不过滤)', async () => {
1423
+ const h = layerHarness();
1424
+ // joycity 供给 parking.query → 假想后 fields = joycity query schema 字段(fixture 全标记 → global:true)
1425
+ const joy = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
1426
+ expect(joy.integrationFieldRegistry!.provider).toBe('joycity');
1427
+ expect(joy.integrationFieldRegistry!.fields.length).toBeGreaterThan(0);
1428
+ expect(joy.integrationFieldRegistry!.fields.every((f) => f.global)).toBe(true);
1429
+ // wanda 供给 parking.payment → mallId global:true / parkLotId global:false 在场(服务轴不过滤——G 批语义假想不变);
1430
+ // payment 先配置入词汇(P9:契约包 ∪ 已配置——layerHarness 初始 integrations.json 未配置 payment;
1431
+ // 覆盖写后 payment 节点 {} 实际态级联双缺 → null,假想 wanda 短路仍非 null——豁免链③同型,S7-7 外部写先例)
1432
+ writeAppFile(h.app.appDir, 'config/integrations.json', JSON.stringify({ services: { 'parking.payment': {} } }));
1433
+ const wan = await h.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
1434
+ expect(wan.integrationFieldRegistry!.fields.map((f) => f.key).sort()).toEqual(['mallId', 'parkLotId']);
1435
+ expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'mallId')!.global).toBe(true);
1436
+ expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'parkLotId')!.global).toBe(false);
1437
+ h.app.dispose();
1438
+ });
1439
+
1440
+ it('D39-9 单源锁:loadServiceFieldRegistry(snap,s,X) ≡ buildIntegrationFieldRegistry over 链函数', async () => {
1441
+ const h = layerHarness();
1442
+ const { loadViewSnapshotSync } = await import('../src/views/context.js');
1443
+ const snap = loadViewSnapshotSync({ appDir: h.app.appDir }, await h.tk.resolveContracts());
1444
+ const X = 'joycity';
1445
+ const reg = loadServiceFieldRegistry(snap, 'parking.query', X);
1446
+ const schemas = loadServiceIntegrationSchemas(snap, 'parking.query', X);
1447
+ expect(schemas).not.toBeNull();
1448
+ // 结构性断言(依赖契约锁——registry 旁路重取 schema 破 I1 即刻红;S8 同族意图)
1449
+ const manual = buildIntegrationFieldRegistry({
1450
+ provider: schemas!.provider,
1451
+ services: { 'parking.query': { configSchema: schemas!.configSchema } },
1452
+ });
1453
+ expect(reg).toEqual(manual);
1454
+ h.app.dispose();
1455
+ });
1456
+ });
1457
+
1251
1458
  /**
1252
1459
  * v4.6 D36/D37 测试族(S5 接续 S1-S4):S5a 纯代数(直调合成 layer 字面量,零 fs)/
1253
1460
  * S5b 接线序 + 等价锁 / S6 同构(假想 ≡ 写后实读)/ S7 边界(400·null 两态·I2·空集)/
@@ -1264,15 +1471,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1264
1471
  unsupplied: [],
1265
1472
  additionalPropertiesAllowed: false,
1266
1473
  fields: [
1267
- { key: 'baseUrl', schema: { type: 'string', format: 'uri' }, sources: [src('parking.query', true), src('parking.payment', false)] },
1268
- { key: 'lotId', schema: null, sources: [src('parking.query', true), src('parking.payment', false)] },
1269
- { key: 'env', schema: { type: 'string', enum: ['dev', 'staging', 'prod'] }, sources: [src('parking.query', false), src('parking.payment', false)] },
1270
- { key: 'envMixed', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
1271
- { key: 'meta', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
1272
- { key: 'options', schema: { type: 'object', properties: { x: { type: 'string' } } }, sources: [src('parking.query', false), src('parking.payment', true)] },
1273
- { key: 'untyped', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
1274
- { key: 'timeout', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
1275
- { key: 'apiKey', schema: { type: 'string' }, sources: [src('parking.payment', true)] },
1474
+ { key: 'baseUrl', schema: { type: 'string', format: 'uri', 'x-tbox-global': true }, global: true, sources: [src('parking.query', true), src('parking.payment', false)] },
1475
+ { key: 'lotId', schema: null, global: true, sources: [src('parking.query', true), src('parking.payment', false)] },
1476
+ { key: 'env', schema: { type: 'string', enum: ['dev', 'staging', 'prod'], 'x-tbox-global': true }, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
1477
+ { key: 'envMixed', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
1478
+ { key: 'meta', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
1479
+ { key: 'options', schema: { type: 'object', properties: { x: { type: 'string', 'x-tbox-global': true } }, 'x-tbox-global': true }, global: true, sources: [src('parking.query', false), src('parking.payment', true)] },
1480
+ { key: 'untyped', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
1481
+ { key: 'timeout', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
1482
+ { key: 'apiKey', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', true)] },
1276
1483
  ],
1277
1484
  };
1278
1485
 
@@ -1283,15 +1490,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1283
1490
  unsupplied: [],
1284
1491
  additionalPropertiesAllowed: false,
1285
1492
  fields: [
1286
- { key: 'apiKey', schema: { type: 'string' }, sources: [src('parking.payment', true)] },
1287
- { key: 'baseUrl', schema: { type: 'string' }, sources: [src('parking.payment', false), src('parking.query', true)] },
1288
- { key: 'lotId', schema: null, sources: [src('parking.payment', false), src('parking.query', true)] },
1289
- { key: 'env', schema: { type: 'string', enum: ['prod', 'dev', 'staging'] }, sources: [src('parking.payment', false), src('parking.query', false)] },
1290
- { key: 'envMixed', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
1291
- { key: 'meta', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
1292
- { key: 'options', schema: { type: 'object', properties: { x: { type: 'string' } } }, sources: [src('parking.payment', true), src('parking.query', false)] },
1293
- { key: 'untyped', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
1294
- { key: 'timeout', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
1493
+ { key: 'apiKey', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', true)] },
1494
+ { key: 'baseUrl', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', false), src('parking.query', true)] },
1495
+ { key: 'lotId', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', true)] },
1496
+ { key: 'env', schema: { type: 'string', enum: ['prod', 'dev', 'staging'], 'x-tbox-global': true }, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
1497
+ { key: 'envMixed', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
1498
+ { key: 'meta', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
1499
+ { key: 'options', schema: { type: 'object', properties: { x: { type: 'string', 'x-tbox-global': true } }, 'x-tbox-global': true }, global: true, sources: [src('parking.payment', true), src('parking.query', false)] },
1500
+ { key: 'untyped', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
1501
+ { key: 'timeout', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
1295
1502
  ],
1296
1503
  };
1297
1504
 
@@ -1301,15 +1508,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1301
1508
  /** E4:module 轴 moduleProvider=ghost-x(未知厂商——非 null 空集 + 全 demand 回填,demand 序) */
1302
1509
  const E4 = { provider: 'ghost-x', schemaless: [], unsupplied: ['parking.query', 'parking.payment'], additionalPropertiesAllowed: true, fields: [] };
1303
1510
 
1304
- /** E5:实际态(无轴参,wanda;scope = [query, payment] */
1511
+ /** E5:实际态(无轴参,wanda;scope = [query, payment])——parkLotId 未标记 global 被表单收窄(G 批负例) */
1305
1512
  const E5 = {
1306
1513
  provider: 'wanda',
1307
1514
  schemaless: [],
1308
1515
  unsupplied: [],
1309
1516
  additionalPropertiesAllowed: true,
1310
1517
  fields: [
1311
- { key: 'baseUrl', schema: { type: 'string', format: 'uri' }, sources: [src('parking.query', true)] },
1312
- { key: 'mallId', schema: { type: 'string' }, sources: [src('parking.payment', false)] },
1518
+ { key: 'baseUrl', schema: { type: 'string', format: 'uri', 'x-tbox-global': true }, global: true, sources: [src('parking.query', true)] },
1519
+ { key: 'mallId', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', false)] },
1313
1520
  ],
1314
1521
  };
1315
1522
 
@@ -1391,7 +1598,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1391
1598
 
1392
1599
  it('S5a-8/9 单源直通 + required 聚合(some;conflict 照常聚合)', () => {
1393
1600
  const single = reg({ 'a.one': { configSchema: { type: 'object', properties: { only: { type: 'string', enum: ['z'] } } } } });
1394
- expect(single.fields[0]).toEqual({ key: 'only', schema: { type: 'string', enum: ['z'] }, sources: [src('a.one', false)] });
1601
+ expect(single.fields[0]).toEqual({ key: 'only', schema: { type: 'string', enum: ['z'] }, global: false, sources: [src('a.one', false)] });
1395
1602
  const some = reg({
1396
1603
  'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string' } }, required: ['k'] } },
1397
1604
  'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
@@ -1447,7 +1654,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1447
1654
  const appInc = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1448
1655
  expect(appInc.integrationFieldRegistry).toEqual(E2);
1449
1656
  expect(appInc.integrationFieldRegistry?.fields[0].key).toBe('apiKey'); // 足迹字母序(vs E1 首键 baseUrl)
1450
- expect(appInc.integrationFieldRegistry?.fields[1].schema).toEqual({ type: 'string' }); // 首源漂移(payment 先)
1657
+ expect(appInc.integrationFieldRegistry?.fields[1].schema).toEqual({ type: 'string', 'x-tbox-global': true }); // 首源漂移(payment 先)
1451
1658
  const partial = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: 'partial' });
1452
1659
  expect(partial.integrationFieldRegistry).toEqual(E3);
1453
1660
  const actual = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'] });
@@ -1485,7 +1692,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1485
1692
  const actJoy = await h2.tk.loadModule('module-parking', { include: both });
1486
1693
  expect(actJoy.integrationFieldRegistry).toEqual(hypJoy.integrationFieldRegistry);
1487
1694
  expect(actJoy.integrationSchemas).toEqual(hypJoy.integrationSchemas);
1488
- // ② 跟随应用级(root.provider=wanda)≡ 清域(N1 删 domains.parking → root 兜底)后实读——非平凡:清域前实际态 = E1
1695
+ // ② 跟随应用级(root.provider=wanda)≡ 清模块位(N1 删 modules['module-parking'] → root 兜底)后实读——非平凡:清位前实际态 = E1
1489
1696
  const hypFollow = await h2.tk.loadModule('module-parking', { include: both, moduleProvider: 'wanda' });
1490
1697
  expect(hypFollow.integrationFieldRegistry).toEqual(E5);
1491
1698
  await h2.tk.writeModuleIntegration('module-parking', {});
@@ -1498,7 +1705,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1498
1705
  h2.app.dispose();
1499
1706
  });
1500
1707
 
1501
- it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(domains/services 不受扰)', async () => {
1708
+ it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(modules/services 不受扰)', async () => {
1502
1709
  const h2 = layerHarness();
1503
1710
  const hyp = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
1504
1711
  const cur = await h2.tk.loadApp();
@@ -1557,7 +1764,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1557
1764
  moduleProvider: 'joycity',
1558
1765
  });
1559
1766
  expect(v.integration).toEqual({ provider: 'wanda' });
1560
- expect(v.providerBindings).toEqual([{ domain: 'parking', effective: 'wanda', layer: 'module' }]);
1767
+ expect(v.binding).toEqual({ provider: 'wanda', layer: 'module' });
1561
1768
  expect(v.configEditable).toBe(true);
1562
1769
  expect(v.integrationFieldRegistry?.provider).toBe('joycity'); // 表单层重定向
1563
1770
  expect(v.integrationSchemas?.provider).toBe('joycity');
@@ -1573,7 +1780,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1573
1780
  'config/integrations.json',
1574
1781
  JSON.stringify({
1575
1782
  instances: [{ id: 'mall-bj-01', name: '北京店' }],
1576
- domains: { parking: { provider: 'joycity' } },
1783
+ modules: { 'module-parking': { provider: 'joycity' } },
1577
1784
  services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
1578
1785
  }),
1579
1786
  );
@@ -1583,7 +1790,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1583
1790
  ).toBeNull();
1584
1791
  h3.app.dispose();
1585
1792
 
1586
- // 可写未绑定(无 domains root.provider——无轴参 → null)
1793
+ // 可写未绑定(无 modules 位无 root.provider——无轴参 → null)
1587
1794
  const h4 = layerHarness();
1588
1795
  writeAppFile(
1589
1796
  h4.app.appDir,
@@ -1599,29 +1806,39 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1599
1806
  expect(unbound.configEditable).toBe(true);
1600
1807
  h4.app.dispose();
1601
1808
 
1602
- // D34 三形态(多域/零域/共享键——带 moduleProvider 仍 null
1809
+ // v6 形态(D34 三形态退役):零归属 → null(带 moduleProvider 仍 null——不豁免);
1810
+ // 多域/跨域归属合法化 → 非 null(override 可计算;unsupplied 回填非供给归属槽)
1603
1811
  const d34 = createTempApp('tbox-v46-d34-');
1604
1812
  invalidateContractsResolver();
1605
1813
  const store34 = joinStore(d34.appDir, 'contracts');
1606
1814
  writeFakeContractsPackage(store34, { version: '0.9.0' });
1607
1815
  linkContracts(d34.appDir, store34, { subdir: 'apps/server' });
1608
1816
  const need = (service: string): { service: string; optional: boolean } => ({ service, optional: false });
1609
- const manifest34 = (name: string, services: Array<{ service: string; optional: boolean }>): string =>
1610
- JSON.stringify({ schemaVersion: 1, name, version: '0.1.0', kind: 'business', contributes: { services }, dependencies: { modules: [] }, env: [] });
1817
+ const manifest34 = (id: string, services: Array<{ service: string; optional: boolean }>): string =>
1818
+ JSON.stringify({ schemaVersion: 1, id, version: '0.1.0', kind: 'business', declare: { owns: services.map((x) => ({ service: x.service })), consumes: services }, dependencies: { modules: [] }, env: [] });
1611
1819
  writeAppFile(d34.appDir, 'packages/module-multi/tbox.module.json', manifest34('module-multi', [need('parking.query'), need('member.account')]));
1612
1820
  writeAppFile(
1613
1821
  d34.appDir,
1614
1822
  'packages/module-zero/tbox.module.json',
1615
- JSON.stringify({ schemaVersion: 1, name: 'module-zero', version: '0.1.0', kind: 'business', contributes: {}, dependencies: { modules: [] }, env: [] }),
1823
+ JSON.stringify({ schemaVersion: 1, id: 'module-zero', version: '0.1.0', kind: 'business', declare: {}, dependencies: { modules: [] }, env: [] }),
1616
1824
  );
1617
1825
  writeAppFile(d34.appDir, 'packages/module-a/tbox.module.json', manifest34('module-a', [need('parking.alpha')]));
1618
1826
  writeAppFile(d34.appDir, 'packages/module-b/tbox.module.json', manifest34('module-b', [need('parking.beta')]));
1619
1827
  const tk34 = createAppToolkit(d34.appDir);
1620
- for (const id of ['module-multi', 'module-zero', 'module-a']) {
1621
- const v = await tk34.loadModule(id, { include: ['integrationFieldRegistry', 'integrationSchemas'], moduleProvider: 'joycity' });
1622
- expect(v.integrationFieldRegistry, id).toBeNull();
1623
- expect(v.integrationSchemas, id).toBeNull();
1624
- }
1828
+ // 零归属:null(唯一 null 形态——override 不豁免)
1829
+ const zero = await tk34.loadModule('module-zero', { include: ['integrationFieldRegistry', 'integrationSchemas'], moduleProvider: 'joycity' });
1830
+ expect(zero.integrationFieldRegistry).toBeNull();
1831
+ expect(zero.integrationSchemas).toBeNull();
1832
+ expect(zero.configEditable).toBe(false);
1833
+ // 多域归属:可写(owns>0)——override joycity 供给 parking.query(member.account → unsupplied)
1834
+ const multi = await tk34.loadModule('module-multi', { include: ['integrationFieldRegistry', 'integrationSchemas'], moduleProvider: 'joycity' });
1835
+ expect(multi.configEditable).toBe(true);
1836
+ expect(multi.integrationSchemas?.provider).toBe('joycity');
1837
+ // d34 fixture 无 provider 包 → joycity 零供给 → 双 owned 全回填(manifest owns 序)
1838
+ expect(multi.integrationFieldRegistry?.unsupplied).toEqual(['parking.query', 'member.account']);
1839
+ // 跨域归属:scope = owns ∩ 供给 = [](parking.alpha 非 joycity 供给)→ 非 null 空集 + unsupplied 全回填
1840
+ const a = await tk34.loadModule('module-a', { include: ['integrationFieldRegistry'], moduleProvider: 'joycity' });
1841
+ expect(a.integrationFieldRegistry).toEqual({ provider: 'joycity', schemaless: [], unsupplied: ['parking.alpha'], additionalPropertiesAllowed: true, fields: [] });
1625
1842
  d34.dispose();
1626
1843
  });
1627
1844
 
@@ -1682,7 +1899,13 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1682
1899
  const props = c.properties;
1683
1900
  const declared = props && typeof props === 'object' && !Array.isArray(props) ? Object.keys(props) : []; // 同款守卫
1684
1901
  const rev = registry.fields.filter((f) => f.sources.some((x) => x.service === s)).map((f) => f.key);
1685
- expect([...declared].sort(), s).toEqual([...rev].sort());
1902
+ // G 批收窄互证:表单键 ⊆ 声明键,且声明侧标记键 ≡ 表单键(未标记键落 ExtraFields——不进 fields)
1903
+ const marked = declared.filter((k) => {
1904
+ const sub = (props as Record<string, Record<string, unknown>>)[k];
1905
+ return !!sub && sub['x-tbox-global'] === true;
1906
+ });
1907
+ expect(rev.every((k) => declared.includes(k)), s).toBe(true);
1908
+ expect([...marked].sort(), s).toEqual([...rev].sort());
1686
1909
  }
1687
1910
  for (const s of registry.schemaless) expect(layer.services[s]?.configSchema).toBeNull();
1688
1911
  // unsupplied ≡ demand − scope 键集
@@ -1692,6 +1915,39 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
1692
1915
  h2.app.dispose();
1693
1916
  });
1694
1917
 
1918
+ it('G 批收窄负例(P16 复合可见面):未标记键在 layer.properties 在场、registry.fields 不收(wanda 实际态 parkLotId)', async () => {
1919
+ const h2 = layerHarness();
1920
+ const v = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas', 'integrationFieldRegistry'] });
1921
+ const layer = v.integrationSchemas!;
1922
+ const registry = v.integrationFieldRegistry!;
1923
+ expect(layer.provider).toBe('wanda');
1924
+ const parkingProps = (layer.services['parking.payment'].configSchema as Record<string, Record<string, unknown>>).properties as Record<string, Record<string, unknown>>;
1925
+ expect(parkingProps.mallId['x-tbox-global']).toBe(true);
1926
+ expect(parkingProps.parkLotId).toBeDefined(); // 原始投影在场
1927
+ expect(parkingProps.parkLotId['x-tbox-global']).toBeUndefined();
1928
+ expect(registry.fields.some((f) => f.key === 'parkLotId')).toBe(false); // 表单收窄不收
1929
+ expect(registry.fields.some((f) => f.key === 'mallId')).toBe(true);
1930
+ // app 轴同律(足迹含 query——baseUrl 标记在场)
1931
+ const appV = await h2.tk.loadApp({ include: ['integrationSchemas', 'integrationFieldRegistry'] });
1932
+ expect(appV.integrationFieldRegistry!.fields.some((f) => f.key === 'baseUrl')).toBe(true);
1933
+ h2.app.dispose();
1934
+ });
1935
+
1936
+ it('G 批服务轴不过滤锁:loadServiceFieldRegistry 全字段带旗标(unmarked → global:false 在场)', async () => {
1937
+ const h2 = layerHarness();
1938
+ const snap = (await import('../src/views/context.js')).loadViewSnapshotSync({ appDir: h2.app.appDir }, await h2.tk.resolveContracts());
1939
+ const reg = loadServiceFieldRegistry(snap, 'parking.payment');
1940
+ expect(reg).not.toBeNull();
1941
+ const keys = reg!.fields.map((f) => f.key).sort();
1942
+ expect(keys).toEqual(['mallId', 'parkLotId']); // 服务轴全量(未标记也入列——旗标 false 如实)
1943
+ const lot = reg!.fields.find((f) => f.key === 'parkLotId')!;
1944
+ expect(lot.global).toBe(false);
1945
+ expect(lot.schema).toEqual({ type: 'string', title: '车场组' }); // 无标记污染
1946
+ const mall = reg!.fields.find((f) => f.key === 'mallId')!;
1947
+ expect(mall.global).toBe(true);
1948
+ h2.app.dispose();
1949
+ });
1950
+
1695
1951
  it('S8 app 轴:unsupplied ≡ [](结构性:足迹 ⊆ 供给)', async () => {
1696
1952
  const h2 = layerHarness();
1697
1953
  const v = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
@@ -1715,8 +1971,8 @@ describe('B1 展示名链', () => {
1715
1971
  version: '0.9.0',
1716
1972
  slots: ['parking.query', 'auth.alipay-login', 'mall.info'],
1717
1973
  meta: {
1718
- 'parking.query': { title: '停车查询', description: '费用报价、车场信息、可享权益' },
1719
- 'auth.alipay-login': { title: '支付宝登录' },
1974
+ 'parking.query': { name: '停车查询', description: '费用报价、车场信息、可享权益' },
1975
+ 'auth.alipay-login': { name: '支付宝登录' },
1720
1976
  },
1721
1977
  });
1722
1978
  linkContracts(app.appDir, store, { subdir: 'apps/server' });
@@ -1726,14 +1982,15 @@ describe('B1 展示名链', () => {
1726
1982
  'packages/module-parking/tbox.module.json',
1727
1983
  JSON.stringify({
1728
1984
  schemaVersion: 1,
1729
- name: 'module-parking',
1985
+ id: 'module-parking',
1730
1986
  version: '0.1.0',
1731
1987
  kind: 'business',
1732
- title: '停车缴费',
1988
+ name: '停车缴费',
1733
1989
  description: '车辆查询与绑定、停车费用查询、缴费支付与开票',
1734
- contributes: {
1735
- // demand meta title=车位查询——应被词汇 meta 停车查询压过(词汇主源)
1736
- services: [{ service: 'parking.query', optional: false, title: '车位查询' }],
1990
+ declare: {
1991
+ owns: [{ service: 'parking.query' }],
1992
+ // demand meta name=车位查询——应被词汇 meta 停车查询压过(词汇主源)
1993
+ consumes: [{ service: 'parking.query', optional: false, name: '车位查询' }],
1737
1994
  },
1738
1995
  dependencies: { modules: [] },
1739
1996
  env: [],
@@ -1745,12 +2002,12 @@ describe('B1 展示名链', () => {
1745
2002
  'packages/provider-demo/tbox.module.json',
1746
2003
  JSON.stringify({
1747
2004
  schemaVersion: 1,
1748
- name: 'provider-demo',
2005
+ id: 'provider-demo',
1749
2006
  version: '0.1.0',
1750
2007
  kind: 'third-party',
1751
- title: '万达',
2008
+ name: '万达',
1752
2009
  description: '万达厂商集成',
1753
- contributes: {
2010
+ declare: {
1754
2011
  providers: {
1755
2012
  slots: [{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'apikey' }],
1756
2013
  },
@@ -1766,49 +2023,89 @@ describe('B1 展示名链', () => {
1766
2023
  it('T1 resolveServiceDisplay 三态:词汇 > demand > id;逐字段回落(词汇仅 description 时 demand title 可达)', () => {
1767
2024
  const walk = {
1768
2025
  slotMeta: {
1769
- 'a.b': { title: '甲', description: '甲描述' },
2026
+ 'a.b': { name: '甲', description: '甲描述' },
1770
2027
  'c.d': { description: '只有描述' },
1771
2028
  },
1772
2029
  catalog: {
1773
2030
  services: {
1774
- 'a.b': { title: '乙', description: '乙描述' },
1775
- 'c.d': { title: '丙' },
2031
+ 'a.b': { name: '乙', description: '乙描述' },
2032
+ 'c.d': { name: '丙' },
1776
2033
  },
1777
2034
  },
1778
2035
  } as unknown as WalkManifestsResult;
1779
2036
  // 词汇全量在场 → 词汇胜出
1780
- expect(resolveServiceDisplay(walk, 'a.b')).toEqual({ title: '甲', description: '甲描述' });
2037
+ expect(resolveServiceDisplay(walk, 'a.b')).toEqual({ name: '甲', description: '甲描述' });
1781
2038
  // 词汇仅 description → title 回落 demand、description 取词汇(逐字段回落)
1782
- expect(resolveServiceDisplay(walk, 'c.d')).toEqual({ title: '丙', description: '只有描述' });
2039
+ expect(resolveServiceDisplay(walk, 'c.d')).toEqual({ name: '丙', description: '只有描述' });
1783
2040
  // 双缺席 → demand
1784
- expect(resolveServiceDisplay(walk, 'e.f')).toEqual({ title: 'e.f' });
2041
+ expect(resolveServiceDisplay(walk, 'e.f')).toEqual({ name: 'e.f' });
1785
2042
  // 双双缺席 → id 兜底(title 恒有值)
1786
2043
  const bare = { slotMeta: {}, catalog: { services: {} } } as unknown as WalkManifestsResult;
1787
- expect(resolveServiceDisplay(bare, 'x.y')).toEqual({ title: 'x.y' });
2044
+ expect(resolveServiceDisplay(bare, 'x.y')).toEqual({ name: 'x.y' });
1788
2045
  });
1789
2046
 
1790
2047
  it('T2 loadModulesView:模块 meta 填充;name 兜底', async () => {
1791
2048
  const h2 = displayHarness();
1792
2049
  const v = await h2.tk.loadModules();
1793
2050
  const parking = v.modules.find((m) => m.id === 'module-parking');
1794
- expect(parking?.title).toBe('停车缴费');
2051
+ expect(parking?.name).toBe('停车缴费');
1795
2052
  expect(parking?.description).toBe('车辆查询与绑定、停车费用查询、缴费支付与开票');
1796
2053
  // 无 meta 模块(provider 家族不在清单——用第二基座验证兜底)
1797
2054
  const h3 = harnessWithContracts();
1798
2055
  const bare = (await h3.tk.loadModules()).modules.find((m) => m.id === 'module-parking');
1799
- expect(bare?.title).toBe('module-parking');
2056
+ expect(bare?.name).toBe('module-parking');
1800
2057
  expect(bare?.description).toBe('');
1801
2058
  h3.app.dispose();
1802
2059
  h2.app.dispose();
1803
2060
  });
1804
2061
 
2062
+ it('T2b loadModulesView:hidden 模块默认过滤;includeHidden 可见 + 徽标;detail 恒 200 不滤', async () => {
2063
+ const app = createTempApp('tbox-hidden-');
2064
+ invalidateContractsResolver();
2065
+ const store = joinStore(app.appDir, 'contracts');
2066
+ writeFakeContractsPackage(store, { version: '0.9.0' });
2067
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
2068
+ const manifest = (id: string, name: string, extra: Record<string, unknown> = {}): string =>
2069
+ JSON.stringify({
2070
+ schemaVersion: 1,
2071
+ id,
2072
+ version: '0.1.0',
2073
+ kind: 'business',
2074
+ name,
2075
+ declare: { owns: [], consumes: [] },
2076
+ dependencies: { modules: [] },
2077
+ env: [],
2078
+ ...extra,
2079
+ });
2080
+ mkdirSync(join(app.appDir, 'packages', 'module-visible'), { recursive: true });
2081
+ writeAppFile(app.appDir, 'packages/module-visible/tbox.module.json', manifest('module-visible', '可见模块'));
2082
+ mkdirSync(join(app.appDir, 'packages', 'module-secret'), { recursive: true });
2083
+ writeAppFile(app.appDir, 'packages/module-secret/tbox.module.json', manifest('module-secret', '隐藏模块', { hidden: true }));
2084
+
2085
+ const tk = createAppToolkit(app.appDir);
2086
+ // 默认:hidden 模块不进清单(可见模块无 hidden 键——additive 纪律)
2087
+ const def = await tk.loadModules();
2088
+ expect(def.modules.map((m) => m.id)).toEqual(['module-visible']);
2089
+ expect(def.modules[0]).not.toHaveProperty('hidden');
2090
+ // includeHidden = true:可见并携带 hidden: true 徽标
2091
+ const inc = await tk.loadModules({ includeHidden: true });
2092
+ expect(inc.modules.map((m) => m.id).sort()).toEqual(['module-secret', 'module-visible']);
2093
+ expect(inc.modules.find((m) => m.id === 'module-secret')?.hidden).toBe(true);
2094
+ expect(inc.modules.find((m) => m.id === 'module-visible')).not.toHaveProperty('hidden');
2095
+ // detail 端点恒 200 不滤——视图携带 hidden 徽标
2096
+ const detail = await tk.loadModule('module-secret');
2097
+ expect(detail.name).toBe('隐藏模块');
2098
+ expect(detail.hidden).toBe(true);
2099
+ app.dispose();
2100
+ });
2101
+
1805
2102
  it('T3 loadModuleDetailView:meta 恒发;services[] 词汇优先(demand 车位查询被词汇停车查询压过——防漂移锁)', async () => {
1806
2103
  const h2 = displayHarness();
1807
2104
  const v = await h2.tk.loadModule('module-parking');
1808
- expect(v.title).toBe('停车缴费');
2105
+ expect(v.name).toBe('停车缴费');
1809
2106
  expect(v.description).toContain('停车费用查询');
1810
2107
  const svc = v.services.find((s) => s.service === 'parking.query');
1811
- expect(svc?.title).toBe('停车查询');
2108
+ expect(svc?.name).toBe('停车查询');
1812
2109
  expect(svc?.description).toBe('费用报价、车场信息、可享权益');
1813
2110
  expect(svc?.required).toBe(true);
1814
2111
  h2.app.dispose();
@@ -1821,22 +2118,63 @@ describe('B1 展示名链', () => {
1821
2118
  h2.app.dispose();
1822
2119
  });
1823
2120
 
2121
+ // ── v7 双键寻址(factory 级——resolve-key 单点经七方法入口;id 优先/展示名兜底/多命中 404)──
2122
+
2123
+ it('v7-DK1 模块双键:展示名「停车缴费」≡ id module-parking(同一视图);读侧 id 恒优先', async () => {
2124
+ const h2 = displayHarness();
2125
+ const byName = await h2.tk.loadModule('停车缴费');
2126
+ const byId = await h2.tk.loadModule('module-parking');
2127
+ expect(byName.name).toBe(byId.name);
2128
+ expect(byName.ownedServices).toEqual(byId.ownedServices);
2129
+ h2.app.dispose();
2130
+ });
2131
+
2132
+ it('v7-DK2 服务双键:展示名「停车查询」→ parking.query(词汇 meta 源);含 include 富化同键', async () => {
2133
+ const h2 = displayHarness();
2134
+ const byName = await h2.tk.loadService('停车查询');
2135
+ expect(byName.service).toBe('parking.query');
2136
+ expect(byName.name).toBe('停车查询');
2137
+ h2.app.dispose();
2138
+ });
2139
+
2140
+ it('v7-DK3 多展示名命中 → 404 带候选(两模块同名;fail-visible 不静默取首)', async () => {
2141
+ const app = createTempApp('tbox-dk3-');
2142
+ invalidateContractsResolver();
2143
+ const store = joinStore(app.appDir, 'contracts');
2144
+ writeFakeContractsPackage(store, { version: '0.9.0' });
2145
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
2146
+ const manifest = (id: string): string =>
2147
+ JSON.stringify({ schemaVersion: 1, id, name: '同名模块', version: '0.1.0', kind: 'business', declare: {}, dependencies: { modules: [] }, env: [] });
2148
+ writeAppFile(app.appDir, 'packages/module-a/tbox.module.json', manifest('module-a'));
2149
+ writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b'));
2150
+ const tk = createAppToolkit(app.appDir);
2151
+ await expect(tk.loadModule('同名模块')).rejects.toMatchObject({
2152
+ code: 'MODULE_NOT_FOUND',
2153
+ httpStatus: 404,
2154
+ message: expect.stringContaining('id=module-a'),
2155
+ });
2156
+ await expect(tk.loadModule('同名模块')).rejects.toMatchObject({
2157
+ message: expect.stringContaining('改用模块 id'),
2158
+ });
2159
+ app.dispose();
2160
+ });
2161
+
1824
2162
  it('T4 loadAppView 平台条目:title 走链恒发;词汇无 meta 槽 = id 兜底', async () => {
1825
2163
  const h2 = displayHarness();
1826
2164
  const v = await h2.tk.loadApp();
1827
2165
  const byKey = Object.fromEntries(v.services.map((s) => [s.service, s]));
1828
2166
  // 平台条目 = 词汇 − 占用(parking.query 被模块占用)
1829
2167
  expect(Object.keys(byKey).sort()).toEqual(['auth.alipay-login', 'mall.info']);
1830
- expect(byKey['auth.alipay-login']?.title).toBe('支付宝登录');
1831
- expect(byKey['mall.info']?.title).toBe('mall.info');
2168
+ expect(byKey['auth.alipay-login']?.name).toBe('支付宝登录');
2169
+ expect(byKey['mall.info']?.name).toBe('mall.info');
1832
2170
  expect(byKey['auth.alipay-login']?.required).toBe(false);
1833
2171
  h2.app.dispose();
1834
2172
  });
1835
2173
 
1836
2174
  it('T5 loadServiceDetailView:词汇优先 + id 兜底恒有值', async () => {
1837
2175
  const h2 = displayHarness();
1838
- expect((await h2.tk.loadService('parking.query')).title).toBe('停车查询');
1839
- expect((await h2.tk.loadService('mall.info')).title).toBe('mall.info');
2176
+ expect((await h2.tk.loadService('parking.query')).name).toBe('停车查询');
2177
+ expect((await h2.tk.loadService('mall.info')).name).toBe('mall.info');
1840
2178
  h2.app.dispose();
1841
2179
  });
1842
2180
 
@@ -1844,11 +2182,11 @@ describe('B1 展示名链', () => {
1844
2182
  const h2 = displayHarness();
1845
2183
  const list = await h2.tk.loadProviders();
1846
2184
  const wanda = list.providers.find((p) => p.provider === 'wanda');
1847
- expect(wanda?.title).toBe('万达');
2185
+ expect(wanda?.name).toBe('万达');
1848
2186
  expect(wanda?.description).toBe('万达厂商集成');
1849
2187
  const detail = await h2.tk.loadProvider('wanda');
1850
- expect(detail.title).toBe('万达');
1851
- expect(detail.services['parking.query']?.title).toBe('停车查询');
2188
+ expect(detail.name).toBe('万达');
2189
+ expect(detail.services['parking.query']?.name).toBe('停车查询');
1852
2190
  expect(detail.services['parking.query']?.description).toBe('费用报价、车场信息、可享权益');
1853
2191
  h2.app.dispose();
1854
2192
  });