@tbox.cn/app-toolkit 0.1.0 → 0.2.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.
@@ -0,0 +1,57 @@
1
+ import type { CredentialBindingEcho, CredentialEcho, CredentialsView } from '../dto.js';
2
+ import { maskCredentialValues, readCredentialFile, stemToFilePath } from '../core/credential-format.js';
3
+ import type { ViewSnapshot } from './context.js';
4
+
5
+ /**
6
+ * 凭据状态轴投影(views/credentials;v4.4 D33 独立端点 GET /credentials):
7
+ * 绑定位全量掩码回显——应用状态轴(随配置失效、零求值零 contracts、永不 503)。
8
+ * GET 永无真值(仅 ref/masked);掩码单源 maskCredentialValues;echoForRef 自
9
+ * service-resolutions 迁入共享(求值单体 instances[].credential 已删净——求值路径零凭据 I/O)。
10
+ *
11
+ * 键集 = 文件语义忠实镜像(api.md §0「键域不对称」):
12
+ * app.byInstance = 注册表 ids ∩ 有 ref(无 ref 的注册实例不出现;未注册 ref 跳过不虚报)
13
+ * services[s].byInstance = 节点 instances map 全键(含 '*',无 ref 亦在场 → null)
14
+ * null 语义:credential/byInstance 值 = 「未就绪」(ref 缺席或文件缺失)——缺失诊断归谓词 #1
15
+ * (CREDENTIAL_FILE_MISSING),echo = 状态回显、issues = 诊断定位(P16 分工)。
16
+ */
17
+
18
+ /** 凭据文件掩码回显(ref 非 secret:// 或文件缺席 → undefined——调用方映射 null) */
19
+ export function echoForRef(appDir: string, ref: string | undefined): CredentialEcho | undefined {
20
+ if (ref === undefined || !ref.startsWith('secret://')) return undefined;
21
+ const stem = ref.slice('secret://'.length);
22
+ const file = readCredentialFile(appDir, stem);
23
+ if (!file) return undefined;
24
+ return { type: file.type, ref, file: stemToFilePath(stem), masked: maskCredentialValues(file.values) };
25
+ }
26
+
27
+ export function loadCredentialsView(snapshot: ViewSnapshot): CredentialsView {
28
+ const cfg = snapshot.integrations as {
29
+ credentialRef?: unknown;
30
+ credentialRefByInstance?: Record<string, string>;
31
+ instances?: Array<{ id: string }>;
32
+ services?: Record<string, { credentialRef?: string; instances?: Record<string, { credentialRef?: string }> }>;
33
+ } | null;
34
+
35
+ const app: CredentialBindingEcho = {
36
+ credential: echoForRef(snapshot.appDir, typeof cfg?.credentialRef === 'string' ? cfg.credentialRef : undefined) ?? null,
37
+ byInstance: {},
38
+ };
39
+ for (const { id } of cfg?.instances ?? []) {
40
+ const ref = cfg?.credentialRefByInstance?.[id];
41
+ if (ref === undefined) continue; // 无 ref 的注册实例不出现(api.md §4.9a 实证)
42
+ app.byInstance[id] = echoForRef(snapshot.appDir, ref) ?? null;
43
+ }
44
+
45
+ const services: Record<string, CredentialBindingEcho> = {};
46
+ for (const [s, node] of Object.entries(cfg?.services ?? {})) {
47
+ const slot: CredentialBindingEcho = {
48
+ credential: echoForRef(snapshot.appDir, node.credentialRef) ?? null,
49
+ byInstance: {},
50
+ };
51
+ for (const [k, inst] of Object.entries(node.instances ?? {})) {
52
+ slot.byInstance[k] = echoForRef(snapshot.appDir, inst.credentialRef) ?? null;
53
+ }
54
+ services[s] = slot;
55
+ }
56
+ return { app, services };
57
+ }
@@ -0,0 +1,53 @@
1
+ import type { LayerIntegrationSchemas } from '../dto.js';
2
+ import { isSupplied, type ViewSnapshot } from './context.js';
3
+ import { readSchemaRef } from './providers.js';
4
+ import { moduleDomainWritable, providerFootprint, resolveDomainBinding } from '../integrations/domain-binding.js';
5
+
6
+ /**
7
+ * 层级 config schema 计算视图(v4.4 D32——静态轴富化,零 contracts 求值)。
8
+ * config 表单结构三形态同源(P10):槽级组合单体(providers.ts)/ 层级内嵌(本文件——
9
+ * /app、/modules/:m include)/ 供给轴批量(providers.ts include——切换态)。
10
+ *
11
+ * 计算规则(D32 裁决):禁交集——config 五层浅合并下「部分服务认识的字段」合法(谓词 #7
12
+ * 并集键集同依据);禁合成单一 schema——required/title 冲突的有损代数;产物 = 扁平映射,
13
+ * 分组归客户端纯呈现。层级 schema = 表单引导**非写入门禁**(P16——写路径校验权威恒在谓词
14
+ * #7 键集 + ajv 凭据链;root 级口径差:#7 = 引用厂商并集,表单 = 层生效厂商单厂商作用域)。
15
+ */
16
+
17
+ /** 核心纯函数:显式作用域(provider null → 整字段 null;services 逐项 serviceSchemas 复合键 +
18
+ * readSchemaRef——既有解析路径零新读;供给在场未声明 = null,mock 合法) */
19
+ export function resolveLayerIntegrationSchemas(
20
+ snapshot: ViewSnapshot,
21
+ provider: string | null,
22
+ services: readonly string[],
23
+ ): LayerIntegrationSchemas | null {
24
+ if (provider === null) return null;
25
+ const out: LayerIntegrationSchemas['services'] = {};
26
+ for (const s of services) {
27
+ const entry = snapshot.walk.serviceSchemas[`${provider}\u0000${s}`];
28
+ out[s] = { configSchema: entry ? readSchemaRef(snapshot.appDir, entry.packageDir, entry.configSchema) : null };
29
+ }
30
+ return { provider, services: out };
31
+ }
32
+
33
+ /** app 级 loader:生效厂商 = root.provider(非空字符串,缺席 → 整字段 null——D32「未绑定」);
34
+ * 作用域 = 该厂商供给足迹全量(≠ AppView.services——root.config 流向包括模块服务在内的所有服务) */
35
+ export function loadAppIntegrationSchemas(snapshot: ViewSnapshot): LayerIntegrationSchemas | null {
36
+ const p = (snapshot.integrations as { provider?: unknown } | null)?.provider;
37
+ const provider = typeof p === 'string' && p.length > 0 ? p : null;
38
+ if (provider === null) return null;
39
+ return resolveLayerIntegrationSchemas(snapshot, provider, providerFootprint(snapshot.catalog, provider));
40
+ }
41
+
42
+ /** module 级 loader:仅 writable 单域;生效厂商 = resolveDomainBinding(D32/R4 单源);
43
+ * 作用域 = demand ∩ 该厂商供给;多域/零域/共享键/未绑定 → 整字段 null(表单降级 = ExtraFields) */
44
+ export function loadModuleIntegrationSchemas(snapshot: ViewSnapshot, moduleId: string): LayerIntegrationSchemas | null {
45
+ const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
46
+ if (!mod) return null; // 视图已先行 404,此处防御
47
+ const w = moduleDomainWritable(snapshot.walk, mod);
48
+ if (!w.writable) return null;
49
+ const provider = resolveDomainBinding(snapshot.integrations, w.domainKey).provider;
50
+ if (provider === null) return null;
51
+ const demand = (mod.descriptor.contributes.services ?? []).map((s) => s.service);
52
+ return resolveLayerIntegrationSchemas(snapshot, provider, demand.filter((s) => isSupplied(snapshot.catalog, provider, s)));
53
+ }
@@ -1,10 +1,14 @@
1
- import type { ModuleDeclaration, ModuleDetailView, ServiceDemand } from '../dto.js';
1
+ import type { ModuleDeclaration, ModuleDetailView, ProviderBinding, ServiceDemand } from '../dto.js';
2
2
  import { AppToolkitError } from '../core/errors.js';
