@tbox.cn/app-toolkit 0.1.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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/chunk-VV3ERZXS.js +1 -0
- package/dist/contracts-resolver-QM4FBQQV.js +1 -0
- package/dist/index.d.ts +1909 -0
- package/dist/index.js +8 -0
- package/package.json +47 -0
- package/src/assembly/app-manifest.ts +132 -0
- package/src/assembly/provider-catalog.ts +167 -0
- package/src/assembly/walk-manifests.ts +461 -0
- package/src/core/contracts-expected.ts +31 -0
- package/src/core/contracts-resolver.ts +274 -0
- package/src/core/credential-format.ts +98 -0
- package/src/core/credential-mask.ts +19 -0
- package/src/core/env-expansion.ts +51 -0
- package/src/core/errors.ts +46 -0
- package/src/core/file-cache.ts +65 -0
- package/src/core/fskit.ts +21 -0
- package/src/core/module-schema.ts +139 -0
- package/src/dto.ts +298 -0
- package/src/factory.ts +297 -0
- package/src/index.ts +137 -0
- package/src/integrations/credentials.ts +153 -0
- package/src/integrations/mock-bindings.ts +55 -0
- package/src/integrations/predicate-io.ts +199 -0
- package/src/integrations/predicate.ts +598 -0
- package/src/integrations/read.ts +53 -0
- package/src/integrations/write.ts +448 -0
- package/src/views/app.ts +28 -0
- package/src/views/context.ts +70 -0
- package/src/views/modules.ts +48 -0
- package/src/views/providers.ts +165 -0
- package/src/views/service-detail.ts +32 -0
- package/src/views/service-resolutions.ts +323 -0
- package/tests/contracts-resolver.test.ts +203 -0
- package/tests/demo-app.ts +146 -0
- package/tests/dev-manifest-catalog.test.ts +114 -0
- package/tests/error-name-safety.test.ts +26 -0
- package/tests/file-cache.test.ts +242 -0
- package/tests/import-layers.test.ts +111 -0
- package/tests/module-schema.test.ts +84 -0
- package/tests/naming-alignment.test.ts +50 -0
- package/tests/views.test.ts +427 -0
- package/tests/write-core.test.ts +188 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +29 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { ProviderDetail, ProviderEntry, ProviderServiceDetail, ProvidersView } from '../dto.js';
|
|
2
|
+
import { AppToolkitError } from '../core/errors.js';
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { walkManifests } from '../assembly/walk-manifests.js';
|
|
6
|
+
import type { ViewSnapshot } from './context.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 供给轴投影(views/providers;C4)。供给关系三切片(P5):
|
|
10
|
+
* 名单 = /providers(基本信息)· 足迹 = /providers/:p(implementations[] 保真)·
|
|
11
|
+
* 表单 = /providers/:p/services/:s(组合单体——schema 唯一暴露面 P10)。
|
|
12
|
+
* 静态端点零求值(铁律 #3)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** GET /providers:厂商基本信息(供给足迹不在本端点 → /providers/:p) */
|
|
16
|
+
export function loadProvidersView(snapshot: ViewSnapshot): ProvidersView {
|
|
17
|
+
const providers: ProviderEntry[] = [];
|
|
18
|
+
for (const [provider, impls] of Object.entries(snapshot.catalog.providers)) {
|
|
19
|
+
const entries = Object.values(impls);
|
|
20
|
+
if (entries.length === 0) continue;
|
|
21
|
+
providers.push({
|
|
22
|
+
provider,
|
|
23
|
+
local: entries.every((e) => e.local === true),
|
|
24
|
+
owner: entries[0].owner,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return { providers: providers.sort((a, b) => a.provider.localeCompare(b.provider)) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** GET /providers/:provider:供给足迹(implementations 恒数组——H1 多实现钉版数据源) */
|
|
31
|
+
export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string): ProviderDetail {
|
|
32
|
+
const impls = snapshot.catalog.providers[provider];
|
|
33
|
+
if (!impls) {
|
|
34
|
+
throw new AppToolkitError('PROVIDER_NOT_FOUND', 404, `厂商 ${provider} 不 ∈ catalog(内联声明厂商无包级档案——身份与表单经组合端点暴露)`);
|
|
35
|
+
}
|
|
36
|
+
const services: ProviderDetail['services'] = {};
|
|
37
|
+
for (const entry of Object.values(impls)) {
|
|
38
|
+
for (const service of entry.services) {
|
|
39
|
+
const bucket = (services[service] ??= { implementations: [] });
|
|
40
|
+
bucket.implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const entries = Object.values(impls);
|
|
44
|
+
return {
|
|
45
|
+
provider,
|
|
46
|
+
local: entries.every((e) => e.local === true),
|
|
47
|
+
owner: entries[0].owner,
|
|
48
|
+
services,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 组合单体(GET /providers/:p/services/:s):catalog 供给与槽级内联声明统一解析(P10/P11) */
|
|
53
|
+
export function loadProviderServiceDetailView(
|
|
54
|
+
snapshot: ViewSnapshot,
|
|
55
|
+
provider: string,
|
|
56
|
+
service: string,
|
|
57
|
+
): ProviderServiceDetail {
|
|
58
|
+
const impls = snapshot.catalog.providers[provider];
|
|
59
|
+
const configured = (snapshot.integrations as {
|
|
60
|
+
services?: Record<string, { provider?: unknown; implementation?: string }>;
|
|
61
|
+
} | null)?.services ?? {};
|
|
62
|
+
|
|
63
|
+
// 供给解析(P11 判据 = 供给存在):catalog 槽供给 → implementations[] 数组装配(声明序)
|
|
64
|
+
const implementations: Array<{ implementation: string; credentialType: string }> = [];
|
|
65
|
+
if (impls) {
|
|
66
|
+
for (const entry of Object.values(impls)) {
|
|
67
|
+
if (entry.services.includes(service)) {
|
|
68
|
+
implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 内联声明(该 provider 名——catalog 外或补充):单元素(声明对)
|
|
74
|
+
const inline = configured[service]?.provider;
|
|
75
|
+
const inlineDeclaration =
|
|
76
|
+
inline && typeof inline === 'object' && (inline as { provider?: string }).provider === provider
|
|
77
|
+
? (inline as { credentialType: string; provider: string })
|
|
78
|
+
: undefined;
|
|
79
|
+
|
|
80
|
+
if (implementations.length === 0 && !inlineDeclaration) {
|
|
81
|
+
if (!impls) {
|
|
82
|
+
throw new AppToolkitError('PROVIDER_NOT_FOUND', 404, `厂商 ${provider} 完全未知(不 ∈ catalog 且无任何内联声明)`);
|
|
83
|
+
}
|
|
84
|
+
throw new AppToolkitError('SUPPLY_NOT_FOUND', 404, `厂商 ${provider} 未供给服务 ${service}(AI 帮我实现流:PUT 显式绑定后实施)`);
|
|
85
|
+
}
|
|
86
|
+
if (implementations.length === 0 && inlineDeclaration) {
|
|
87
|
+
implementations.push({ implementation: configured[service]?.implementation ?? `${provider}-inline@1`, credentialType: inlineDeclaration.credentialType });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// integrationSchemas:catalog schema(复合键)优先;内联声明 configSchema 引用应用 config/schemas/
|
|
91
|
+
// (路径约束 H9——resolve 落 config/schemas/ 内 + realpath 复核,违例 400)
|
|
92
|
+
let configSchema: object | null = null;
|
|
93
|
+
let credentialSchema: object | null = null;
|
|
94
|
+
const schemaEntry = walkManifests(snapshot.appDir).serviceSchemas[`${provider}\u0000${service}`];
|
|
95
|
+
if (schemaEntry) {
|
|
96
|
+
configSchema = readSchemaRef(snapshot.appDir, schemaEntry.packageDir, schemaEntry.configSchema);
|
|
97
|
+
credentialSchema = readSchemaRef(snapshot.appDir, schemaEntry.packageDir, schemaEntry.credentialSchema);
|
|
98
|
+
} else if (inlineDeclaration) {
|
|
99
|
+
// 内联声明 schema 解析:引用文件在 integrations 节点(provider.configSchema)——
|
|
100
|
+
// 形状同 config/schemas/ 约束(路径来源 = 节点声明,由服务端读应用侧)
|
|
101
|
+
const inlineSchemaRef = (inline as { configSchema?: string } | undefined)?.configSchema;
|
|
102
|
+
if (typeof inlineSchemaRef === 'string') {
|
|
103
|
+
configSchema = readInlineSchema(snapshot.appDir, inlineSchemaRef);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
provider,
|
|
109
|
+
service,
|
|
110
|
+
implementations,
|
|
111
|
+
integrationSchemas: { configSchema, credentialSchema },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** catalog schema 引用读取(相对包根;缺席 → null——SCHEMA_FILE_MISSING 归谓词 #6) */
|
|
116
|
+
function readSchemaRef(_appDir: string, packageDir: string, ref: string | undefined): object | null {
|
|
117
|
+
if (!ref) return null;
|
|
118
|
+
const file = join(packageDir, ref);
|
|
119
|
+
if (!existsSync(file)) return null;
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(readFileSync(file, 'utf8')) as object;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 内联声明 configSchema 读取(H9 安全钉子,FX-2a 修:分隔符级边界 + realpath 复核):
|
|
128
|
+
* ①lexical:path.relative 判定必须落在 <appDir>/config/schemas/ 内(禁 `..` 逃逸/绝对路径/
|
|
129
|
+
* 同前缀兄弟目录 `config/schemas-evil`——原 startsWith 无分隔符边界被绕过);
|
|
130
|
+
* ②physical:realpathSync 复核(symlink 逃逸——防「凭据 JSON 被当 schema 回显」任意文件读原语)。 */
|
|
131
|
+
function readInlineSchema(appDir: string, ref: string): object | null {
|
|
132
|
+
const schemasRoot = resolve(appDir, 'config', 'schemas');
|
|
133
|
+
const target = resolve(appDir, ref);
|
|
134
|
+
const rel = relative(schemasRoot, target);
|
|
135
|
+
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
|
|
136
|
+
throw new AppToolkitError(
|
|
137
|
+
'VALIDATION_FAILED',
|
|
138
|
+
400,
|
|
139
|
+
'configSchema 引用必须落在 config/schemas/ 内(禁路径逃逸)',
|
|
140
|
+
['provider.configSchema'],
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!existsSync(target)) return null;
|
|
144
|
+
let realRoot: string;
|
|
145
|
+
let realTarget: string;
|
|
146
|
+
try {
|
|
147
|
+
realRoot = realpathSync(schemasRoot);
|
|
148
|
+
realTarget = realpathSync(target);
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (realTarget !== realRoot && !realTarget.startsWith(realRoot + sep)) {
|
|
153
|
+
throw new AppToolkitError(
|
|
154
|
+
'VALIDATION_FAILED',
|
|
155
|
+
400,
|
|
156
|
+
'configSchema 引用经 symlink 逃逸出 config/schemas/(realpath 复核拒绝)',
|
|
157
|
+
['provider.configSchema'],
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(readFileSync(target, 'utf8')) as object;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ServiceDetail } from '../dto.js';
|
|
2
|
+
import { AppToolkitError } from '../core/errors.js';
|
|
3
|
+
import { suppliedByVendors, type ViewSnapshot } from './context.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 服务静态单体投影(views/service-detail;C4)。求值数据不在此(→ /service-resolutions/:service)。
|
|
7
|
+
* SERVICE_NOT_FOUND 判据 = 服务词汇 ∪ 已配置(P9——词汇成员零 demand/供给/config 仍 200)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function loadServiceDetailView(snapshot: ViewSnapshot, service: string): ServiceDetail {
|
|
11
|
+
const configured = (snapshot.integrations as { services?: Record<string, unknown> } | null)?.services ?? {};
|
|
12
|
+
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) {
|
|
17
|
+
throw new AppToolkitError('SERVICE_NOT_FOUND', 404, `服务 ${service} 不在服务词汇(已装契约包 ∪ 已配置)`);
|
|
18
|
+
}
|
|
19
|
+
// integration = 文件 services[s] 逐字原值(编辑预填源 D21);无声明 = null
|
|
20
|
+
const integration = (configured[service] as ServiceDetail['integration'] | undefined) ?? null;
|
|
21
|
+
return {
|
|
22
|
+
service,
|
|
23
|
+
...(demand?.title ? { title: demand.title } : { title: service }),
|
|
24
|
+
...(demand?.description ? { description: demand.description } : {}),
|
|
25
|
+
required: demand ? !demand.optional : false,
|
|
26
|
+
// defaults 唯一落位(manifest 预填)
|
|
27
|
+
defaults: demand ? demand.defaults.map((d) => ({ ...d })) : [],
|
|
28
|
+
integration,
|
|
29
|
+
// picker 数据源:供给该服务的厂商名单(catalog 静态切片)
|
|
30
|
+
suppliedBy: suppliedByVendors(snapshot.catalog, service),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BindingLayer,
|
|
3
|
+
ContractsResolution,
|
|
4
|
+
CredentialEcho,
|
|
5
|
+
EffectiveBinding,
|
|
6
|
+
IntegrationIssue,
|
|
7
|
+
ResolvedInstance,
|
|
8
|
+
ServiceResolution,
|
|
9
|
+
ServiceResolutionEntry,
|
|
10
|
+
ServiceResolutionsView,
|
|
11
|
+
ServiceResolutionDetail,
|
|
12
|
+
ServiceStatus,
|
|
13
|
+
} from '../dto.js';
|
|
14
|
+
import { maskCredentialValues, readCredentialFile, stemToFilePath } from '../core/credential-format.js';
|
|
15
|
+
import type { ResolvedContracts } from '../core/contracts-resolver.js';
|
|
16
|
+
import type { WalkCatalog } from '../assembly/walk-manifests.js';
|
|
17
|
+
import { domainOfService, isSupplied, type ViewSnapshot } from './context.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 求值轴投影(views/service-resolutions;C4;FX-2c——B6 effective/credential 填充 + F2 layer 推导)。
|
|
21
|
+
*
|
|
22
|
+
* status(service, instanceId) = contracts.resolveEffectiveService(integrations, service, instanceId,
|
|
23
|
+
* supplies).status——动态(应用自身 contracts 求值单源)。
|
|
24
|
+
* 默认实例 = 应用自身 normalizeInstances 委托(显式 defaultInstanceId ?? 单候选唯一解;
|
|
25
|
+
* null → '*' 通配口径求值——显式键集无通配 → instance-not-found 如实暴露;不自造 instances[0])。
|
|
26
|
+
* required(service) = 任一需求方 required(OR 聚合——demandedServices.optional AND 合并的补语义;
|
|
27
|
+
* 平台/孤儿恒 false)——集成就绪判定单源:pending = Σ (required && status !== 'ok')。
|
|
28
|
+
* **P14 单一形状(结构保证)**:resolveServiceSummary() 单源函数——/service-resolutions map 值 ≡
|
|
29
|
+
* /service-resolutions/:s 的 resolution 摘要基(ServiceResolution extends ServiceResolutionEntry)。
|
|
30
|
+
* **实例条目**:effective(合并绑定投影 + layer 推导——contracts EffectiveService 无 layer,F2)
|
|
31
|
+
* + credential(掩码回显——GET 永无真值;文件缺席省略,缺失归谓词 #1)。
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
function toContractsResolution(contracts: ResolvedContracts): ContractsResolution {
|
|
35
|
+
return {
|
|
36
|
+
version: contracts.version,
|
|
37
|
+
evaluation: contracts.evaluation,
|
|
38
|
+
strictValidation: contracts.strictValidation,
|
|
39
|
+
notes: [...contracts.notes],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 实例求值产物(摘要消费 status/provider;单体消费 effective) */
|
|
44
|
+
interface InstanceEvaluation {
|
|
45
|
+
status: ServiceStatus;
|
|
46
|
+
statusMessage?: string;
|
|
47
|
+
provider?: string;
|
|
48
|
+
implementation?: string;
|
|
49
|
+
effective?: EffectiveBinding;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 最特异声明层推导(F2):inst 位 provider 在场 → 'instance';槽级 → 'service';域级 → 'module';
|
|
53
|
+
* root → 'app'。实例位查找复刻 TIER0 语义(精确键优先、'*' 通配兜底——与 resolveEffectiveService
|
|
54
|
+
* 的 instances 查找层同轴)。 */
|
|
55
|
+
function deriveBindingLayer(snapshot: ViewSnapshot, service: string, instanceId: string): BindingLayer {
|
|
56
|
+
const integrations = snapshot.integrations as {
|
|
57
|
+
provider?: unknown;
|
|
58
|
+
domains?: Record<string, { provider?: unknown }>;
|
|
59
|
+
services?: Record<string, { provider?: unknown; instances?: Record<string, { provider?: unknown }> }>;
|
|
60
|
+
} | null;
|
|
61
|
+
const domain = domainOfService(service);
|
|
62
|
+
const slot = integrations?.services?.[service];
|
|
63
|
+
const inst = slot?.instances?.[instanceId] ?? slot?.instances?.['*'];
|
|
64
|
+
if (inst && inst.provider !== undefined) return 'instance';
|
|
65
|
+
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) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 求值单实例(resolveEffectiveService 动态求值;ok 态携带完整 effective——layer 推导附) */
|
|
80
|
+
function evaluateInstance(
|
|
81
|
+
snapshot: ViewSnapshot,
|
|
82
|
+
contracts: NonNullable<ReturnType<typeof evaluationContracts>>,
|
|
83
|
+
service: string,
|
|
84
|
+
instanceId: string,
|
|
85
|
+
supplies: unknown,
|
|
86
|
+
): InstanceEvaluation {
|
|
87
|
+
const r = contracts.resolveEffectiveService(
|
|
88
|
+
snapshot.integrations as never,
|
|
89
|
+
service,
|
|
90
|
+
instanceId,
|
|
91
|
+
supplies as never,
|
|
92
|
+
);
|
|
93
|
+
if (r.status === 'ok' && r.effective) {
|
|
94
|
+
const raw: unknown = r.effective.provider;
|
|
95
|
+
const provider = typeof raw === 'string' ? raw : String((raw as { provider?: string } | null)?.provider ?? '');
|
|
96
|
+
return {
|
|
97
|
+
status: 'ok',
|
|
98
|
+
provider,
|
|
99
|
+
implementation: r.effective.implementation,
|
|
100
|
+
effective: {
|
|
101
|
+
provider,
|
|
102
|
+
implementation: r.effective.implementation,
|
|
103
|
+
...(r.effective.config !== undefined ? { config: { ...(r.effective.config as Record<string, unknown>) } } : {}),
|
|
104
|
+
...(r.effective.credentialRef !== undefined ? { credentialRef: r.effective.credentialRef } : {}),
|
|
105
|
+
layer: deriveBindingLayer(snapshot, service, instanceId),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (r.status === 'binding-unresolved') return { status: 'binding-unresolved', statusMessage: r.message };
|
|
110
|
+
return { status: r.status };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 应用自身 contracts(快照.contracts.module 的求值子集——门控保证在场才调用) */
|
|
114
|
+
function evaluationContracts(snapshot: ViewSnapshot) {
|
|
115
|
+
const mod = snapshot.contracts.module as {
|
|
116
|
+
resolveEffectiveService?: (...args: never[]) => EffectiveResolutionShape;
|
|
117
|
+
normalizeInstances?: (input: unknown) => { instances: Array<{ id: string }>; defaultInstanceId: string | null };
|
|
118
|
+
buildSupplyLookup?: (catalog: unknown) => unknown;
|
|
119
|
+
};
|
|
120
|
+
// 门控(工厂边缘)保证 evaluation/strictValidation 后此断言恒真
|
|
121
|
+
if (!mod.resolveEffectiveService || !mod.normalizeInstances) {
|
|
122
|
+
throw new Error('contracts 求值面缺席(工厂门控应先行拦截)');
|
|
123
|
+
}
|
|
124
|
+
return mod as unknown as {
|
|
125
|
+
resolveEffectiveService: (
|
|
126
|
+
integrations: never,
|
|
127
|
+
service: string,
|
|
128
|
+
instanceId: string,
|
|
129
|
+
supplies?: never,
|
|
130
|
+
) => EffectiveResolutionShape;
|
|
131
|
+
normalizeInstances: (input: unknown) => { instances: Array<{ id: string }>; defaultInstanceId: string | null };
|
|
132
|
+
buildSupplyLookup: (catalog: unknown) => unknown;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** contracts EffectiveServiceResolution 消费面形状(effective 五字段——FX-2c 全集透传) */
|
|
137
|
+
interface EffectiveResolutionShape {
|
|
138
|
+
status: ServiceStatus;
|
|
139
|
+
effective?: {
|
|
140
|
+
provider: unknown;
|
|
141
|
+
implementation: string;
|
|
142
|
+
config?: Readonly<Record<string, unknown>>;
|
|
143
|
+
credentialRef?: string;
|
|
144
|
+
};
|
|
145
|
+
message?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** P14 单源摘要函数:/service-resolutions map 值 ≡ /service-resolutions/:s 的 resolution 摘要基 */
|
|
149
|
+
export function resolveServiceSummary(
|
|
150
|
+
service: string,
|
|
151
|
+
snapshot: ViewSnapshot,
|
|
152
|
+
): ServiceResolutionEntry {
|
|
153
|
+
const contracts = evaluationContracts(snapshot);
|
|
154
|
+
const { integrations, catalog } = snapshot;
|
|
155
|
+
|
|
156
|
+
// 供给查表(buildSupplyLookup 动态派生——应用自身 contracts)
|
|
157
|
+
const supplies = contracts.buildSupplyLookup(catalog as never);
|
|
158
|
+
|
|
159
|
+
// 默认实例 = 应用自身 normalizeInstances 委托(null → '*' 通配口径)
|
|
160
|
+
const cfg = integrations as { instances?: unknown; defaultInstanceId?: unknown } | null;
|
|
161
|
+
const normalized = contracts.normalizeInstances({
|
|
162
|
+
instances: cfg?.instances,
|
|
163
|
+
defaultInstanceId: cfg?.defaultInstanceId,
|
|
164
|
+
});
|
|
165
|
+
const defaultInstanceId = normalized.defaultInstanceId ?? '*';
|
|
166
|
+
|
|
167
|
+
// 跨实例求值异常聚合(statusMessage 直投)
|
|
168
|
+
const evaluation = evaluateInstance(snapshot, contracts, service, defaultInstanceId, supplies);
|
|
169
|
+
|
|
170
|
+
// provider:effective 求值厂商(最特异声明层——跨层事实);缺席 = 未配置/未解析
|
|
171
|
+
const declaredProvider = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
|
|
172
|
+
?.services?.[service]?.provider;
|
|
173
|
+
const provider =
|
|
174
|
+
evaluation.provider ??
|
|
175
|
+
(typeof declaredProvider === 'string' ? declaredProvider : typeof declaredProvider === 'object' && declaredProvider ? String((declaredProvider as { provider?: string }).provider) : undefined);
|
|
176
|
+
|
|
177
|
+
// supplied 点查:provider ∈ 厂商集合时在场(true 供给 / false 显式绑定未供给——「AI 实施中」);
|
|
178
|
+
// provider ∉(自定义 API)或缺席时缺席
|
|
179
|
+
const knownVendors = new Set(Object.keys(snapshot.catalog.providers));
|
|
180
|
+
const supplied =
|
|
181
|
+
provider !== undefined && knownVendors.has(provider)
|
|
182
|
+
? isSuppliedCheck(snapshot.catalog, provider, service)
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
// required echo(OR 聚合——demand 单源随求值轴;平台/孤儿恒 false)
|
|
186
|
+
const demand = catalog.services[service];
|
|
187
|
+
const required = demand !== undefined ? !demand.optional : undefined;
|
|
188
|
+
|
|
189
|
+
// module = owner 模块(demand owners 首);平台/孤儿缺席
|
|
190
|
+
const module = demand?.owners[0];
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
status: evaluation.status,
|
|
194
|
+
...(evaluation.statusMessage ? { statusMessage: evaluation.statusMessage } : {}),
|
|
195
|
+
...(provider !== undefined ? { provider } : {}),
|
|
196
|
+
...(supplied !== undefined ? { supplied } : {}),
|
|
197
|
+
...(module !== undefined ? { module } : {}),
|
|
198
|
+
...(required !== undefined ? { required } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isSuppliedCheck(catalog: WalkCatalog, provider: string, service: string): boolean {
|
|
203
|
+
return isSupplied(catalog, provider, service);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 批量求值(键集 = 服务词汇 ∪ 已配置;?service= 过滤语义——未知键缺席;issues 恒全量) */
|
|
207
|
+
export function loadServiceResolutionsView(
|
|
208
|
+
snapshot: ViewSnapshot,
|
|
209
|
+
issues: IntegrationIssue[],
|
|
210
|
+
filter?: string[],
|
|
211
|
+
): ServiceResolutionsView {
|
|
212
|
+
const keys = new Set<string>([...snapshot.walk.vocabulary, ...Object.keys(snapshot.catalog.services)]);
|
|
213
|
+
const configured = (snapshot.integrations as { services?: Record<string, unknown> } | null)?.services ?? {};
|
|
214
|
+
for (const k of Object.keys(configured)) keys.add(k);
|
|
215
|
+
|
|
216
|
+
const resolutions: Record<string, ServiceResolutionEntry> = {};
|
|
217
|
+
for (const service of [...keys].sort()) {
|
|
218
|
+
if (filter && !filter.includes(service)) continue;
|
|
219
|
+
resolutions[service] = resolveServiceSummary(service, snapshot);
|
|
220
|
+
}
|
|
221
|
+
return { contracts: toContractsResolution(snapshot.contracts), resolutions, issues };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 求值单体(完整结构 instances + inheritance + 过滤 issues;P14 extends 结构保证) */
|
|
225
|
+
export function loadServiceResolutionDetail(
|
|
226
|
+
service: string,
|
|
227
|
+
snapshot: ViewSnapshot,
|
|
228
|
+
allIssues: IntegrationIssue[],
|
|
229
|
+
): ServiceResolutionDetail {
|
|
230
|
+
const contracts = evaluationContracts(snapshot);
|
|
231
|
+
const supplies = contracts.buildSupplyLookup(snapshot.catalog as never);
|
|
232
|
+
const cfg = snapshot.integrations as { instances?: unknown; defaultInstanceId?: unknown } | null;
|
|
233
|
+
const normalized = contracts.normalizeInstances({
|
|
234
|
+
instances: cfg?.instances,
|
|
235
|
+
defaultInstanceId: cfg?.defaultInstanceId,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// P4 枚举规则:root 注册表 id 逐一 + 显式 '*' 条目(若在);无注册表且节点通配激活 → 单 '*' 合成;
|
|
239
|
+
// 无注册表无节点 → []
|
|
240
|
+
const declaredIds = normalized.instances.map((i) => i.id);
|
|
241
|
+
const slot = (snapshot.integrations as { services?: Record<string, { instances?: Record<string, unknown> }> } | null)
|
|
242
|
+
?.services?.[service];
|
|
243
|
+
const slotKeys = slot?.instances ? Object.keys(slot.instances) : undefined;
|
|
244
|
+
let instanceIds: string[];
|
|
245
|
+
if (declaredIds.length > 0) {
|
|
246
|
+
instanceIds = [...declaredIds];
|
|
247
|
+
if (slotKeys?.includes('*')) instanceIds.push('*');
|
|
248
|
+
} else if (slotKeys !== undefined && slotKeys.length > 0) {
|
|
249
|
+
instanceIds = slotKeys.includes('*') ? ['*'] : slotKeys;
|
|
250
|
+
} else {
|
|
251
|
+
instanceIds = [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 默认实例口径(摘要 status = 默认实例求值——不重复头部;原 defaultEval 死代码已删 FX-2c)
|
|
255
|
+
|
|
256
|
+
const instances: ResolvedInstance[] = instanceIds.map((id) => {
|
|
257
|
+
const evalResult = evaluateInstance(snapshot, contracts, service, id, supplies);
|
|
258
|
+
const echo = echoForRef(snapshot.appDir, evalResult.effective?.credentialRef);
|
|
259
|
+
return {
|
|
260
|
+
instanceId: id,
|
|
261
|
+
status: evalResult.status,
|
|
262
|
+
...(evalResult.statusMessage ? { statusMessage: evalResult.statusMessage } : {}),
|
|
263
|
+
...(evalResult.effective ? { effective: evalResult.effective } : {}),
|
|
264
|
+
...(echo ? { credential: echo } : {}),
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// inheritance 四层(app → module → service → instance——effective 链声明位投影)
|
|
269
|
+
const inheritance = buildInheritance(snapshot, service);
|
|
270
|
+
|
|
271
|
+
const summary = resolveServiceSummary(service, snapshot);
|
|
272
|
+
const resolution: ServiceResolution = {
|
|
273
|
+
...summary,
|
|
274
|
+
instances,
|
|
275
|
+
inheritance,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
contracts: toContractsResolution(snapshot.contracts),
|
|
280
|
+
resolution,
|
|
281
|
+
issues: allIssues.filter((i) => i.service === service),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** inheritance 投影:四层各取该层声明 provider/implementation(缺席层 = 继承位,provider 缺席)。
|
|
286
|
+
* 实例层 = **默认实例口径**(FX-2c 修——与摘要 status 同轴:normalizeInstances 委托解析
|
|
287
|
+
* defaultInstanceId ?? '*';原实现取 Object.keys[0] 任意实例)。 */
|
|
288
|
+
function buildInheritance(
|
|
289
|
+
snapshot: ViewSnapshot,
|
|
290
|
+
service: string,
|
|
291
|
+
): ServiceResolution['inheritance'] {
|
|
292
|
+
const integrations = snapshot.integrations as {
|
|
293
|
+
provider?: string;
|
|
294
|
+
domains?: Record<string, { provider?: string }>;
|
|
295
|
+
services?: Record<string, { provider?: unknown; implementation?: string; instances?: Record<string, { provider?: string; implementation?: string }> }>;
|
|
296
|
+
} | null;
|
|
297
|
+
const domain = domainOfService(service);
|
|
298
|
+
const slot = integrations?.services?.[service];
|
|
299
|
+
const cfg = snapshot.integrations as { instances?: unknown; defaultInstanceId?: unknown } | null;
|
|
300
|
+
const normalized = evaluationContracts(snapshot).normalizeInstances({
|
|
301
|
+
instances: cfg?.instances,
|
|
302
|
+
defaultInstanceId: cfg?.defaultInstanceId,
|
|
303
|
+
});
|
|
304
|
+
const defaultInstanceId = normalized.defaultInstanceId ?? '*';
|
|
305
|
+
const defaultInstance = slot?.instances?.[defaultInstanceId] ?? slot?.instances?.['*'];
|
|
306
|
+
return [
|
|
307
|
+
{ layer: 'app' as const, ...(integrations?.provider ? { provider: integrations.provider } : {}) },
|
|
308
|
+
{ layer: 'module' as const, ...(integrations?.domains?.[domain]?.provider ? { provider: integrations.domains[domain].provider } : {}) },
|
|
309
|
+
{
|
|
310
|
+
layer: 'service' as const,
|
|
311
|
+
...(typeof slot?.provider === 'string' ? { provider: slot.provider } : {}),
|
|
312
|
+
...(slot?.implementation ? { implementation: slot.implementation } : {}),
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
layer: 'instance' as const,
|
|
316
|
+
...(defaultInstance?.provider ? { provider: defaultInstance.provider } : {}),
|
|
317
|
+
...(defaultInstance?.implementation ? { implementation: defaultInstance.implementation } : {}),
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** 谓词全量 issues 装配(写路径/求值轴共用——工厂经 evaluateIntegrationServices 装配后传入) */
|
|
323
|
+
export type { IntegrationIssue };
|