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