@tbox.cn/app-toolkit 0.3.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 (41) 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 +382 -129
  4. package/dist/index.js +9 -8
  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 +216 -96
  9. package/src/core/contracts-expected.ts +2 -2
  10. package/src/core/contracts-resolver.ts +12 -1
  11. package/src/core/env-expansion.ts +14 -12
  12. package/src/core/module-schema.ts +88 -12
  13. package/src/dto.ts +53 -25
  14. package/src/factory.ts +62 -18
  15. package/src/index.ts +18 -7
  16. package/src/integrations/evaluate-app.ts +41 -0
  17. package/src/integrations/mock-bindings.ts +1 -1
  18. package/src/integrations/module-binding.ts +60 -0
  19. package/src/integrations/predicate-io.ts +26 -5
  20. package/src/integrations/predicate.ts +97 -57
  21. package/src/integrations/prune-bindings.ts +205 -0
  22. package/src/integrations/write.ts +43 -27
  23. package/src/views/app.ts +8 -4
  24. package/src/views/context.ts +26 -7
  25. package/src/views/integration-schemas.ts +61 -17
  26. package/src/views/modules.ts +45 -27
  27. package/src/views/providers.ts +13 -1
  28. package/src/views/service-detail.ts +5 -3
  29. package/src/views/service-resolutions.ts +14 -13
  30. package/tests/credentials-view.test.ts +2 -2
  31. package/tests/demo-app.ts +34 -5
  32. package/tests/dev-manifest-catalog.test.ts +86 -6
  33. package/tests/file-cache.test.ts +5 -5
  34. package/tests/module-schema.test.ts +117 -16
  35. package/tests/resolve-key.test.ts +154 -0
  36. package/tests/vendor-schema-keywords.test.ts +86 -0
  37. package/tests/views.test.ts +425 -139
  38. package/tests/write-core.test.ts +83 -0
  39. package/dist/chunk-KPD3LUH4.js +0 -1
  40. package/dist/contracts-resolver-E4SECBOG.js +0 -1
  41. package/src/integrations/domain-binding.ts +0 -81
@@ -1,14 +1,14 @@
1
- import type { ModuleDeclaration, ModuleDetailView, ProviderBinding, ServiceDemand } from '../dto.js';
1
+ import type { ModuleDeclaration, ModuleBinding, ModuleDetailView, ServiceDemand } from '../dto.js';
2
2
  import { AppToolkitError } from '../core/errors.js';
3
- import { moduleDomainWritable, resolveDomainBinding } from '../integrations/domain-binding.js';
4
- import type { ViewSnapshot } from './context.js';
3
+ import { moduleOwnershipWritable, resolveModuleBinding } from '../integrations/module-binding.js';
4
+ import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
5
5
 
6
6
  /**
7
7
  * 需求轴/模块静态投影(views/modules;C4)。静态端点结构性零求值(铁律 #3——
8
8
  * 无 resolution/issues/contracts 键,永不 503)。
9
- * v4.4 D34:domainKeys/providerBindings/configEditable 显式化;integration 仅单域
10
- * (writable)时在场——多域/零域/共享键无单键落点;多域回退兜底键机器拆除
11
- * (派生统一 = domainOfService 去重集合,moduleDomainWritable 单源)。
9
+ * v6 所有权模型:ownedServices(declare.owns 展平)/ binding 单数(resolveModuleBinding)/
10
+ * configEditable = owns>0moduleOwnershipWritable 单源);integration = modules[install id]
11
+ * 集成位投影(provider/config;resources 不投影——N2)。v5 domainKeys/providerBindings 退役。
12
12
  * 2026-09-16 双缺陷批:loadModulesView 排除 provider 家族包(供给/实施载体非可配置
13
13
  * 业务模块)——walk 声明集与 loadModuleDetailView 不变(装包仍合法,详情保持 200)。
14
14
  */
