@tbox.cn/app-toolkit 0.5.0 → 0.7.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/README.md +2 -2
- package/dist/index.d.ts +101 -20
- package/dist/index.js +9 -9
- package/package.json +1 -1
- package/src/assembly/walk-manifests.ts +28 -12
- package/src/core/errors.ts +12 -2
- package/src/core/module-schema.ts +3 -0
- package/src/dto.ts +41 -5
- package/src/factory.ts +48 -28
- package/src/index.ts +4 -2
- package/src/integrations/write.ts +4 -0
- package/src/views/context.ts +4 -2
- package/src/views/integration-schemas.ts +17 -3
- package/src/views/modules.ts +66 -4
- package/src/views/service-resolutions.ts +113 -25
- package/tests/dev-manifest-catalog.test.ts +39 -1
- package/tests/module-resources.test.ts +148 -0
- package/tests/module-schema.test.ts +14 -0
- package/tests/views.test.ts +437 -120
- package/tests/write-core.test.ts +34 -0
package/src/factory.ts
CHANGED
|
@@ -18,7 +18,13 @@ import { loadViewSnapshotSync } from './views/context.js';
|
|
|
18
18
|
import { loadModulesView, loadModuleDetailView } from './views/modules.js';
|
|
19
19
|
import { loadAppView } from './views/app.js';
|
|
20
20
|
import { loadServiceDetailView } from './views/service-detail.js';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
loadServiceResolutionsView,
|
|
23
|
+
loadServiceResolutionDetail,
|
|
24
|
+
loadServiceResolutionsDegradeView,
|
|
25
|
+
loadServiceResolutionDegradeDetail,
|
|
26
|
+
contractsEvaluable,
|
|
27
|
+
} from './views/service-resolutions.js';
|
|
22
28
|
import { loadAppFormViews, loadModuleFormViews, loadServiceIntegrationSchemas, loadServiceFieldRegistry } from './views/integration-schemas.js';
|
|
23
29
|
import { loadCredentialsView } from './views/credentials.js';
|
|
24
30
|
import { loadCredentialTypesView } from './views/credential-types.js';
|
|
@@ -45,16 +51,19 @@ import type {
|
|
|
45
51
|
* createAppToolkit(C4 工厂;v4.4——24 方法 = 11 读(含 credentials/credential-types 双端点)
|
|
46
52
|
* + 4 写 + 9 迁移面;loadApp/loadModule/loadProvider 增 include 选项;v4.6——loadApp/loadModule
|
|
47
53
|
* += 轴向假想绑定选项(appProvider/moduleProvider,D37);表单双投影经 formViews 单点装配;
|
|
48
|
-
* v4.7——loadService 增 include 选项(服务级生效绑定 schema
|
|
54
|
+
* v4.7——loadService 增 include 选项(服务级生效绑定 schema 单体);v5.1 D39——loadService
|
|
55
|
+
* += serviceProvider 轴参(服务轴向假想绑定,三轴收口))。
|
|
49
56
|
* AppToolkit = AppToolkitRead & AppToolkitWrite & AppToolkitInternals——
|
|
50
57
|
* Read+Write = HTTP 直通面 additive 冻结(L1 端点↔方法 1:1,L3 命名镜像:Integration=节点级 /
|
|
51
58
|
* Config=文件级);Internals = CLI/doctor/桥消费(自由演进)。
|
|
52
59
|
*
|
|
53
|
-
* **门控断言单点(policy at edge
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
60
|
+
* **门控断言单点(policy at edge——v7 读求值轴退役后范围 = 写 + 迁移面)**:写(4)+
|
|
61
|
+
* ensureMockBindings / writeIntegrationsConfig / evaluateIntegrationServices 入口
|
|
62
|
+
* assertContractsUsable——strictValidation === false → 503 CONTRACTS_NOT_RESOLVED;静态方法不门控
|
|
63
|
+
* (永不 503——铁律 #3);**读求值轴(loadServiceResolutions/loadServiceResolution 及 include 装配)
|
|
64
|
+
* v7 起不再 503**——contracts 不可用 → 200 降级骨架(evaluation-unavailable,doctor 分级/D31 先例
|
|
65
|
+
* 对齐;依据链与裁决见 Agent Note evaluation-axis-degrade);**include 装配为条件分支非门控**
|
|
66
|
+
* (D31 null 分支退役——三条件门 + 内嵌轴同步降级,四面统一骨架);integrations/ views/ 机制层保持纯函数。
|
|
58
67
|
*
|
|
59
68
|
* 并发:进程内写串行(per-factory promise chain)+ 原子写 + 写后本实例缓存即刻失效(D26)。
|
|
60
69
|
* onApplied(D7):toolkit 判定「需要重启」(实际落盘且非 dryRun/no-diff)→ 调用一次 +
|
|
@@ -98,9 +107,10 @@ function validateInclude(kind: keyof typeof INCLUDE_WHITELIST, include?: string[
|
|
|
98
107
|
const FORM_INCLUDE_KEYS = ['integrationSchemas', 'integrationFieldRegistry'] as const;
|
|
99
108
|
|
|
100
109
|
/** v4.6 D37 轴参严检(400 先于 404——镜像输入校验先行):非空轴参需与表单 include 键同现;
|
|
101
|
-
* 缺席/空串 ≡ 按实际配置解析(I3——不触发严检);错轴参数由壳层按名读取,天然静默忽略。
|
|
110
|
+
* 缺席/空串 ≡ 按实际配置解析(I3——不触发严检);错轴参数由壳层按名读取,天然静默忽略。
|
|
111
|
+
* v5.1 D39:联合类型 += serviceProvider(service 轴同律)。 */
|
|
102
112
|
function assertAxisProviderUsable(
|
|
103
|
-
paramName: 'appProvider' | 'moduleProvider',
|
|
113
|
+
paramName: 'appProvider' | 'moduleProvider' | 'serviceProvider',
|
|
104
114
|
value: string | undefined,
|
|
105
115
|
include: ReadonlySet<string>,
|
|
106
116
|
): void {
|
|
@@ -119,13 +129,16 @@ function assertAxisProviderUsable(
|
|
|
119
129
|
* **双键契约(v7 词汇统一)**:资源路径参数双键寻址(id 优先、唯一展示名兜底——resolve-key
|
|
120
130
|
* 单点;多命中 404 带候选);query 过滤参数(loadServiceResolutions services?)恒 id-only。 */
|
|
121
131
|
export interface AppToolkitRead {
|
|
122
|
-
|
|
132
|
+
/** GET /modules:hidden 模块默认过滤(includeHidden = true 可见——对齐 ?dryRun 布尔参数先例) */
|
|
133
|
+
loadModules(o?: { includeHidden?: boolean }): Promise<ModulesView>;
|
|
123
134
|
/** v4.6 D37:moduleProvider = 轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include 键同现)。
|
|
124
135
|
* moduleId = 双键(id 优先、唯一展示名兜底)。 */
|
|
125
136
|
loadModule(moduleId: string, o?: { include?: string[]; moduleProvider?: string }): Promise<ModuleDetailView>;
|
|
126
137
|
/** v4.6 D37:appProvider = 轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include 键同现) */
|
|
127
138
|
loadApp(o?: { include?: string[]; appProvider?: string }): Promise<AppView>;
|
|
128
|
-
|
|
139
|
+
/** v5.1 D39:serviceProvider = 服务轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include
|
|
140
|
+
* 键同现)——假想重定向仅表单双投影,integration/suppliedBy/defaults 恒实际态(I2) */
|
|
141
|
+
loadService(service: string, o?: { include?: string[]; serviceProvider?: string }): Promise<ServiceDetail>;
|
|
129
142
|
loadServiceResolutions(services?: string[]): Promise<ServiceResolutionsView>;
|
|
130
143
|
loadServiceResolution(service: string): Promise<ServiceResolutionDetail>;
|
|
131
144
|
loadProviders(): Promise<ProvidersView>;
|
|
@@ -188,11 +201,8 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
|
|
|
188
201
|
throw new AppToolkitError('CONTRACTS_NOT_RESOLVED', 503, contractsNotResolvedMessage(contracts));
|
|
189
202
|
}
|
|
190
203
|
if (!contracts.ownershipBindingV6) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
503,
|
|
194
|
-
`contracts ${contracts.version ?? '(版本未知)'} 不支持 v6 modules 绑定形状(OWNERSHIP_BINDING_V6 标记缺席)——升级应用 @tbox.cn/app-contracts 依赖后可用`,
|
|
195
|
-
);
|
|
204
|
+
// v6 标记门第四分支(文案单源 = contractsNotResolvedMessage——降级轴 statusMessage 同源)
|
|
205
|
+
throw new AppToolkitError('CONTRACTS_NOT_RESOLVED', 503, contractsNotResolvedMessage(contracts));
|
|
196
206
|
}
|
|
197
207
|
}
|
|
198
208
|
|
|
@@ -242,9 +252,9 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
|
|
|
242
252
|
}
|
|
243
253
|
|
|
244
254
|
const read = {
|
|
245
|
-
async loadModules() {
|
|
255
|
+
async loadModules(o?: { includeHidden?: boolean }) {
|
|
246
256
|
const contracts = await resolveContractsForApp(appDir);
|
|
247
|
-
return loadModulesView(snapshot(contracts));
|
|
257
|
+
return loadModulesView(snapshot(contracts), o);
|
|
248
258
|
},
|
|
249
259
|
// v4.4 D31:门控与内嵌分离——静态方法零门控分支;include=serviceResolutions 按
|
|
250
260
|
// evaluation && strictValidation 双条件装配(<0.9/形态异常 → null,永不 503——技术方案 §2.4:
|
|
@@ -258,10 +268,12 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
|
|
|
258
268
|
const mod = resolveModuleKey(snap.walk, moduleId);
|
|
259
269
|
const view = loadModuleDetailView(snap, mod.id);
|
|
260
270
|
if (include.has('serviceResolutions')) {
|
|
261
|
-
|
|
271
|
+
// v7:三条件门 + 内嵌轴同步降级(P14 (b) 四面统一骨架——D31 null 分支退役:
|
|
272
|
+
// include 请求了即恒返回视图;老消费方 null 分支保留为兼容死码)
|
|
273
|
+
view.serviceResolutions = contractsEvaluable(contracts)
|
|
262
274
|
? await predicateIssues().then((issues) =>
|
|
263
275
|
loadServiceResolutionsView(snap, issues, view.services.map((s) => s.service)))
|
|
264
|
-
:
|
|
276
|
+
: loadServiceResolutionsDegradeView(snap, contracts, view.services.map((s) => s.service)); // module = demand 服务过滤(≡ 批量 ?service= 语义)
|
|
265
277
|
}
|
|
266
278
|
if (include.has('integrationSchemas') || include.has('integrationFieldRegistry')) {
|
|
267
279
|
// v4.6 D37:表单层双投影同源一次计算(双取单算 + I1——假想重定向天然同步)
|
|
@@ -278,9 +290,10 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
|
|
|
278
290
|
const snap = snapshot(contracts);
|
|
279
291
|
const view = loadAppView(snap);
|
|
280
292
|
if (include.has('serviceResolutions')) {
|
|
281
|
-
|
|
293
|
+
// v7:内嵌轴同步降级(同 loadModule——D31 null 退役,app = 全量键集)
|
|
294
|
+
view.serviceResolutions = contractsEvaluable(contracts)
|
|
282
295
|
? await predicateIssues().then((issues) => loadServiceResolutionsView(snap, issues)) // app = 全量键集
|
|
283
|
-
:
|
|
296
|
+
: loadServiceResolutionsDegradeView(snap, contracts);
|
|
284
297
|
}
|
|
285
298
|
if (include.has('integrationSchemas') || include.has('integrationFieldRegistry')) {
|
|
286
299
|
const form = loadAppFormViews(snap, o?.appProvider); // 双取单算 + I1(同 loadModule)
|
|
@@ -291,32 +304,39 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
|
|
|
291
304
|
},
|
|
292
305
|
// v4.7 D38:include 镜像 loadModule 三段式——① 输入校验 400 先于 404 ② 视图 ③ 静态富化(永不 503)
|
|
293
306
|
// G 批:service 轴 += integrationFieldRegistry(loadServiceFieldRegistry——D38 四分支链复用)
|
|
294
|
-
|
|
307
|
+
// v5.1 D39:loadService += serviceProvider 轴参(三段式序对齐 loadModule——①' 严检先于 resolveContracts,
|
|
308
|
+
// 即先于 404 也先于 503;I1 双投影同源一次计算经单源链透传)
|
|
309
|
+
async loadService(service: string, o?: { include?: string[]; serviceProvider?: string }): Promise<ServiceDetail> {
|
|
295
310
|
const include = validateInclude('service', o?.include);
|
|
311
|
+
assertAxisProviderUsable('serviceProvider', o?.serviceProvider, include); // ①' D39 轴参严检(先于 404)
|
|
296
312
|
const contracts = await resolveContractsForApp(appDir);
|
|
297
313
|
const snap = snapshot(contracts); // 单快照——detail 与 include 装配共享
|
|
298
314
|
// 双键寻址(v7):id 优先、唯一展示名兜底 → 真实 service id(404 面)
|
|
299
315
|
const serviceId = resolveServiceKey(snap.walk, configuredServiceKeys(snap), service);
|
|
300
316
|
const view = loadServiceDetailView(snap, serviceId);
|
|
301
317
|
if (include.has('integrationSchemas')) {
|
|
302
|
-
view.integrationSchemas = loadServiceIntegrationSchemas(snap, serviceId);
|
|
318
|
+
view.integrationSchemas = loadServiceIntegrationSchemas(snap, serviceId, o?.serviceProvider);
|
|
303
319
|
}
|
|
304
320
|
if (include.has('integrationFieldRegistry')) {
|
|
305
|
-
view.integrationFieldRegistry = loadServiceFieldRegistry(snap, serviceId);
|
|
321
|
+
view.integrationFieldRegistry = loadServiceFieldRegistry(snap, serviceId, o?.serviceProvider);
|
|
306
322
|
}
|
|
307
323
|
return view;
|
|
308
324
|
},
|
|
309
325
|
async loadServiceResolutions(services?: string[]) {
|
|
310
326
|
const contracts = await resolveContractsForApp(appDir);
|
|
311
|
-
|
|
327
|
+
const snap = snapshot(contracts);
|
|
328
|
+
// v7 读求值轴降级门(policy-at-edge 读轴退役):contracts 不可用 → 200 静态骨架 +
|
|
329
|
+
// evaluation-unavailable(fail-visible 保留在数据内;写路径 503 门控不变)
|
|
330
|
+
if (!contractsEvaluable(contracts)) return loadServiceResolutionsDegradeView(snap, contracts, services);
|
|
312
331
|
const issues = await predicateIssues();
|
|
313
|
-
return loadServiceResolutionsView(
|
|
332
|
+
return loadServiceResolutionsView(snap, issues, services);
|
|
314
333
|
},
|
|
315
334
|
async loadServiceResolution(service: string) {
|
|
316
335
|
const contracts = await resolveContractsForApp(appDir);
|
|
317
|
-
assertContractsUsable(contracts);
|
|
318
336
|
const snap = snapshot(contracts);
|
|
319
337
|
const serviceId = resolveServiceKey(snap.walk, configuredServiceKeys(snap), service);
|
|
338
|
+
// v7 降级门同批量端点(三条件与;降级单体携带声明实例枚举 + inheritance 静态投影)
|
|
339
|
+
if (!contractsEvaluable(contracts)) return loadServiceResolutionDegradeDetail(serviceId, snap, contracts);
|
|
320
340
|
const issues = await predicateIssues();
|
|
321
341
|
return loadServiceResolutionDetail(serviceId, snap, issues);
|
|
322
342
|
},
|
package/src/index.ts
CHANGED
|
@@ -64,10 +64,10 @@ export { pruneIntegrationBindings } from './integrations/prune-bindings.js';
|
|
|
64
64
|
export type { PruneResult } from './integrations/prune-bindings.js';
|
|
65
65
|
|
|
66
66
|
// ── assembly 层(目录知识——walkManifests 四产物 + catalog 双形态 + 组合单体装配)──
|
|
67
|
-
export { walkManifests } from './assembly/walk-manifests.js';
|
|
67
|
+
export { walkManifests, discoverContractPackages } from './assembly/walk-manifests.js';
|
|
68
68
|
// 双键寻址(v7 词汇统一——factory 七方法入口单点消费的解析 helper;纯参数式零 IO)
|
|
69
69
|
export { resolveModuleKey, resolveServiceKey } from './assembly/resolve-key.js';
|
|
70
|
-
export type { WalkManifestsResult, WalkModuleInfo, WalkCatalog, WalkProviderEntry, DeclarationIssue, SlotMeta } from './assembly/walk-manifests.js';
|
|
70
|
+
export type { WalkManifestsResult, WalkModuleInfo, WalkCatalog, WalkProviderEntry, DeclarationIssue, SlotMeta, DiscoveredSlotSource } from './assembly/walk-manifests.js';
|
|
71
71
|
export { readAppManifest, writeAppManifest, manifestPath } from './assembly/app-manifest.js';
|
|
72
72
|
export type { AppManifest, NpmModuleEntry, ModuleMode } from './assembly/app-manifest.js';
|
|
73
73
|
export { loadProviderCatalog, loadProviderCatalogRaw } from './assembly/provider-catalog.js';
|
|
@@ -129,6 +129,8 @@ export type {
|
|
|
129
129
|
ServiceResolutionDetail,
|
|
130
130
|
AppIntegrationNode,
|
|
131
131
|
ModuleIntegrationNode,
|
|
132
|
+
ModuleResourceEcho,
|
|
133
|
+
ModuleResourcesEcho,
|
|
132
134
|
ServiceIntegrationNode,
|
|
133
135
|
ServiceInstanceNode,
|
|
134
136
|
InlineVendorDeclaration,
|
|
@@ -261,6 +261,10 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
261
261
|
cache?.invalidate([join(appDir, INTEGRATIONS_FILE)]);
|
|
262
262
|
}
|
|
263
263
|
const written = integrationsWritten || credentialsWritten;
|
|
264
|
+
// 孤儿凭据写后对账(api.md §4.13 承诺追平——「覆盖 DELETE 与 PUT 替换两类来源」;三 PUT 写路径
|
|
265
|
+
// finalize 单点收口,未来第四写方法自动继承)。dryRun(written 蕴含 !dryRun)与 no-diff
|
|
266
|
+
// (finalize 前短路 return)天然跳过;applyModule modules 节无凭据键——对账恒零新增,无害捎带。
|
|
267
|
+
if (written) issues.push(...reconcileOrphanCredentials(appDir));
|
|
264
268
|
const restartScheduled = written && onApplied !== undefined;
|
|
265
269
|
if (restartScheduled) onApplied?.();
|
|
266
270
|
const files: string[] = [];
|
package/src/views/context.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface ViewSnapshot {
|
|
|
19
19
|
catalog: WalkCatalog;
|
|
20
20
|
integrations: Record<string, unknown> | null;
|
|
21
21
|
contracts: ResolvedContracts;
|
|
22
|
+
/** 文件读原语透传(可选——resources 台账惰性读等 stat 指纹缓存消费;缺席 = 调用方直读) */
|
|
23
|
+
cache?: FileCache;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** 读取视图输入快照(每方法调用独立——Freshness Contract 单位 = 方法调用) */
|
|
@@ -27,14 +29,14 @@ export async function loadViewSnapshot(ctx: ViewContext): Promise<ViewSnapshot>
|
|
|
27
29
|
const integrations = await readIntegrationsConfig(ctx.appDir, ctx.cache);
|
|
28
30
|
const { resolveContractsForApp } = await import('../core/contracts-resolver.js');
|
|
29
31
|
const contracts = await resolveContractsForApp(ctx.appDir);
|
|
30
|
-
return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts };
|
|
32
|
+
return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts, ...(ctx.cache ? { cache: ctx.cache } : {}) };
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** 同步快照(调用方已持有 contracts 时——避免重复解析) */
|
|
34
36
|
export function loadViewSnapshotSync(ctx: ViewContext, contracts: ResolvedContracts): ViewSnapshot {
|
|
35
37
|
const walk = walkManifests(ctx.appDir, ctx.cache);
|
|
36
38
|
const integrations = readIntegrationsConfig(ctx.appDir, ctx.cache);
|
|
37
|
-
return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts };
|
|
39
|
+
return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts, ...(ctx.cache ? { cache: ctx.cache } : {}) };
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/** 模块占用服务集(v6——demand ∪ supply ∪ owns:归属模型下「平台服务」= 无主服务;
|
|
@@ -229,11 +229,22 @@ function serviceLevelBindingProvider(snapshot: ViewSnapshot, service: string): s
|
|
|
229
229
|
* ③ 受控内联(isControlledInlineProvider 单源)→ 名 = p.provider;catalog 复合键 > p.configSchema ref
|
|
230
230
|
* (H9 逃逸 400 照抛——readInlineSchema 共享路径)
|
|
231
231
|
* ④ 其余(null/数组/原始值/坏对象)→ 整字段 null(≡ 谓词 #5 INLINE_PROVIDER_INVALID 同判——
|
|
232
|
-
* 不放行域级联,防错误 schema 污染坏节点)
|
|
232
|
+
* 不放行域级联,防错误 schema 污染坏节点)
|
|
233
|
+
* v5.1 D39:serviceProvider = 服务轴向假想绑定(缺席/空串 ≡ 按实际解析——I3 归一单点在此头部,
|
|
234
|
+
* registry 侧不重复归一);非空轴参 → 短路 bare 解析(镜像 formProviderOf「输入替换非分支复制」先例:
|
|
235
|
+
* 任一原分支改写 provider 为 X 后实读都进 bare 分支,同构承诺 by construction)。短路在节点读取之前——
|
|
236
|
+
* 豁免链三分支(D39 与 D38 四分支的有意分歧:预览与坏文件解耦):malformed(分支④)不污染假想 /
|
|
237
|
+
* 内联 ref 不读取(假想态无 H9 逃逸 400;catalog 命中的 manifest 声明路径照读,与节点声明的
|
|
238
|
+
* 用户可控路径不同性)/ 全链未绑定时假想仍非 null(服务轴恒可写,无 module 轴零归属对应物)。 */
|
|
233
239
|
export function loadServiceIntegrationSchemas(
|
|
234
240
|
snapshot: ViewSnapshot,
|
|
235
241
|
service: string,
|
|
242
|
+
serviceProvider?: string,
|
|
236
243
|
): ServiceIntegrationSchemas | null {
|
|
244
|
+
const ovr = typeof serviceProvider === 'string' && serviceProvider.length > 0 ? serviceProvider : undefined;
|
|
245
|
+
if (ovr !== undefined) {
|
|
246
|
+
return { provider: ovr, configSchema: resolveServiceConfigSchema(snapshot, ovr, service) };
|
|
247
|
+
}
|
|
237
248
|
const p = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
|
|
238
249
|
?.services?.[service]?.provider;
|
|
239
250
|
if (p === undefined || p === '') {
|
|
@@ -255,12 +266,15 @@ export function loadServiceIntegrationSchemas(
|
|
|
255
266
|
/** G 批:服务轴字段注册表(D38 四分支链复用——消费 loadServiceIntegrationSchemas 单源;
|
|
256
267
|
* 伪层 {provider, services: {[s]: {configSchema}}} → buildIntegrationFieldRegistry——
|
|
257
268
|
* fields = 该 schema 全部字段带 global 旗标;configSchema null → schemaless=[service]
|
|
258
|
-
* 非 null 空集;schemas 整体 null(全链未绑定/坏节点)→ registry null——镜像 D38
|
|
269
|
+
* 非 null 空集;schemas 整体 null(全链未绑定/坏节点)→ registry null——镜像 D38 语义)。
|
|
270
|
+
* v5.1 D39:serviceProvider = 服务轴向假想绑定,纯透传(I1 单源——归一与短路都在链函数头部;
|
|
271
|
+
* 轴参仅重定向表单层投影,global 旗标全量透传语义不变)。 */
|
|
259
272
|
export function loadServiceFieldRegistry(
|
|
260
273
|
snapshot: ViewSnapshot,
|
|
261
274
|
service: string,
|
|
275
|
+
serviceProvider?: string,
|
|
262
276
|
): IntegrationFieldRegistryView | null {
|
|
263
|
-
const schemas = loadServiceIntegrationSchemas(snapshot, service);
|
|
277
|
+
const schemas = loadServiceIntegrationSchemas(snapshot, service, serviceProvider);
|
|
264
278
|
if (schemas === null) return null;
|
|
265
279
|
const layer: LayerIntegrationSchemas = {
|
|
266
280
|
provider: schemas.provider,
|
package/src/views/modules.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ModuleDeclaration, ModuleBinding, ModuleDetailView, ModuleResourceEcho, ModuleResourcesEcho, ServiceDemand } from '../dto.js';
|
|
2
4
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
5
|
import { moduleOwnershipWritable, resolveModuleBinding } from '../integrations/module-binding.js';
|
|
4
6
|
import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
@@ -8,11 +10,62 @@ import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
|
8
10
|
* 无 resolution/issues/contracts 键,永不 503)。
|
|
9
11
|
* v6 所有权模型:ownedServices(declare.owns 展平)/ binding 单数(resolveModuleBinding)/
|
|
10
12
|
* configEditable = owns>0(moduleOwnershipWritable 单源);integration = modules[install id]
|
|
11
|
-
* 集成位投影(provider/config;resources
|
|
13
|
+
* 集成位投影(provider/config;resources 走独立只读字段,见投影 resourcesOf——N2 读半边)。
|
|
14
|
+
* v5 domainKeys/providerBindings 退役。
|
|
12
15
|
* 2026-09-16 双缺陷批:loadModulesView 排除 provider 家族包(供给/实施载体非可配置
|
|
13
16
|
* 业务模块)——walk 声明集与 loadModuleDetailView 不变(装包仍合法,详情保持 200)。
|
|
14
17
|
*/
|
|
15
18
|
|
|
19
|
+
/** knowledge.json 台账路径(与 toolkit readIntegrationsConfig 同源锚定 appDir/config/——
|
|
20
|
+
* TBOX_INTEGRATIONS_FILE 为 SDK 运行时逃生舱,视图不跟随,既有分叉见 dto ModuleResourceEcho 注) */
|
|
21
|
+
const KNOWLEDGE_REGISTRY_FILE = join('config', 'knowledge.json');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* modules[install id].resources 只读投影(N2 读半边;文件镜像 L1 + ref 台账展开态):
|
|
25
|
+
* - 无模块键/无资源节 → null(DTO null 语义:无资源 ≠ 空表);
|
|
26
|
+
* - `datasetId` 直填 → resolvedDatasetId = 该值(镜像 SDK resolveKnowledge:并存时 datasetId 优先);
|
|
27
|
+
* - `ref` → 同目录 knowledge.json 台账命中 = 台账 datasetId;未命中/台账缺失/解析失败 = null
|
|
28
|
+
* (镜像 SDK resolveKnowledge miss-warn 降级语义,资源缺席走模块占位回退);
|
|
29
|
+
* - 非 knowledge 型资源(未来 additive)原样透传 type,resolvedDatasetId = null。
|
|
30
|
+
*/
|
|
31
|
+
function resourcesOf(snapshot: ViewSnapshot, moduleId: string, modulesNode: Record<string, unknown>): ModuleResourcesEcho | null {
|
|
32
|
+
const raw = modulesNode[moduleId] as { resources?: Record<string, unknown> } | undefined;
|
|
33
|
+
const resources = raw?.resources;
|
|
34
|
+
if (!resources || typeof resources !== 'object' || Array.isArray(resources)) return null;
|
|
35
|
+
const registryPath = join(snapshot.appDir, KNOWLEDGE_REGISTRY_FILE);
|
|
36
|
+
// 台账惰性读(stat 指纹缓存;缺席/坏文件 = 空表——展开态 null,与 SDK loadKnowledgeRegistry fail-soft 同语义)
|
|
37
|
+
let bases: Record<string, { datasetId?: unknown }> = {};
|
|
38
|
+
if (existsSync(registryPath)) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = (snapshot.cache?.read(registryPath, (raw: string) => JSON.parse(raw) as unknown) ?? JSON.parse(readFileSync(registryPath, 'utf8'))) as {
|
|
41
|
+
knowledge_bases?: Record<string, { datasetId?: unknown }>;
|
|
42
|
+
};
|
|
43
|
+
bases = parsed.knowledge_bases ?? {};
|
|
44
|
+
} catch {
|
|
45
|
+
bases = {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const out: ModuleResourcesEcho = {};
|
|
49
|
+
for (const [id, spec] of Object.entries(resources)) {
|
|
50
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) continue;
|
|
51
|
+
const s = spec as Record<string, unknown>;
|
|
52
|
+
const type = s.type === 'knowledge' ? 'knowledge' : String(s.type ?? 'knowledge');
|
|
53
|
+
const direct = typeof s.datasetId === 'string' && s.datasetId !== '' ? s.datasetId : undefined;
|
|
54
|
+
const ref = typeof s.ref === 'string' && s.ref !== '' ? s.ref : undefined;
|
|
55
|
+
const resolved = direct ?? (ref !== undefined && typeof bases[ref]?.datasetId === 'string' ? (bases[ref].datasetId as string) : null);
|
|
56
|
+
const echo: ModuleResourceEcho = {
|
|
57
|
+
type: type as ModuleResourceEcho['type'],
|
|
58
|
+
...(direct !== undefined ? { datasetId: direct } : {}),
|
|
59
|
+
...(ref !== undefined ? { ref } : {}),
|
|
60
|
+
resolvedDatasetId: resolved,
|
|
61
|
+
...(typeof s.topK === 'number' ? { topK: s.topK } : {}),
|
|
62
|
+
...(typeof s.scoreThreshold === 'number' ? { scoreThreshold: s.scoreThreshold } : {}),
|
|
63
|
+
};
|
|
64
|
+
out[id] = echo;
|
|
65
|
+
}
|
|
66
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
16
69
|
/** 家族名归一(cli deriveModuleId 同源语义——依赖方向禁 import,故内联):
|
|
17
70
|
* `@tbox.cn/app-provider-x` / `@app/provider-y` / `provider-z` → `provider-*` */
|
|
18
71
|
function familyName(s: string): string {
|
|
@@ -27,15 +80,21 @@ function isProviderFamily(mod: { id: string; pkg: string }): boolean {
|
|
|
27
80
|
return familyName(mod.id).startsWith('provider-') || familyName(mod.pkg).startsWith('provider-');
|
|
28
81
|
}
|
|
29
82
|
|
|
30
|
-
/** GET /modules:纯模块清单(demand 不在——卡片展开 = GET /modules/:m;provider
|
|
31
|
-
|
|
83
|
+
/** GET /modules:纯模块清单(demand 不在——卡片展开 = GET /modules/:m;provider 家族包不在清单;
|
|
84
|
+
* hidden 模块默认过滤——includeHidden = true 可见并携带 hidden: true 徽标) */
|
|
85
|
+
export function loadModulesView(
|
|
86
|
+
snapshot: ViewSnapshot,
|
|
87
|
+
opts: { includeHidden?: boolean } = {},
|
|
88
|
+
): { modules: ModuleDeclaration[] } {
|
|
32
89
|
const modules: ModuleDeclaration[] = snapshot.walk.modules
|
|
33
90
|
.filter((mod) => !isProviderFamily(mod))
|
|
91
|
+
.filter((mod) => opts.includeHidden === true || mod.descriptor.hidden !== true)
|
|
34
92
|
.map((mod) => ({
|
|
35
93
|
id: mod.id,
|
|
36
94
|
// v7 词汇统一:name = 展示名(manifest name;兜底 id——恒有值)
|
|
37
95
|
name: mod.descriptor.name ?? mod.descriptor.id,
|
|
38
96
|
description: mod.descriptor.description ?? '',
|
|
97
|
+
...(mod.descriptor.hidden === true ? { hidden: true } : {}),
|
|
39
98
|
}))
|
|
40
99
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
41
100
|
return { modules };
|
|
@@ -75,9 +134,12 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
|
|
|
75
134
|
// v7 词汇统一:name = 展示名(兜底与 /modules 同语义——manifest name ?? id)
|
|
76
135
|
name: mod.descriptor.name ?? mod.descriptor.id,
|
|
77
136
|
description: mod.descriptor.description ?? '',
|
|
137
|
+
...(mod.descriptor.hidden === true ? { hidden: true } : {}),
|
|
78
138
|
// v6:ownedServices(declare.owns 展平排序)——N13:展示名走 resolveServiceDisplay 链,不重复携带
|
|
79
139
|
ownedServices: w.ownedServices,
|
|
80
140
|
integration,
|
|
141
|
+
// 模块资源只读镜像(N2 读半边——knowledge.json 台账 ref 展开态随行)
|
|
142
|
+
resources: resourcesOf(snapshot, moduleId, modulesNode),
|
|
81
143
|
binding: { provider: binding.provider, layer: binding.layer },
|
|
82
144
|
configEditable: w.writable,
|
|
83
145
|
services,
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
ServiceResolutionDetail,
|
|
11
11
|
ServiceStatus,
|
|
12
12
|
} from '../dto.js';
|
|
13
|
-
import { AppToolkitError } from '../core/errors.js';
|
|
13
|
+
import { AppToolkitError, contractsNotResolvedMessage } from '../core/errors.js';
|
|
14
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';
|
|
@@ -83,6 +83,10 @@ function evaluateInstance(
|
|
|
83
83
|
service,
|
|
84
84
|
instanceId,
|
|
85
85
|
supplies as never,
|
|
86
|
+
// v6 ownerOf:服务归属表(walk.ownership——buildOwnershipMap 同源产物)显式参。
|
|
87
|
+
// 缺席 + 文件含 modules 级绑定/配置 → contracts v6 fail-visible(binding-unresolved),
|
|
88
|
+
// root/modules 级联全断——视图轴与谓词轴同源透传,勿再遗漏(bug 复盘:fef9a71a 漏改点)。
|
|
89
|
+
snapshot.walk.ownership as Record<string, string>,
|
|
86
90
|
);
|
|
87
91
|
if (r.status === 'ok' && r.effective) {
|
|
88
92
|
const raw: unknown = r.effective.provider;
|
|
@@ -124,6 +128,7 @@ function evaluationContracts(snapshot: ViewSnapshot) {
|
|
|
124
128
|
service: string,
|
|
125
129
|
instanceId: string,
|
|
126
130
|
supplies?: never,
|
|
131
|
+
ownerOf?: Record<string, string>,
|
|
127
132
|
) => EffectiveResolutionShape;
|
|
128
133
|
normalizeInstances: (input: unknown) => { instances: Array<{ id: string }>; defaultInstanceId: string | null };
|
|
129
134
|
buildSupplyLookup: (catalog: unknown) => unknown;
|
|
@@ -205,18 +210,22 @@ function isSuppliedCheck(catalog: WalkCatalog, provider: string, service: string
|
|
|
205
210
|
return isSupplied(catalog, provider, service);
|
|
206
211
|
}
|
|
207
212
|
|
|
213
|
+
/** 批量键集(求值轴/降级轴共用单源:服务词汇 ∪ 需求侧 ∪ 已配置) */
|
|
214
|
+
function resolutionKeys(snapshot: ViewSnapshot): string[] {
|
|
215
|
+
const keys = new Set<string>([...snapshot.walk.vocabulary, ...Object.keys(snapshot.catalog.services)]);
|
|
216
|
+
const configured = (snapshot.integrations as { services?: Record<string, unknown> } | null)?.services ?? {};
|
|
217
|
+
for (const k of Object.keys(configured)) keys.add(k);
|
|
218
|
+
return [...keys].sort();
|
|
219
|
+
}
|
|
220
|
+
|
|
208
221
|
/** 批量求值(键集 = 服务词汇 ∪ 已配置;?service= 过滤语义——未知键缺席;issues 恒全量) */
|
|
209
222
|
export function loadServiceResolutionsView(
|
|
210
223
|
snapshot: ViewSnapshot,
|
|
211
224
|
issues: IntegrationIssue[],
|
|
212
225
|
filter?: string[],
|
|
213
226
|
): ServiceResolutionsView {
|
|
214
|
-
const keys = new Set<string>([...snapshot.walk.vocabulary, ...Object.keys(snapshot.catalog.services)]);
|
|
215
|
-
const configured = (snapshot.integrations as { services?: Record<string, unknown> } | null)?.services ?? {};
|
|
216
|
-
for (const k of Object.keys(configured)) keys.add(k);
|
|
217
|
-
|
|
218
227
|
const resolutions: Record<string, ServiceResolutionEntry> = {};
|
|
219
|
-
for (const service of
|
|
228
|
+
for (const service of resolutionKeys(snapshot)) {
|
|
220
229
|
if (filter && !filter.includes(service)) continue;
|
|
221
230
|
resolutions[service] = resolveServiceSummary(service, snapshot);
|
|
222
231
|
}
|
|
@@ -238,20 +247,12 @@ export function loadServiceResolutionDetail(
|
|
|
238
247
|
});
|
|
239
248
|
|
|
240
249
|
// P4 枚举规则:root 注册表 id 逐一 + 显式 '*' 条目(若在);无注册表且节点通配激活 → 单 '*' 合成;
|
|
241
|
-
// 无注册表无节点 → []
|
|
250
|
+
// 无注册表无节点 → [](枚举单源 = enumerateInstanceIds——降级轴声明实例投影共用)
|
|
242
251
|
const declaredIds = normalized.instances.map((i) => i.id);
|
|
243
252
|
const slot = (snapshot.integrations as { services?: Record<string, { instances?: Record<string, unknown> }> } | null)
|
|
244
253
|
?.services?.[service];
|
|
245
254
|
const slotKeys = slot?.instances ? Object.keys(slot.instances) : undefined;
|
|
246
|
-
|
|
247
|
-
if (declaredIds.length > 0) {
|
|
248
|
-
instanceIds = [...declaredIds];
|
|
249
|
-
if (slotKeys?.includes('*')) instanceIds.push('*');
|
|
250
|
-
} else if (slotKeys !== undefined && slotKeys.length > 0) {
|
|
251
|
-
instanceIds = slotKeys.includes('*') ? ['*'] : slotKeys;
|
|
252
|
-
} else {
|
|
253
|
-
instanceIds = [];
|
|
254
|
-
}
|
|
255
|
+
const instanceIds = enumerateInstanceIds(slotKeys, declaredIds);
|
|
255
256
|
|
|
256
257
|
// 默认实例口径(摘要 status = 默认实例求值——不重复头部;原 defaultEval 死代码已删 FX-2c)
|
|
257
258
|
|
|
@@ -267,7 +268,7 @@ export function loadServiceResolutionDetail(
|
|
|
267
268
|
});
|
|
268
269
|
|
|
269
270
|
// inheritance 四层(app → module → service → instance——effective 链声明位投影)
|
|
270
|
-
const inheritance = buildInheritance(snapshot, service);
|
|
271
|
+
const inheritance = buildInheritance(snapshot, service, normalized.defaultInstanceId ?? '*');
|
|
271
272
|
|
|
272
273
|
const summary = resolveServiceSummary(service, snapshot);
|
|
273
274
|
const resolution: ServiceResolution = {
|
|
@@ -283,24 +284,31 @@ export function loadServiceResolutionDetail(
|
|
|
283
284
|
};
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
/** P4 实例枚举单源(求值轴/降级轴共用):root 注册表 id 逐一 + 显式 '*' 条目(若在);
|
|
288
|
+
* 无注册表且节点通配激活 → 单 '*' 合成;无注册表无节点 → []。 */
|
|
289
|
+
function enumerateInstanceIds(slotKeys: string[] | undefined, declaredIds: string[]): string[] {
|
|
290
|
+
if (declaredIds.length > 0) {
|
|
291
|
+
const ids = [...declaredIds];
|
|
292
|
+
if (slotKeys?.includes('*')) ids.push('*');
|
|
293
|
+
return ids;
|
|
294
|
+
}
|
|
295
|
+
if (slotKeys !== undefined && slotKeys.length > 0) return slotKeys.includes('*') ? ['*'] : slotKeys;
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
|
|
286
299
|
/** inheritance 投影:四层各取该层声明 provider/implementation(缺席层 = 继承位,provider 缺席)。
|
|
287
|
-
* 实例层 = **默认实例口径**(FX-2c 修——与摘要 status
|
|
288
|
-
*
|
|
300
|
+
* 实例层 = **默认实例口径**(FX-2c 修——与摘要 status 同轴);defaultInstanceId 由调用方解析
|
|
301
|
+
* (求值轴 = normalizeInstances 委托;降级轴 = 原始直读——本函数零 contracts 依赖,双轴共用单源)。 */
|
|
289
302
|
function buildInheritance(
|
|
290
303
|
snapshot: ViewSnapshot,
|
|
291
304
|
service: string,
|
|
305
|
+
defaultInstanceId: string,
|
|
292
306
|
): ServiceResolution['inheritance'] {
|
|
293
307
|
const integrations = snapshot.integrations as {
|
|
294
308
|
provider?: string;
|
|
295
309
|
services?: Record<string, { provider?: unknown; implementation?: string; instances?: Record<string, { provider?: string; implementation?: string }> }>;
|
|
296
310
|
} | null;
|
|
297
311
|
const slot = integrations?.services?.[service];
|
|
298
|
-
const cfg = snapshot.integrations as { instances?: unknown; defaultInstanceId?: unknown } | null;
|
|
299
|
-
const normalized = evaluationContracts(snapshot).normalizeInstances({
|
|
300
|
-
instances: cfg?.instances,
|
|
301
|
-
defaultInstanceId: cfg?.defaultInstanceId,
|
|
302
|
-
});
|
|
303
|
-
const defaultInstanceId = normalized.defaultInstanceId ?? '*';
|
|
304
312
|
const defaultInstance = slot?.instances?.[defaultInstanceId] ?? slot?.instances?.['*'];
|
|
305
313
|
// module 段消费 resolveModuleBinding 单源(v6——与 binding / 谓词 #7 同源;ownerOf=walk.ownership)
|
|
306
314
|
const owner = snapshot.walk.ownership[service];
|
|
@@ -328,3 +336,83 @@ function buildInheritance(
|
|
|
328
336
|
|
|
329
337
|
/** 谓词全量 issues 装配(写路径/求值轴共用——工厂经 evaluateIntegrationServices 装配后传入) */
|
|
330
338
|
export type { IntegrationIssue };
|
|
339
|
+
|
|
340
|
+
// ── 降级轴(v7——policy-at-edge 读求值轴退役)──
|
|
341
|
+
// contracts 不可用(未安装 / 无严格面 / 求值面缺席 / v6 标记缺席)时,读求值轴不再 503,
|
|
342
|
+
// 改投静态骨架(walk + integrations 纯文件投影,零 contracts 依赖)+ evaluation-unavailable 标记。
|
|
343
|
+
// 依据链:doctor 分级先例(未安装 = warning 正常态)+ D31 include→null 先例;fail-visible 语义
|
|
344
|
+
// 保留在数据内(status + statusMessage N1 分支文案),写路径与 internals 门控不变。
|
|
345
|
+
|
|
346
|
+
/** 读求值轴可用判据(三条件与——降级门单源;写路径 assertContractsUsable 另有 503 语义,不复用) */
|
|
347
|
+
export function contractsEvaluable(contracts: ResolvedContracts): boolean {
|
|
348
|
+
return contracts.evaluation && contracts.strictValidation && contracts.ownershipBindingV6;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** 降级单条目(静态可算字段:module = owners 首 / required = OR 聚合;provider/layer 不投——
|
|
352
|
+
* 保持「求值产物」语义纯净,layer 单独在场违 D31 孤儿层口径) */
|
|
353
|
+
function degradeEntry(snapshot: ViewSnapshot, service: string, message: string): ServiceResolutionEntry {
|
|
354
|
+
const demand = snapshot.catalog.services[service];
|
|
355
|
+
return {
|
|
356
|
+
status: 'evaluation-unavailable',
|
|
357
|
+
statusMessage: message,
|
|
358
|
+
...(demand?.owners[0] !== undefined ? { module: demand.owners[0] } : {}),
|
|
359
|
+
...(demand !== undefined ? { required: !demand.optional } : {}),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** 批量降级视图(键集与求值轴同源 resolutionKeys;?service= 过滤语义一致;issues 恒 []——
|
|
364
|
+
* 谓词与求值同源缺席;声明面诊断走 declarationIssues 通道不在此混装) */
|
|
365
|
+
export function loadServiceResolutionsDegradeView(
|
|
366
|
+
snapshot: ViewSnapshot,
|
|
367
|
+
contracts: ResolvedContracts,
|
|
368
|
+
filter?: string[],
|
|
369
|
+
): ServiceResolutionsView {
|
|
370
|
+
const message = contractsNotResolvedMessage(contracts);
|
|
371
|
+
const resolutions: Record<string, ServiceResolutionEntry> = {};
|
|
372
|
+
for (const service of resolutionKeys(snapshot)) {
|
|
373
|
+
if (filter && !filter.includes(service)) continue;
|
|
374
|
+
resolutions[service] = degradeEntry(snapshot, service, message);
|
|
375
|
+
}
|
|
376
|
+
return { contracts: toContractsResolution(contracts), resolutions, issues: [] };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** 单体降级视图(P14 extends 结构保证——摘要基 + instances 声明枚举 + inheritance 四层静态投影) */
|
|
380
|
+
export function loadServiceResolutionDegradeDetail(
|
|
381
|
+
service: string,
|
|
382
|
+
snapshot: ViewSnapshot,
|
|
383
|
+
contracts: ResolvedContracts,
|
|
384
|
+
): ServiceResolutionDetail {
|
|
385
|
+
const message = contractsNotResolvedMessage(contracts);
|
|
386
|
+
const resolution: ServiceResolution = {
|
|
387
|
+
...degradeEntry(snapshot, service, message),
|
|
388
|
+
instances: degradeInstances(snapshot, service),
|
|
389
|
+
inheritance: buildInheritance(snapshot, service, degradeDefaultInstanceId(snapshot)),
|
|
390
|
+
};
|
|
391
|
+
return { contracts: toContractsResolution(contracts), resolution, issues: [] };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 声明实例投影(原始数据——root instances[].id ∪ slot 实例键,P4 枚举单源共用;
|
|
395
|
+
* 非 normalizeInstances 派生语义,单源不破)。逐实例同标记,statusMessage 不重复携带
|
|
396
|
+
* (resolution 顶层已携带)。 */
|
|
397
|
+
function degradeInstances(snapshot: ViewSnapshot, service: string): ResolvedInstance[] {
|
|
398
|
+
const slot = (snapshot.integrations as { services?: Record<string, { instances?: Record<string, unknown> }> } | null)
|
|
399
|
+
?.services?.[service];
|
|
400
|
+
const cfg = snapshot.integrations as { instances?: unknown } | null;
|
|
401
|
+
const declaredIds = Array.isArray(cfg?.instances)
|
|
402
|
+
? (cfg.instances as Array<{ id?: unknown }>)
|
|
403
|
+
.map((i) => (i && typeof i === 'object' ? i.id : undefined))
|
|
404
|
+
.filter((id): id is string => typeof id === 'string')
|
|
405
|
+
: [];
|
|
406
|
+
const slotKeys = slot?.instances ? Object.keys(slot.instances) : undefined;
|
|
407
|
+
return enumerateInstanceIds(slotKeys, declaredIds).map((id) => ({
|
|
408
|
+
instanceId: id,
|
|
409
|
+
status: 'evaluation-unavailable' as const,
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** 降级默认实例口径(近似声明):defaultInstanceId 原始直读,缺席 → '*' 通配位。
|
|
414
|
+
* normalizeInstances 的「单候选唯一解」推导不可复制(求值面单源)——近似口径见 api 契约。 */
|
|
415
|
+
function degradeDefaultInstanceId(snapshot: ViewSnapshot): string {
|
|
416
|
+
const cfg = snapshot.integrations as { defaultInstanceId?: unknown } | null;
|
|
417
|
+
return typeof cfg?.defaultInstanceId === 'string' && cfg.defaultInstanceId.length > 0 ? cfg.defaultInstanceId : '*';
|
|
418
|
+
}
|