3
- import { domainOfService, type ViewSnapshot } from './context.js';
3
+ import { moduleDomainWritable, resolveDomainBinding } from '../integrations/domain-binding.js';
4
+ import type { ViewSnapshot } from './context.js';
4
5
 
5
6
  /**
6
7
  * 需求轴/模块静态投影(views/modules;C4)。静态端点结构性零求值(铁律 #3——
7
8
  * 无 resolution/issues/contracts 键,永不 503)。
9
+ * v4.4 D34:domainKeys/providerBindings/configEditable 显式化;integration 仅单域
10
+ * (writable)时在场——多域/零域/共享键无单键落点;多域回退兜底键机器拆除
11
+ * (派生统一 = domainOfService 去重集合,moduleDomainWritable 单源)。
8
12
  */
9
13
 
10
14
  /** GET /modules:纯模块清单(demand 不在——卡片展开 = GET /modules/:m) */
@@ -22,12 +26,6 @@ export function loadModulesView(snapshot: ViewSnapshot): { modules: ModuleDeclar
22
26
  return { modules };
23
27
  }
24
28
 
25
- /** 域键换算(模块服务集派生域键集;单域 = 该域,多域/零域 = 模块 id——与 write.moduleDomainKey 同约定) */
26
- export function moduleDomainKeyOf(services: ServiceDemand[], moduleId: string): string {
27
- const domainKeys = new Set(services.map((s) => domainOfService(s.service)));
28
- return domainKeys.size === 1 ? [...domainKeys][0] : moduleId;
29
- }
30
-
31
29
  /** GET /modules/:module:模块静态一站式(integration 节点原值 + demand;PUT 读对偶) */
32
30
  export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string): ModuleDetailView {
33
31
  const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
@@ -38,11 +36,15 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
38
36
  service: need.service,
39
37
  ...(need.title ? { title: need.title } : {}),
40
38
  ...(need.description ? { description: need.description } : {}),
41
- required: need.optional !== false,
39
+ required: need.optional === false,
42
40
  }));
41
+ const w = moduleDomainWritable(snapshot.walk, mod);
43
42
  const domains = (snapshot.integrations as { domains?: Record<string, unknown> } | null)?.domains ?? {};