@@ -19,7 +19,7 @@ function familyName(s: string): string {
19
19
  return s.replace(/^@tbox\.cn\/app-/, '').replace(/^@[^/]+\//, '');
20
20
  }
21
21
 
22
- /** provider 家族包判定(模块清单排除供给/实施载体;生态三先例同源:verify-cli 排除判定 /
22
+ /** provider 家族包判定(模块清单排除供给/实施载体;生态三先例同源:stage 排除判定 /
23
23
  * guard-vendor-neutrality「provider-* 包」/ cli deriveModuleId 家族推导)。install id 与
24
24
  * 包名 basename 双查 = 防 .tbox/app.json 登记漂移纵深;命名而非结构(provider-custom 零贡献
25
25
  * 空壳与「零贡献合法」形态不可结构区分);未来收敛 = manifest 显式角色标记(届时整体退场)。 */
@@ -33,35 +33,53 @@ export function loadModulesView(snapshot: ViewSnapshot): { modules: ModuleDeclar
33
33
  .filter((mod) => !isProviderFamily(mod))
34
34
  .map((mod) => ({
35
35
  id: mod.id,
36
- name: mod.descriptor.name,
37
- // 模块级 title/description:manifest 无模块级 meta 字段(demand meta 属 per-service)——
38
- // 一期回退 install id / 空描述(UI 侧按词汇表或 demand meta 增强)
39
- title: mod.descriptor.name,
40
- description: '',
36
+ // v7 词汇统一:name = 展示名(manifest name;兜底 id——恒有值)
37
+ name: mod.descriptor.name ?? mod.descriptor.id,
38
+ description: mod.descriptor.description ?? '',
41
39
  }))
42
40
  .sort((a, b) => a.id.localeCompare(b.id));
43
41
  return { modules };
44
42
  }
45
43
 
46
- /** GET /modules/:module:模块静态一站式(integration 节点原值 + demand;PUT 读对偶) */
44
+ /** GET /modules/:module:模块静态一站式(modules[install id] 集成位原值 + demand;PUT 读对偶) */
47
45
  export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string): ModuleDetailView {
48
46
  const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
49
47
  if (!mod) {
50
48
  throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
51
49
  }
52
- const services: ServiceDemand[] = (mod.descriptor.contributes.services ?? []).map((need) => ({
53
- service: need.service,
54
- ...(need.title ? { title: need.title } : {}),
55
- ...(need.description ? { description: need.description } : {}),
56
- required: need.optional === false,
57
- }));
58
- const w = moduleDomainWritable(snapshot.walk, mod);
59
- const domains = (snapshot.integrations as { domains?: Record<string, unknown> } | null)?.domains ?? {};
60
- // integration = domains[换算键] 节点原值(D21 编辑预填源);未配置 = null;多域/零域/共享键 = null(D34)
61
- const integration = w.writable ? ((domains[w.domainKey] as ModuleDetailView['integration']) ?? null) : null;
62
- const providerBindings: ProviderBinding[] = w.domainKeys.map((d) => {
63
- const binding = resolveDomainBinding(snapshot.integrations, d);
64
- return { domain: d, effective: binding.provider, layer: binding.layer };
50
+ const services: ServiceDemand[] = (mod.descriptor.declare.consumes ?? []).map((need) => {
51
+ // 展示名单源走链(B1):词汇 meta > demand meta > id——词汇主源防 manifest demand 漂移
52
+ const display = resolveServiceDisplay(snapshot.walk, need.service);
53
+ return {
54
+ service: need.service,
55
+ name: display.name,
56
+ ...(display.description !== undefined ? { description: display.description } : {}),
57
+ required: need.optional === false,
58
+ };
65
59
  });
66
- return { domainKeys: [...w.domainKeys], integration, providerBindings, configEditable: w.writable, services };
60
+ const w = moduleOwnershipWritable(snapshot.walk, mod);
61
+ const modulesNode = (snapshot.integrations as { modules?: Record<string, unknown> } | null)?.modules ?? {};
62
+ // integration = modules[install id] 集成位投影(D21 编辑预填源;provider/config 两键——
63
+ // resources 不投影不写,N2);未配置 = null
64
+ const rawNode = modulesNode[moduleId] as Record<string, unknown> | undefined;
65
+ const integration: ModuleDetailView['integration'] = rawNode
66
+ ? {
67
+ ...(typeof rawNode.provider === 'string' ? { provider: rawNode.provider } : {}),
68
+ ...(rawNode.config && typeof rawNode.config === 'object' && !Array.isArray(rawNode.config)
69
+ ? { config: rawNode.config as Record<string, unknown> }
70
+ : {}),
71
+ }
72
+ : null;
73
+ const binding: ModuleBinding = resolveModuleBinding(snapshot.integrations, moduleId);
74
+ return {
75
+ // v7 词汇统一:name = 展示名(兜底与 /modules 同语义——manifest name ?? id)
76
+ name: mod.descriptor.name ?? mod.descriptor.id,
77
+ description: mod.descriptor.description ?? '',
78
+ // v6:ownedServices(declare.owns 展平排序)——N13:展示名走 resolveServiceDisplay 链,不重复携带
79
+ ownedServices: w.ownedServices,
80
+ integration,
81
+ binding: { provider: binding.provider, layer: binding.layer },
82
+ configEditable: w.writable,
83
+ services,
84
+ };
67
85
  }
@@ -2,7 +2,7 @@ import type { ProviderDetail, ProviderEntry, ProviderServiceDetail, ProvidersVie
2
2
  import { AppToolkitError } from '../core/errors.js';
3
3
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
4
4
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
5
- import type { ViewSnapshot } from './context.js';
5
+ import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
6
6
 
7
7
  /**
8
8
  * 供给轴投影(views/providers;C4)。供给关系三切片(P5):
@@ -21,6 +21,9 @@ export function loadProvidersView(snapshot: ViewSnapshot): ProvidersView {
21
21
  provider,
22
22
  local: entries.every((e) => e.local === true),
23
23
  owner: entries[0].owner,
24
+ // 厂商展示元数据(B1 walk 层传播——首条目 = 首声明,与 owner 同语义)
25
+ ...(entries[0].name ? { name: entries[0].name } : {}),
26
+ ...(entries[0].description ? { description: entries[0].description } : {}),
24
27
  });
25
28
  }
26
29
  return { providers: providers.sort((a, b) => a.provider.localeCompare(b.provider)) };
@@ -40,6 +43,12 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
40
43
  bucket.implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
41
44
  }
42
45
  }
46
+ // 服务展示名走链(B1):词汇 meta > demand meta > id(name 恒发)
47
+ for (const [service, bucket] of Object.entries(services)) {
48
+ const display = resolveServiceDisplay(snapshot.walk, service);
49
+ bucket.name = display.name;
50
+ if (display.description !== undefined) bucket.description = display.description;
51
+ }
43
52
  if (include?.has('integrationSchemas')) {
44
53
  for (const [service, bucket] of Object.entries(services)) {
45
54
  bucket.integrationSchemas = { configSchema: resolveServiceConfigSchema(snapshot, provider, service) };
@@ -50,6 +59,9 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
50
59
  provider,
51
60
  local: entries.every((e) => e.local === true),
52
61
  owner: entries[0].owner,
62
+ // 厂商展示元数据(B1 walk 层传播——首条目透传;v7 title→name)
63
+ ...(entries[0].name ? { name: entries[0].name } : {}),
64
+ ...(entries[0].description ? { description: entries[0].description } : {}),
53
65
  services,
54
66
  };
55
67
  }
@@ -1,7 +1,7 @@
1
1
  import type { ServiceDetail } from '../dto.js';
2
2
  import { AppToolkitError } from '../core/errors.js';
3
3
  import { isKnownService } from '../integrations/read.js';
4
- import { suppliedByVendors, type ViewSnapshot } from './context.js';
4
+ import { resolveServiceDisplay, suppliedByVendors, type ViewSnapshot } from './context.js';
5
5
 
6
6
  /**
7
7
  * 服务静态单体投影(views/service-detail;C4)。求值数据不在此(→ /service-resolutions/:service)。
@@ -17,10 +17,12 @@ export function loadServiceDetailView(snapshot: ViewSnapshot, service: string):
17
17
  }
18
18
  // integration = 文件 services[s] 逐字原值(编辑预填源 D21);无声明 = null
19
19
  const integration = (configured[service] as ServiceDetail['integration'] | undefined) ?? null;
20
+ // 展示名单源走链(B1):词汇 meta > demand meta > id——替换 demand 直拼(恒有值)
21
+ const display = resolveServiceDisplay(snapshot.walk, service);
20
22
  return {
21
23
  service,
22
- ...(demand?.title ? { title: demand.title } : { title: service }),
23
- ...(demand?.description ? { description: demand.description } : {}),
24
+ name: display.name,
25
+ ...(display.description !== undefined ? { description: display.description } : {}),
24
26
  required: demand ? !demand.optional : false,
25
27
  // defaults 唯一落位(manifest 预填)
26
28
  defaults: demand ? demand.defaults.map((d) => ({ ...d })) : [],
@@ -11,10 +11,10 @@ import type {
11
11
  ServiceStatus,
12
12
  } from '../dto.js';
13
13
  import { AppToolkitError } from '../core/errors.js';
14
- import { resolveDomainBinding } from '../integrations/domain-binding.js';
14
+ import { resolveModuleBinding } from '../integrations/module-binding.js';
15
15
  import type { ResolvedContracts } from '../core/contracts-resolver.js';
16
16
  import type { WalkCatalog } from '../assembly/walk-manifests.js';
17
- import { domainOfService, isSupplied, type ViewSnapshot } from './context.js';
17
+ import { isSupplied, type ViewSnapshot } from './context.js';
18
18
 
19
19
  /**
20
20
  * 求值轴投影(views/service-resolutions;C4;FX-2c——B6 effective 填充 + F2 layer 推导;
@@ -50,22 +50,22 @@ interface InstanceEvaluation {
50
50
  effective?: EffectiveBinding;
51
51
  }
52
52
 
53
- /** 最特异声明层推导(F2):inst 位 provider 在场 → 'instance';槽级 → 'service';域级 → 'module';
53
+ /** 最特异声明层推导(F2):inst 位 provider 在场 → 'instance';槽级 → 'service';模块级 → 'module';
54
54
  * root → 'app'。实例位查找复刻 TIER0 语义(精确键优先、'*' 通配兜底——与 resolveEffectiveService
55
- * 的 instances 查找层同轴)。v4.4 F1:域级段消费 resolveDomainBinding 单源;返回扩 null
56
- * (四层全无 provider 声明 → null——Entry.layer「未配置双缺席」口径,D31)。 */
55
+ * 的 instances 查找层同轴)。v6:模块级段消费 resolveModuleBinding 单源(ownerOf=walk.ownership);
56
+ * 返回扩 null(四层全无 provider 声明 → null——Entry.layer「未配置双缺席」口径,D31)。 */
57
57
  function deriveBindingLayer(snapshot: ViewSnapshot, service: string, instanceId: string): BindingLayer | null {
58
58
  const integrations = snapshot.integrations as {
59
59
  provider?: unknown;
60
- domains?: Record<string, { provider?: unknown }>;
61
60
  services?: Record<string, { provider?: unknown; instances?: Record<string, { provider?: unknown }> }>;
62
61
  } | null;
63
- const domain = domainOfService(service);
64
62
  const slot = integrations?.services?.[service];
65
63
  const inst = slot?.instances?.[instanceId] ?? slot?.instances?.['*'];
66
64
  if (inst && inst.provider !== undefined) return 'instance';
67
65
  if (slot?.provider !== undefined) return 'service';
68
- if (resolveDomainBinding(integrations, domain).layer === 'module') return 'module';
66
+ const owner = snapshot.walk.ownership[service];
67
+ if (owner !== undefined
68
+ && resolveModuleBinding(snapshot.integrations, owner).layer === 'module') return 'module';
69
69
  if (typeof integrations?.provider === 'string' && integrations.provider.length > 0) return 'app';
70
70
  return null;
71
71
  }
@@ -292,10 +292,8 @@ function buildInheritance(
292
292
  ): ServiceResolution['inheritance'] {
293
293
  const integrations = snapshot.integrations as {
294
294
  provider?: string;
295
- domains?: Record<string, { provider?: string }>;
296
295
  services?: Record<string, { provider?: unknown; implementation?: string; instances?: Record<string, { provider?: string; implementation?: string }> }>;
297
296
  } | null;
298
- const domain = domainOfService(service);
299
297
  const slot = integrations?.services?.[service];
300
298
  const cfg = snapshot.integrations as { instances?: unknown; defaultInstanceId?: unknown } | null;
301
299
  const normalized = evaluationContracts(snapshot).normalizeInstances({
@@ -304,13 +302,16 @@ function buildInheritance(
304
302
  });
305
303
  const defaultInstanceId = normalized.defaultInstanceId ?? '*';
306
304
  const defaultInstance = slot?.instances?.[defaultInstanceId] ?? slot?.instances?.['*'];
307
- // module 段消费 resolveDomainBinding 单源(v4.4 F1——与 ProviderBinding / 谓词 #7 同源)
308
- const domainBinding = resolveDomainBinding(integrations as Record<string, unknown> | null, domain);
305
+ // module 段消费 resolveModuleBinding 单源(v6——与 binding / 谓词 #7 同源;ownerOf=walk.ownership)
306
+ const owner = snapshot.walk.ownership[service];
307
+ const moduleBinding = owner !== undefined
308
+ ? resolveModuleBinding(integrations as Record<string, unknown> | null, owner)
309
+ : { provider: null as string | null, layer: null as 'module' | 'app' | null };
309
310
  return [
310
311
  { layer: 'app' as const, ...(integrations?.provider ? { provider: integrations.provider } : {}) },
311
312
  {
312
313
  layer: 'module' as const,
313
- ...(domainBinding.layer === 'module' && domainBinding.provider ? { provider: domainBinding.provider } : {}),
314
+ ...(moduleBinding.layer === 'module' && moduleBinding.provider ? { provider: moduleBinding.provider } : {}),
314
315
  },
315
316
  {
316
317
  layer: 'service' as const,
@@ -31,10 +31,10 @@ function credHarness(): { app: TempApp; tk: AppToolkit } {
31
31
  'packages/provider-w/tbox.module.json',
32
32
  JSON.stringify({
33
33
  schemaVersion: 1,
34
- name: 'provider-w',
34
+ id: 'provider-w',
35
35
  version: '0.1.0',
36
36
  kind: 'business',
37
- contributes: {
37
+ declare: {
38
38
  providers: {
39
39
  slots: [
40
40
  { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c-signed-v1', credentialSchema: 'schemas/cred.json' },
package/tests/demo-app.ts CHANGED
@@ -46,10 +46,18 @@ export interface FakeContractsOptions {
46
46
  /** 严格校验失败 fixture(FX-1c T5):parseIntegrationsConfig 对含该子串文档 throw */
47
47
  rejectMarker?: string;
48
48
  /** resolveEffectiveService 自定义函数体(FX-2c O4 fixture:默认恒 service-not-configured;
49
- * 入参 (integrations, service, instanceId, supplies),需 return EffectiveServiceResolution 形状) */
49
+ * 入参 (integrations, service, instanceId, supplies, ownerOf),需 return EffectiveServiceResolution 形状) */
50
50
  resolveBody?: string;
51
+ /** v6 形状标记(OWNERSHIP_BINDING_V6)缺席 fixture——默认在场(v6 假件签名随动 N11);
52
+ * false = v5 contracts 形态(ownershipBindingV6: false → assertContractsUsable 503 第四分支) */
53
+ withMarker?: boolean;
51
54
  /** service-slots.json 词汇(P9 身份判据/求值键集;缺省不物化 = 词汇空) */
52
55
  slots?: string[];
56
+ /** 词汇 meta 节(per-service {name,description}——B1 展示名链 fixture) */
57
+ meta?: Record<string, { name?: string; description?: string }>;
58
+ /** service-slots.json 落位:'root' = 包根(缺省——夹具/旧布局形态);
59
+ * 'src' = 包内 src/(发布包 src 附带 / shim 链接形态——B0 探测跳 ②) */
60
+ slotsLayout?: 'root' | 'src';
53
61
  /** zod 形拒绝 fixture(A1 T9/T10):parseIntegrationsConfig 恒抛带 issues 的结构化错误
54
62
  * (结构性 ZodError 形——issues[].path 供 fields 断言;仅在专属 fixture 上使用) */
55
63
  zodRejectPaths?: string[][];
@@ -58,7 +66,7 @@ export interface FakeContractsOptions {
58
66
  /** 假 contracts 包:package.json exports 形态逐字段复刻发布面(types+import 双条件、无 require/default)
59
67
  * ——resolver 不经 exports 解析(包根直探),该形态为 E4 类错误的机械防线。 */
60
68
  export function writeFakeContractsPackage(packageRoot: string, opts: FakeContractsOptions = {}): void {
61
- const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, slots, zodRejectPaths } = opts;
69
+ const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, withMarker = true, slots, meta, slotsLayout = 'root', zodRejectPaths } = opts;
62
70
  const strictBody = withStrict
63
71
  ? `export function parseIntegrationsConfig(raw) { ${
64
72
  rejectMarker ? `if (JSON.stringify(raw).includes(${JSON.stringify(rejectMarker)})) throw new Error("fake strict reject"); ` : ''
@@ -68,9 +76,10 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
68
76
  : ''
69
77
  }return raw; }`
70
78
  : '';
71
- const resolveLine = `export function resolveEffectiveService(integrations, service, instanceId, supplies) { ${
79
+ const resolveLine = `export function resolveEffectiveService(integrations, service, instanceId, supplies, ownerOf) { ${
72
80
  resolveBody ?? 'return { status: "service-not-configured" };'
73
81
  } }`;
82
+ const markerLine = withMarker ? 'export const OWNERSHIP_BINDING_V6 = true;' : '';
74
83
  const exportsBody = missingEvaluationExports
75
84
  ? `export const marker = 1;\n${strictBody}`
76
85
  : [
@@ -79,6 +88,7 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
79
88
  'export function moduleDomainOf(service) { return String(service).split(".")[0] ?? ""; }',
80
89
  'export function normalizeInstances(config) { return { instances: (config && config.instances) || [], defaultInstanceId: (config && config.defaultInstanceId) ?? null }; }',
81
90
  'export const RESOURCE_TYPE_IDS = ["knowledge"];',
91
+ markerLine,
82
92
  strictBody,
83
93
  '',
84
94
  ]
@@ -113,12 +123,31 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
113
123
  mkdirSync(join(packageRoot, 'src'), { recursive: true });
114
124
  writeFileSync(join(packageRoot, 'src', 'runtime.ts'), exportsBody);
115
125
  }
116
- // service-slots.json(P9 词汇源——探测链发现位 = 包根;缺省不物化 = 词汇空)
126
+ // service-slots.json(P9 词汇源——探测三跳发现位;缺省不物化 = 词汇空;
127
+ // slotsLayout 控制包根/src 落位——B0 探测跳①/②)
117
128
  if (slots && slots.length > 0) {
118
- writeFileSync(join(packageRoot, 'service-slots.json'), `${JSON.stringify({ slots }, null, 2)}\n`);
129
+ const slotsFile = slotsLayout === 'src' ? join(packageRoot, 'src', 'service-slots.json') : join(packageRoot, 'service-slots.json');
130
+ mkdirSync(dirname(slotsFile), { recursive: true });
131
+ writeFileSync(slotsFile, `${JSON.stringify(meta ? { slots, meta } : { slots }, null, 2)}\n`);
119
132
  }
120
133
  }
121
134
 
135
+ /**
136
+ * 候选自身 src 词汇包(B0 探测跳 ③——local/codegen 全量副本形态):
137
+ * 落 <appDir>/packages/<name>/src/service-slots.json(无 package.json/node_modules——
138
+ * mall 生成应用主形态:stage rewriteExportsToSrc 展开后永驻 packages/*,不依赖 install)。
139
+ */
140
+ export function writeSelfSrcContractPackage(
141
+ appDir: string,
142
+ name: string,
143
+ content: { slots: string[]; meta?: Record<string, { name?: string; description?: string }> },
144
+ ): string {
145
+ const dir = join(appDir, 'packages', name, 'src');
146
+ mkdirSync(dir, { recursive: true });
147
+ writeFileSync(join(dir, 'service-slots.json'), `${JSON.stringify(content, null, 2)}\n`);
148
+ return join(appDir, 'packages', name);
149
+ }
150
+
122
151
  export interface LinkOptions {
123
152
  /** 候选子目录:''(appDir 根)/ 'apps/server' / 'apps/client' / 'packages/<id>' */
124
153
  subdir: string;
@@ -2,7 +2,7 @@ import { describe, it, expect, afterEach } from 'vitest';
2
2
  import { mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { walkManifests } from '../src/assembly/walk-manifests.js';
5
- import { createTempApp, type TempApp } from './demo-app.js';
5
+ import { createTempApp, linkContracts, writeFakeContractsPackage, writeSelfSrcContractPackage, type TempApp } from './demo-app.js';
6
6
 
7
7
  /**
8
8
  * dev-manifest catalog 通道(通道 1.5)单测:聚合宽/声明窄不变量锁定。
@@ -11,20 +11,20 @@ import { createTempApp, type TempApp } from './demo-app.js';
11
11
  */
12
12
 
13
13
  /** strict descriptor(形状对齐 provider-custom 出厂 manifest——parseModuleDescriptor 严格面) */
14
- function descriptor(name: string, provider: string, service: string): Record<string, unknown> {
14
+ function descriptor(id: string, provider: string, service: string): Record<string, unknown> {
15
15
  return {
16
16
  schemaVersion: 1,
17
- name,
17
+ id,
18
18
  version: '1.0.0',
19
19
  kind: 'business',
20
20
  risk: { level: 'low', writeBoundary: 'source' },
21
21
  distribution: { defaultMode: 'codegen' },
22
- contributes: {
23
- handlers: [], tools: [], cards: [], routes: [], pages: [], tabs: [],
22
+ declare: {
23
+ handlers: [], tools: [], cards: [], routes: [], pages: [], tabs: [], owns: [], consumes: [],
24
24
  providers: {
25
25
  slots: [{ service, provider, implementation: `${provider}-impl@1`, credentialType: 'apikey' }],
26
26
  },
27
- services: [],
27
+ resources: [],
28
28
  },
29
29
  dependencies: { modules: [] },
30
30
  env: [],
@@ -112,3 +112,83 @@ describe('walkManifests · dev-manifest catalog 通道(聚合宽/声明窄)'
112
112
  expect(walk.modules).toHaveLength(0);
113
113
  });
114
114
  });
115
+
116
+ /**
117
+ * 词汇探测三跳(B0 回归锁):词汇源三真实布局全覆盖——
118
+ * ① nm 包根(夹具/旧布局)② nm src/(发布包 src 附带 + 模板 shim 链接)③ 候选自身 src/
119
+ * (local/codegen 全量副本——mall 生成应用主形态,不依赖 install)。
120
+ * 修复前探测只扫包根 → 三形态全 miss(vocabulary 恒空)。
121
+ */
122
+ describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
123
+ it('P1: nm 包根布局(跳①)→ vocabulary 命中;slotMeta 透传', () => {
124
+ app = createTempApp('tbox-probe-');
125
+ const store = join(app.appDir, '_store-a');
126
+ writeFakeContractsPackage(store, {
127
+ slots: ['auth.alipay-login', 'mall.info'],
128
+ meta: { 'auth.alipay-login': { name: '支付宝登录', description: '登录换码(code2Session)' } },
129
+ });
130
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
131
+
132
+ const walk = walkManifests(app.appDir);
133
+ expect(walk.vocabulary).toEqual(['auth.alipay-login', 'mall.info']);
134
+ expect(walk.slotMeta['auth.alipay-login']).toEqual({ name: '支付宝登录', description: '登录换码(code2Session)' });
135
+ expect(walk.slotMeta['mall.info']).toBeUndefined(); // meta 缺席槽不入表
136
+ });
137
+
138
+ it('P2: nm src 布局(跳②——发布包 src 附带 / shim 链接形态)→ 命中', () => {
139
+ app = createTempApp('tbox-probe-');
140
+ const store = join(app.appDir, '_store-b');
141
+ writeFakeContractsPackage(store, { slots: ['parking.query'], slotsLayout: 'src' });
142
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
143
+
144
+ const walk = walkManifests(app.appDir);
145
+ expect(walk.vocabulary).toEqual(['parking.query']);
146
+ });
147
+
148
+ it('P3: 候选自身 src 布局(跳③——local/codegen 全量副本,无 node_modules 链接)→ 命中', () => {
149
+ app = createTempApp('tbox-probe-');
150
+ writeSelfSrcContractPackage(app.appDir, 'contracts-mall', {
151
+ slots: ['member.account', 'parking.payment'],
152
+ meta: { 'member.account': { name: '会员账户' } },
153
+ });
154
+
155
+ const walk = walkManifests(app.appDir);
156
+ expect(walk.vocabulary).toEqual(['member.account', 'parking.payment']);
157
+ expect(walk.slotMeta['member.account']).toEqual({ name: '会员账户' });
158
+ });
159
+
160
+ it('P4: 多包同槽 meta → 发现序首声明胜出(apps/* 先于 packages/* 候选序)', () => {
161
+ app = createTempApp('tbox-probe-');
162
+ const storeA = join(app.appDir, '_store-first');
163
+ writeFakeContractsPackage(storeA, { slots: ['x.y'], meta: { 'x.y': { name: '甲' } } });
164
+ linkContracts(app.appDir, storeA, { subdir: 'apps/server' });
165
+ const storeB = join(app.appDir, '_store-second');
166
+ writeFakeContractsPackage(storeB, { slots: ['x.y'], meta: { 'x.y': { name: '乙' } } });
167
+ linkContracts(app.appDir, storeB, { subdir: 'packages/mock' });
168
+
169
+ const walk = walkManifests(app.appDir);
170
+ expect(walk.slotMeta['x.y']).toEqual({ name: '甲' });
171
+ });
172
+
173
+ it('P5: 坏 meta 条目(非对象/非 string/空串)→ 宽松跳过,slots 正常', () => {
174
+ app = createTempApp('tbox-probe-');
175
+ const store = join(app.appDir, '_store-bad');
176
+ writeFakeContractsPackage(store, { slots: ['a.b'] });
177
+ writeFileSync(
178
+ join(store, 'service-slots.json'),
179
+ JSON.stringify({ slots: ['a.b', 'c.d', 'e.f'], meta: { 'a.b': '字符串', 'c.d': { name: 42 }, 'e.f': { name: '甲', description: '' } } }),
180
+ );
181
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
182
+
183
+ const walk = walkManifests(app.appDir);
184
+ expect(walk.vocabulary).toEqual(['a.b', 'c.d', 'e.f']);
185
+ expect(walk.slotMeta).toEqual({ 'e.f': { name: '甲' } }); // 空串 description 不入表(与 title 守卫对称)
186
+ });
187
+
188
+ it('P6: 无契约包(退化态)→ vocabulary 空 + slotMeta 空(P9 退化路径语义锁)', () => {
189
+ app = createTempApp('tbox-probe-');
190
+ const walk = walkManifests(app.appDir);
191
+ expect(walk.vocabulary).toEqual([]);
192
+ expect(walk.slotMeta).toEqual({});
193
+ });
194
+ });
@@ -132,7 +132,7 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
132
132
  mkdirSync(join(app.appDir, 'packages', 'module-new'), { recursive: true });
133
133
  writeFileSync(
134
134
  join(app.appDir, 'packages', 'module-new', 'tbox.module.json'),
135
- JSON.stringify({ schemaVersion: 1, name: 'module-new', contributes: { services: [{ service: 'new.slot' }] } }),
135
+ JSON.stringify({ schemaVersion: 1, id: 'module-new', declare: { owns: [{ service: 'new.slot' }], consumes: [{ service: 'new.slot' }] } }),
136
136
  );
137
137
  const walk = walkManifests(app.appDir);
138
138
  expect(walk.modules.map((m) => m.id)).toContain('module-new');
@@ -172,10 +172,10 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
172
172
  join(app.appDir, 'packages', 'provider-x', 'tbox.module.json'),
173
173
  JSON.stringify({
174
174
  schemaVersion: 1,
175
- name: 'provider-x',
176
- contributes: {
175
+ id: 'provider-x',
176
+ declare: {
177
177
  providers: { slots: [{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c' }] },
178
- services: [{ service: 'member.account' }],
178
+ consumes: [{ service: 'member.account' }],
179
179
  },
180
180
  }),
181
181
  );
@@ -196,7 +196,7 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
196
196
  mkdirSync(sdkDir, { recursive: true });
197
197
  writeFileSync(
198
198
  join(sdkDir, 'tbox.module.json'),
199
- JSON.stringify({ schemaVersion: 1, name: 'module-sdk-y', contributes: { services: [{ service: 'sdk.slot-y', title: 'Sdk 槽' }] } }),
199
+ JSON.stringify({ schemaVersion: 1, id: 'module-sdk-y', declare: { owns: [{ service: 'sdk.slot-y' }], consumes: [{ service: 'sdk.slot-y', name: 'Sdk 槽' }] } }),
200
200
  );
201
201
  const modules2 = await tk.loadModules();
202
202
  expect(modules2.modules.map((m) => m.id)).toContain('module-sdk-y');