44
- const domainKey = moduleDomainKeyOf(services, moduleId);
45
- // integration = domains[换算键] 节点原值(D21 编辑预填源);未配置 = null
46
- const integration = (domains[domainKey] as ModuleDetailView['integration']) ?? null;
47
- return { integration, services };
43
+ // integration = domains[换算键] 节点原值(D21 编辑预填源);未配置 = null;多域/零域/共享键 = null(D34)
44
+ const integration = w.writable ? ((domains[w.domainKey] as ModuleDetailView['integration']) ?? null) : null;
45
+ const providerBindings: ProviderBinding[] = w.domainKeys.map((d) => {
46
+ const binding = resolveDomainBinding(snapshot.integrations, d);
47
+ return { domain: d, effective: binding.provider, layer: binding.layer };
48
+ });
49
+ return { domainKeys: [...w.domainKeys], integration, providerBindings, configEditable: w.writable, services };
48
50
  }
@@ -2,7 +2,6 @@ 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 { walkManifests } from '../assembly/walk-manifests.js';
6
5
  import type { ViewSnapshot } from './context.js';
7
6
 
8
7
  /**
@@ -27,8 +26,9 @@ export function loadProvidersView(snapshot: ViewSnapshot): ProvidersView {
27
26
  return { providers: providers.sort((a, b) => a.provider.localeCompare(b.provider)) };
28
27
  }
29
28
 
30
- /** GET /providers/:provider:供给足迹(implementations 恒数组——H1 多实现钉版数据源) */
31
- export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string): ProviderDetail {
29
+ /** GET /providers/:provider:供给足迹(implementations 恒数组——H1 多实现钉版数据源;
30
+ * v4.4 F5:include=integrationSchemas 切换态批量——services[s] += { configSchema }(纯 config)) */
31
+ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string, include?: ReadonlySet<string>): ProviderDetail {
32
32
  const impls = snapshot.catalog.providers[provider];
33
33
  if (!impls) {
34
34
  throw new AppToolkitError('PROVIDER_NOT_FOUND', 404, `厂商 ${provider} 不 ∈ catalog(内联声明厂商无包级档案——身份与表单经组合端点暴露)`);
@@ -40,6 +40,14 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string)
40
40
  bucket.implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
41
41
  }
42
42
  }
43
+ if (include?.has('integrationSchemas')) {
44
+ for (const [service, bucket] of Object.entries(services)) {
45
+ const schemaEntry = snapshot.walk.serviceSchemas[`${provider}\u0000${service}`];
46
+ bucket.integrationSchemas = {
47
+ configSchema: schemaEntry ? readSchemaRef(snapshot.appDir, schemaEntry.packageDir, schemaEntry.configSchema) : null,
48
+ };
49
+ }
50
+ }
43
51
  const entries = Object.values(impls);
44
52
  return {
45
53
  provider,
@@ -89,12 +97,12 @@ export function loadProviderServiceDetailView(
89
97
 
90
98
  // integrationSchemas:catalog schema(复合键)优先;内联声明 configSchema 引用应用 config/schemas/
91
99
  // (路径约束 H9——resolve 落 config/schemas/ 内 + realpath 复核,违例 400)
100
+ // v4.4 F5 消重:schema 表取自 snapshot.walk(原 walkManifests(appDir) 重复全量目录扫描);
101
+ // v4.4 D33:credentialSchema 删净(结构归 GET /credential-types——组合单体纯 config)
92
102
  let configSchema: object | null = null;
93
- let credentialSchema: object | null = null;
94
- const schemaEntry = walkManifests(snapshot.appDir).serviceSchemas[`${provider}\u0000${service}`];
103
+ const schemaEntry = snapshot.walk.serviceSchemas[`${provider}\u0000${service}`];
95
104
  if (schemaEntry) {
96
105
  configSchema = readSchemaRef(snapshot.appDir, schemaEntry.packageDir, schemaEntry.configSchema);
97
- credentialSchema = readSchemaRef(snapshot.appDir, schemaEntry.packageDir, schemaEntry.credentialSchema);
98
106
  } else if (inlineDeclaration) {
99
107
  // 内联声明 schema 解析:引用文件在 integrations 节点(provider.configSchema)——
100
108
  // 形状同 config/schemas/ 约束(路径来源 = 节点声明,由服务端读应用侧)
@@ -108,12 +116,13 @@ export function loadProviderServiceDetailView(
108
116
  provider,
109
117
  service,
110
118
  implementations,
111
- integrationSchemas: { configSchema, credentialSchema },
119
+ integrationSchemas: { configSchema },
112
120
  };
113
121
  }
114
122
 
115
- /** catalog schema 引用读取(相对包根;缺席 → null——SCHEMA_FILE_MISSING 归谓词 #6) */
116
- function readSchemaRef(_appDir: string, packageDir: string, ref: string | undefined): object | null {
123
+ /** catalog schema 引用读取(相对包根;缺席 → null——SCHEMA_FILE_MISSING 归谓词 #6)。
124
+ * v4.4 导出共享:层级内嵌(integration-schemas.ts)/ 凭据结构清单(credential-types.ts)同源消费。 */
125
+ export function readSchemaRef(_appDir: string, packageDir: string, ref: string | undefined): object | null {
117
126
  if (!ref) return null;
118
127
  const file = join(packageDir, ref);
119
128
  if (!existsSync(file)) return null;
@@ -1,5 +1,6 @@
1
1
  import type { ServiceDetail } from '../dto.js';
2
2
  import { AppToolkitError } from '../core/errors.js';
3
+ import { isKnownService } from '../integrations/read.js';
3
4
  import { suppliedByVendors, type ViewSnapshot } from './context.js';
4
5
 
5
6
  /**
@@ -10,10 +11,8 @@ import { suppliedByVendors, type ViewSnapshot } from './context.js';
10
11
  export function loadServiceDetailView(snapshot: ViewSnapshot, service: string): ServiceDetail {
11
12
  const configured = (snapshot.integrations as { services?: Record<string, unknown> } | null)?.services ?? {};
12
13
  const demand = snapshot.catalog.services[service];
13
- const inVocabulary = snapshot.walk.vocabulary.includes(service);
14
- const isConfigured = service in configured;
15
- // 404 判据与 /service-resolutions/:service 单源(P9)
16
- if (!inVocabulary && !isConfigured) {
14
+ // 404 判据与写路径单源(P9——A2 isKnownService;词汇成员零 demand/供给/config 仍 200)
15
+ if (!isKnownService(snapshot.walk.vocabulary, Object.keys(configured), service)) {
17
16
  throw new AppToolkitError('SERVICE_NOT_FOUND', 404, `服务 ${service} 不在服务词汇(已装契约包 ∪ 已配置)`);
18
17
  }
19
18
  // integration = 文件 services[s] 逐字原值(编辑预填源 D21);无声明 = null
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  BindingLayer,
3
3
  ContractsResolution,
4
- CredentialEcho,
5
4
  EffectiveBinding,
6
5
  IntegrationIssue,
7
6
  ResolvedInstance,
@@ -11,13 +10,15 @@ import type {
11
10
  ServiceResolutionDetail,
12
11
  ServiceStatus,
13
12
  } from '../dto.js';
14
- import { maskCredentialValues, readCredentialFile, stemToFilePath } from '../core/credential-format.js';
13
+ import { AppToolkitError } from '../core/errors.js';
14
+ import { resolveDomainBinding } from '../integrations/domain-binding.js';
15
15
  import type { ResolvedContracts } from '../core/contracts-resolver.js';
16
16
  import type { WalkCatalog } from '../assembly/walk-manifests.js';
17
17
  import { domainOfService, isSupplied, type ViewSnapshot } from './context.js';
18
18
 
19
19
  /**
20
- * 求值轴投影(views/service-resolutions;C4;FX-2c——B6 effective/credential 填充 + F2 layer 推导)。
20
+ * 求值轴投影(views/service-resolutions;C4;FX-2c——B6 effective 填充 + F2 layer 推导;
21
+ * v4.4——D31 Entry.layer 双缺席语义 + D33 凭据内嵌删净(求值路径零凭据文件 I/O))。
21
22
  *
22
23
  * status(service, instanceId) = contracts.resolveEffectiveService(integrations, service, instanceId,
23
24
  * supplies).status——动态(应用自身 contracts 求值单源)。
@@ -26,9 +27,9 @@ import { domainOfService, isSupplied, type ViewSnapshot } from './context.js';
26
27
  * required(service) = 任一需求方 required(OR 聚合——demandedServices.optional AND 合并的补语义;
27
28
  * 平台/孤儿恒 false)——集成就绪判定单源:pending = Σ (required && status !== 'ok')。
28
29
  * **P14 单一形状(结构保证)**:resolveServiceSummary() 单源函数——/service-resolutions map 值 ≡
29
- * /service-resolutions/:s 的 resolution 摘要基(ServiceResolution extends ServiceResolutionEntry)。
30
- * **实例条目**:effective(合并绑定投影 + layer 推导——contracts EffectiveService 无 layer,F2)
31
- * + credential(掩码回显——GET 永无真值;文件缺席省略,缺失归谓词 #1)。
30
+ * /service-resolutions/:s 的 resolution 摘要基(ServiceResolution extends ServiceResolutionEntry)
31
+ * ≡ /app 内嵌 map 值 ≡ /modules/:m 内嵌 map 值(v4.4 等价面四处)。
32
+ * **实例条目**:effective(合并绑定投影 + layer 推导——contracts EffectiveService 无 layer,F2)。
32
33
  */
33
34
 
34
35
  function toContractsResolution(contracts: ResolvedContracts): ContractsResolution {
@@ -51,8 +52,9 @@ interface InstanceEvaluation {
51
52
 
52
53
  /** 最特异声明层推导(F2):inst 位 provider 在场 → 'instance';槽级 → 'service';域级 → 'module';
53
54
  * root → 'app'。实例位查找复刻 TIER0 语义(精确键优先、'*' 通配兜底——与 resolveEffectiveService
54
- * 的 instances 查找层同轴)。 */
55
- function deriveBindingLayer(snapshot: ViewSnapshot, service: string, instanceId: string): BindingLayer {
55
+ * 的 instances 查找层同轴)。v4.4 F1:域级段消费 resolveDomainBinding 单源;返回扩 null
56
+ * (四层全无 provider 声明 → null——Entry.layer「未配置双缺席」口径,D31)。 */
57
+ function deriveBindingLayer(snapshot: ViewSnapshot, service: string, instanceId: string): BindingLayer | null {
56
58
  const integrations = snapshot.integrations as {
57
59
  provider?: unknown;
58
60
  domains?: Record<string, { provider?: unknown }>;
@@ -63,17 +65,9 @@ function deriveBindingLayer(snapshot: ViewSnapshot, service: string, instanceId:
63
65
  const inst = slot?.instances?.[instanceId] ?? slot?.instances?.['*'];
64
66
  if (inst && inst.provider !== undefined) return 'instance';
65
67
  if (slot?.provider !== undefined) return 'service';
66
- if (integrations?.domains?.[domain]?.provider !== undefined) return 'module';
67
- return 'app';
68
- }
69
-
70
- /** 凭据掩码回显(O4:GET 永无真值——maskCredentialValues 单源;文件缺席省略) */
71
- function echoForRef(appDir: string, ref: string | undefined): CredentialEcho | undefined {
72
- if (ref === undefined || !ref.startsWith('secret://')) return undefined;
73
- const stem = ref.slice('secret://'.length);
74
- const file = readCredentialFile(appDir, stem);
75
- if (!file) return undefined;
76
- return { type: file.type, ref, file: stemToFilePath(stem), masked: maskCredentialValues(file.values) };
68
+ if (resolveDomainBinding(integrations, domain).layer === 'module') return 'module';
69
+ if (typeof integrations?.provider === 'string' && integrations.provider.length > 0) return 'app';
70
+ return null;
77
71
  }
78
72
 
79
73
  /** 求值单实例(resolveEffectiveService 动态求值;ok 态携带完整 effective——layer 推导附) */
@@ -102,7 +96,8 @@ function evaluateInstance(
102
96
  implementation: r.effective.implementation,
103
97
  ...(r.effective.config !== undefined ? { config: { ...(r.effective.config as Record<string, unknown>) } } : {}),
104
98
  ...(r.effective.credentialRef !== undefined ? { credentialRef: r.effective.credentialRef } : {}),
105
- layer: deriveBindingLayer(snapshot, service, instanceId),
99
+ // ok 态可证非空(effective.provider 必有声明来源层)——null 兜底为防御性
100
+ layer: deriveBindingLayer(snapshot, service, instanceId) ?? 'app',
106
101
  },
107
102
  };
108
103
  }
@@ -117,9 +112,11 @@ function evaluationContracts(snapshot: ViewSnapshot) {
117
112
  normalizeInstances?: (input: unknown) => { instances: Array<{ id: string }>; defaultInstanceId: string | null };
118
113
  buildSupplyLookup?: (catalog: unknown) => unknown;
119
114
  };
120
- // 门控(工厂边缘)保证 evaluation/strictValidation 后此断言恒真
115
+ // 门控(工厂边缘)保证 evaluation/strictValidation 后此断言恒真;A2:裸 Error →
116
+ // AppToolkitError 503(predicateIssues 的 config === null 早退绕过工厂 assertFn 后,
117
+ // 本函数是畸形包(严格面在场+求值面缺席)唯一后续拦截点——不再 500)
121
118
  if (!mod.resolveEffectiveService || !mod.normalizeInstances) {
122
- throw new Error('contracts 求值面缺席(工厂门控应先行拦截)');
119
+ throw new AppToolkitError('CONTRACTS_NOT_RESOLVED', 503, 'contracts 求值面缺席(契约包形态异常)——重新安装或升级应用 @tbox.cn/app-contracts 后重试');
123
120
  }
124
121
  return mod as unknown as {
125
122
  resolveEffectiveService: (
@@ -167,6 +164,10 @@ export function resolveServiceSummary(
167
164
  // 跨实例求值异常聚合(statusMessage 直投)
168
165
  const evaluation = evaluateInstance(snapshot, contracts, service, defaultInstanceId, supplies);
169
166
 
167
+ // v4.4 D31:layer = provider 字段来源声明层(默认实例口径,与 status 同轴);
168
+ // 未配置(provider 缺席)→ 双缺席——layer 单独在场即「无 provider 的孤儿层」,违 D31 口径
169
+ const layer = deriveBindingLayer(snapshot, service, defaultInstanceId);
170
+
170
171
  // provider:effective 求值厂商(最特异声明层——跨层事实);缺席 = 未配置/未解析
171
172
  const declaredProvider = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
172
173
  ?.services?.[service]?.provider;
@@ -193,6 +194,7 @@ export function resolveServiceSummary(
193
194
  status: evaluation.status,
194
195
  ...(evaluation.statusMessage ? { statusMessage: evaluation.statusMessage } : {}),
195
196
  ...(provider !== undefined ? { provider } : {}),
197
+ ...(provider !== undefined && layer !== null ? { layer } : {}),
196
198
  ...(supplied !== undefined ? { supplied } : {}),
197
199
  ...(module !== undefined ? { module } : {}),
198
200
  ...(required !== undefined ? { required } : {}),
@@ -255,13 +257,12 @@ export function loadServiceResolutionDetail(
255
257
 
256
258
  const instances: ResolvedInstance[] = instanceIds.map((id) => {
257
259
  const evalResult = evaluateInstance(snapshot, contracts, service, id, supplies);
258
- const echo = echoForRef(snapshot.appDir, evalResult.effective?.credentialRef);
260
+ // v4.4 D33:instances[].credential 删净(凭据掩码回显归 GET /credentials——求值路径零凭据文件 I/O)
259
261
  return {
260
262
  instanceId: id,
261
263
  status: evalResult.status,
262
264
  ...(evalResult.statusMessage ? { statusMessage: evalResult.statusMessage } : {}),
263
265
  ...(evalResult.effective ? { effective: evalResult.effective } : {}),
264
- ...(echo ? { credential: echo } : {}),
265
266
  };
266
267
  });
267
268
 
@@ -303,9 +304,14 @@ function buildInheritance(
303
304
  });
304
305
  const defaultInstanceId = normalized.defaultInstanceId ?? '*';
305
306
  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);
306
309
  return [
307
310
  { layer: 'app' as const, ...(integrations?.provider ? { provider: integrations.provider } : {}) },
308
- { layer: 'module' as const, ...(integrations?.domains?.[domain]?.provider ? { provider: integrations.domains[domain].provider } : {}) },
311
+ {
312
+ layer: 'module' as const,
313
+ ...(domainBinding.layer === 'module' && domainBinding.provider ? { provider: domainBinding.provider } : {}),
314
+ },
309
315
  {
310
316
  layer: 'service' as const,
311
317
  ...(typeof slot?.provider === 'string' ? { provider: slot.provider } : {}),
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { createAppToolkit, type AppToolkit } from '../src/factory.js';
4
+ import { invalidateContractsResolver } from '../src/core/contracts-resolver.js';
5
+ import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
6
+
7
+ /**
8
+ * v4.4 D33 凭据域视图矩阵(F6——C1-C5):键集三断言(app.byInstance = 注册表 ids ∩ 有 ref /
9
+ * services = 已配置 / services.byInstance = 节点 instances map 全键含 '*')/ null 语义
10
+ * (未就绪——诊断归 issues #1)/ 零真值(masked 形态)/ credential-types 清单 + ?provider= 过滤 /
11
+ * 删净断言(instances[].credential、组合单体 credentialSchema——序列化面不信 typecheck)。
12
+ */
13
+
14
+ const OK_RESOLVE = [
15
+ 'const slot = integrations && integrations.services && integrations.services[service];',
16
+ 'if (!slot) return { status: "service-not-configured" };',
17
+ 'const prov = typeof slot.provider === "string" ? slot.provider : slot.provider && slot.provider.provider;',
18
+ 'if (!prov) return { status: "service-not-configured" };',
19
+ 'return { status: "ok", effective: { provider: prov, implementation: slot.implementation || "impl@1", config: slot.config || {}, ...(slot.credentialRef ? { credentialRef: slot.credentialRef } : {}) } };',
20
+ ].join(' ');
21
+
22
+ function credHarness(): { app: TempApp; tk: AppToolkit } {
23
+ const app = createTempApp('tbox-cred-');
24
+ invalidateContractsResolver();
25
+ const store = join(app.appDir, 'contracts');
26
+ writeFakeContractsPackage(store, { version: '0.9.0', resolveBody: OK_RESOLVE });
27
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
28
+ // 供给 manifest(wanda 两槽共享 type + schema 文件;mock '-' 槽)——types 清单与组合单体删净断言用
29
+ writeAppFile(
30
+ app.appDir,
31
+ 'packages/provider-w/tbox.module.json',
32
+ JSON.stringify({
33
+ schemaVersion: 1,
34
+ name: 'provider-w',
35
+ version: '0.1.0',
36
+ kind: 'business',
37
+ contributes: {
38
+ providers: {
39
+ slots: [
40
+ { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c-signed-v1', credentialSchema: 'schemas/cred.json' },
41
+ { service: 'mall.info', provider: 'mock', implementation: 'mock-mall@1', credentialType: '-' },
42
+ ],
43
+ },
44
+ },
45
+ dependencies: { modules: [] },
46
+ env: [],
47
+ }),
48
+ );
49
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/cred.json', JSON.stringify({ type: 'object', properties: { appKey: { type: 'string' } } }));
50
+ writeAppFile(
51
+ app.appDir,
52
+ 'config/integrations.json',
53
+ JSON.stringify({
54
+ provider: 'wanda',
55
+ credentialRef: 'secret://wanda-global',
56
+ instances: [
57
+ { id: 'mall-bj-01', name: '北京店' },
58
+ { id: 'mall-sh-01', name: '上海店' },
59
+ ],
60
+ credentialRefByInstance: { 'mall-sh-01': 'secret://wanda-mall-sh-01-global', 'ghost-id': 'secret://wanda-ghost' },
61
+ services: {
62
+ 'parking.payment': {
63
+ provider: 'wanda',
64
+ implementation: 'wanda-parking@1',
65
+ credentialRef: 'secret://wanda-parking.payment',
66
+ instances: { 'mall-bj-01': { credentialRef: 'secret://wanda-mall-bj-01-parking' }, '*': {} },
67
+ },
68
+ },
69
+ }),
70
+ );
71
+ writeAppFile(app.appDir, 'config/credentials/wanda-global.json', JSON.stringify({ type: 'wanda-c-signed-v1', appKey: 'KEY-GLOBAL', salt: 'SALT-GLOBAL-VALUE' }));
72
+ writeAppFile(app.appDir, 'config/credentials/wanda-mall-sh-01-global.json', JSON.stringify({ type: 'wanda-c-signed-v1', appKey: 'SH' }));
73
+ writeAppFile(app.appDir, 'config/credentials/wanda-parking.payment.json', JSON.stringify({ type: 'wanda-c-signed-v1', appKey: 'SVC-KEY' }));
74
+ // wanda-mall-bj-01-parking.json 故意缺席(文件缺失 → null——诊断归 issues #1)
75
+ return { app, tk: createAppToolkit(app.appDir) };
76
+ }
77
+
78
+ describe('v4.4 D33 凭据域(F6)', () => {
79
+ it('C1 键集三断言:app.byInstance = 注册表 ∩ 有 ref;services = 已配置;byInstance = 节点全键含 *', async () => {
80
+ const { app, tk } = credHarness();
81
+ const view = await tk.loadCredentials();
82
+ // app 本位 = wanda-global echo
83
+ expect(view.app.credential).toEqual({
84
+ type: 'wanda-c-signed-v1',
85
+ ref: 'secret://wanda-global',
86
+ file: 'config/credentials/wanda-global.json',
87
+ masked: { appKey: 'KE****', salt: 'SA****' },
88
+ });
89
+ // app.byInstance:mall-bj-01(无 ref)不出现;ghost-id(未注册 ref)跳过不虚报;mall-sh-01 在场
90
+ expect(Object.keys(view.app.byInstance)).toEqual(['mall-sh-01']);
91
+ expect(view.app.byInstance['mall-sh-01']?.ref).toBe('secret://wanda-mall-sh-01-global');
92
+ // services 键集 = 已配置服务;byInstance 键集 = 节点 instances map 全键(含 '*')
93
+ expect(Object.keys(view.services)).toEqual(['parking.payment']);
94
+ expect(Object.keys(view.services['parking.payment']!.byInstance).sort()).toEqual(['*', 'mall-bj-01']);
95
+ app.dispose();
96
+ });
97
+
98
+ it('C2 null 语义:ref 缺席/文件缺失 → null(未就绪——诊断归 issues #1 分工)', async () => {
99
+ const { app, tk } = credHarness();
100
+ const view = await tk.loadCredentials();
101
+ expect(view.services['parking.payment']!.credential).toEqual({
102
+ type: 'wanda-c-signed-v1',
103
+ ref: 'secret://wanda-parking.payment',
104
+ file: 'config/credentials/wanda-parking.payment.json',
105
+ masked: { appKey: 'SV****' },
106
+ });
107
+ expect(view.services['parking.payment']!.byInstance['*']).toBeNull(); // 无 ref → null
108
+ expect(view.services['parking.payment']!.byInstance['mall-bj-01']).toBeNull(); // ref 在场文件缺失 → null
109
+ app.dispose();
110
+ });
111
+
112
+ it('C3 零真值:序列化无明文;≤4 字符全掩码(掩码单源)', async () => {
113
+ const { app, tk } = credHarness();
114
+ const view = await tk.loadCredentials();
115
+ const serialized = JSON.stringify(view);
116
+ expect(serialized).not.toContain('KEY-GLOBAL');
117
+ expect(serialized).not.toContain('SALT-GLOBAL-VALUE');
118
+ expect(serialized).not.toContain('SVC-KEY');
119
+ expect(view.app.byInstance['mall-sh-01']?.masked.appKey).toBe('****'); // ≤4 字符全掩码
120
+ app.dispose();
121
+ });
122
+
123
+ it('C4 credential-types:全量清单 + ?provider= 过滤(未知 → 空不 404)+ schema 原文', async () => {
124
+ const { app, tk } = credHarness();
125
+ const all = await tk.loadCredentialTypes();
126
+ expect(all.types.map((t) => t.credentialType)).toEqual(['-', 'wanda-c-signed-v1']);
127
+ const wanda = await tk.loadCredentialTypes('wanda');
128
+ expect(wanda.types).toHaveLength(1);
129
+ expect(wanda.types[0]).toEqual({
130
+ credentialType: 'wanda-c-signed-v1',
131
+ owner: expect.any(String),
132
+ credentialSchema: { type: 'object', properties: { appKey: { type: 'string' } } },
133
+ });
134
+ expect((await tk.loadCredentialTypes('mock')).types.map((t) => t.credentialType)).toEqual(['-']);
135
+ expect((await tk.loadCredentialTypes('ghost')).types).toEqual([]);
136
+ app.dispose();
137
+ });
138
+
139
+ it('C5 删净断言:单体 instances[].credential 与组合单体 credentialSchema 全链不在场', async () => {
140
+ const { app, tk } = credHarness();
141
+ const detail = await tk.loadServiceResolution('parking.payment');
142
+ const inst = detail.resolution.instances.find((i) => i.instanceId === 'mall-bj-01');
143
+ // 假 resolve 仅读槽级 ref(简化级联)——effective.credentialRef = 槽级 secret://wanda-parking.payment
144
+ expect(inst?.effective?.credentialRef).toBe('secret://wanda-parking.payment');
145
+ expect(inst && 'credential' in inst).toBe(false);
146
+ expect('credential' in JSON.parse(JSON.stringify(detail.resolution))).toBe(false);
147
+ const combo = await tk.loadProviderService('wanda', 'parking.payment');
148
+ expect('credentialSchema' in combo.integrationSchemas).toBe(false);
149
+ expect(combo.implementations[0]?.credentialType).toBe('wanda-c-signed-v1'); // 寻址键保留
150
+ app.dispose();
151
+ });
152
+ });
package/tests/demo-app.ts CHANGED
@@ -48,15 +48,24 @@ export interface FakeContractsOptions {
48
48
  /** resolveEffectiveService 自定义函数体(FX-2c O4 fixture:默认恒 service-not-configured;
49
49
  * 入参 (integrations, service, instanceId, supplies),需 return EffectiveServiceResolution 形状) */
50
50
  resolveBody?: string;
51
+ /** service-slots.json 词汇(P9 身份判据/求值键集;缺省不物化 = 词汇空) */
52
+ slots?: string[];
53
+ /** zod 形拒绝 fixture(A1 T9/T10):parseIntegrationsConfig 恒抛带 issues 的结构化错误
54
+ * (结构性 ZodError 形——issues[].path 供 fields 断言;仅在专属 fixture 上使用) */
55
+ zodRejectPaths?: string[][];
51
56
  }
52
57
 
53
58
  /** 假 contracts 包:package.json exports 形态逐字段复刻发布面(types+import 双条件、无 require/default)
54
59
  * ——resolver 不经 exports 解析(包根直探),该形态为 E4 类错误的机械防线。 */
55
60
  export function writeFakeContractsPackage(packageRoot: string, opts: FakeContractsOptions = {}): void {
56
- const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody } = opts;
61
+ const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, slots, zodRejectPaths } = opts;
57
62
  const strictBody = withStrict
58
63
  ? `export function parseIntegrationsConfig(raw) { ${
59
64
  rejectMarker ? `if (JSON.stringify(raw).includes(${JSON.stringify(rejectMarker)})) throw new Error("fake strict reject"); ` : ''
65
+ }${
66
+ zodRejectPaths
67
+ ? `throw Object.assign(new Error("fake zod reject"), { issues: ${JSON.stringify(zodRejectPaths.map((p) => ({ path: p })))} }); `
68
+ : ''
60
69
  }return raw; }`
61
70
  : '';
62
71
  const resolveLine = `export function resolveEffectiveService(integrations, service, instanceId, supplies) { ${
@@ -104,6 +113,10 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
104
113
  mkdirSync(join(packageRoot, 'src'), { recursive: true });
105
114
  writeFileSync(join(packageRoot, 'src', 'runtime.ts'), exportsBody);
106
115
  }
116
+ // service-slots.json(P9 词汇源——探测链发现位 = 包根;缺省不物化 = 词汇空)
117
+ if (slots && slots.length > 0) {
118
+ writeFileSync(join(packageRoot, 'service-slots.json'), `${JSON.stringify({ slots }, null, 2)}\n`);
119
+ }
107
120
  }
108
121
 
109
122
  export interface LinkOptions {
@@ -6,7 +6,7 @@ import { describe, it, expect } from 'vitest';
6
6
  * (方法名断言依赖 factory 导出——届时 import { createAppToolkit } 逐行断言 typeof)。
7
7
  */
8
8
 
9
- /** 13 行映射表(api.md §8 单源;C4 补全为逐行断言) */
9
+ /** 15 行映射表(api.md §8 单源——v4.4 +GET /credentials、+GET /credential-types) */
10
10
  export const URL_METHOD_MAP: ReadonlyArray<{ method: string; url: string }> = [
11
11
  // ── 静态(7)──
12
12
  { method: 'loadModules', url: 'GET /modules' },
@@ -19,6 +19,9 @@ export const URL_METHOD_MAP: ReadonlyArray<{ method: string; url: string }> = [
19
19
  // ── 求值(2)──
20
20
  { method: 'loadServiceResolutions', url: 'GET /service-resolutions' },
21
21
  { method: 'loadServiceResolution', url: 'GET /service-resolutions/:service' },
22
+ // ── 凭据域(2;v4.4 D33)──
23
+ { method: 'loadCredentials', url: 'GET /credentials' },
24
+ { method: 'loadCredentialTypes', url: 'GET /credential-types' },
22
25
  // ── 写(4)──
23
26
  { method: 'writeAppIntegration', url: 'PUT /app/integration' },
24
27
  { method: 'writeModuleIntegration', url: 'PUT /modules/:module/integration' },
@@ -26,10 +29,10 @@ export const URL_METHOD_MAP: ReadonlyArray<{ method: string; url: string }> = [
26
29
  { method: 'deleteServiceIntegration', url: 'DELETE /services/:service/integration' },
27
30
  ];
28
31
 
29
- describe('naming-alignment(L1 映射表——C2 骨架)', () => {
30
- it('映射表骨架 = 13 行(静态 7 + 求值 2 + 写 4)——计数守卫', () => {
31
- expect(URL_METHOD_MAP).toHaveLength(13);
32
- expect(URL_METHOD_MAP.filter((r) => r.url.startsWith('GET '))).toHaveLength(9);
32
+ describe('naming-alignment(L1 映射表)', () => {
33
+ it('映射表 = 15 行(静态 7 + 求值 2 + 凭据域 2 + 写 4)——计数守卫(v4.4)', () => {
34
+ expect(URL_METHOD_MAP).toHaveLength(15);
35
+ expect(URL_METHOD_MAP.filter((r) => r.url.startsWith('GET '))).toHaveLength(11);
33
36
  expect(URL_METHOD_MAP.filter((r) => r.url.startsWith('PUT '))).toHaveLength(3);
34
37
  expect(URL_METHOD_MAP.filter((r) => r.url.startsWith('DELETE '))).toHaveLength(1);
35
38
  });