@tbox.cn/app-toolkit 0.1.0 → 0.3.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/{chunk-VV3ERZXS.js → chunk-KPD3LUH4.js} +1 -1
- package/dist/contracts-resolver-E4SECBOG.js +1 -0
- package/dist/index.d.ts +234 -43
- package/dist/index.js +8 -8
- package/package.json +2 -2
- package/src/assembly/app-manifest.ts +6 -0
- package/src/assembly/provider-catalog.ts +6 -1
- package/src/assembly/walk-manifests.ts +2 -1
- package/src/core/contracts-resolver.ts +3 -2
- package/src/core/credential-format.ts +4 -2
- package/src/core/errors.ts +9 -3
- package/src/dto.ts +127 -15
- package/src/factory.ts +129 -16
- package/src/index.ts +15 -4
- package/src/integrations/credentials.ts +1 -1
- package/src/integrations/domain-binding.ts +81 -0
- package/src/integrations/predicate.ts +7 -6
- package/src/integrations/read.ts +23 -3
- package/src/integrations/write.ts +93 -34
- package/src/views/app.ts +7 -5
- package/src/views/context.ts +3 -6
- package/src/views/credential-types.ts +28 -0
- package/src/views/credentials.ts +57 -0
- package/src/views/integration-schemas.ts +226 -0
- package/src/views/modules.ts +33 -14
- package/src/views/providers.ts +37 -22
- package/src/views/service-detail.ts +3 -4
- package/src/views/service-resolutions.ts +31 -25
- package/tests/credentials-view.test.ts +152 -0
- package/tests/demo-app.ts +14 -1
- package/tests/file-cache.test.ts +3 -1
- package/tests/naming-alignment.test.ts +8 -5
- package/tests/vendor-schema-keywords.test.ts +101 -0
- package/tests/views.test.ts +1285 -13
- package/tests/write-core.test.ts +107 -8
- package/dist/contracts-resolver-QM4FBQQV.js +0 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { IntegrationFieldRegistryView, LayerIntegrationSchemas, ServiceIntegrationSchemas } from '../dto.js';
|
|
2
|
+
import { isSupplied, type ViewSnapshot } from './context.js';
|
|
3
|
+
import { resolveServiceConfigSchema } from './providers.js';
|
|
4
|
+
import { domainOfService, moduleDomainWritable, providerFootprint, resolveDomainBinding } from '../integrations/domain-binding.js';
|
|
5
|
+
import { isControlledInlineProvider } from '../integrations/predicate.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 层级/服务级 config schema 计算视图(v4.4 D32——静态轴富化,零 contracts 求值;v4.7 D38 服务级)。
|
|
9
|
+
* config 表单结构四形态同源(P10):槽级组合单体(providers.ts)/ 层级内嵌(本文件——
|
|
10
|
+
* /app、/modules/:m include)/ 供给轴批量(providers.ts include——切换态)/ 服务级生效绑定
|
|
11
|
+
* (本文件 loadServiceIntegrationSchemas——/services/:s include)。
|
|
12
|
+
*
|
|
13
|
+
* 计算规则(D32 裁决):禁交集——config 五层浅合并下「部分服务认识的字段」合法(谓词 #7
|
|
14
|
+
* 并集键集同依据);禁合成单一 schema——required/title 冲突的有损代数;产物 = 扁平映射,
|
|
15
|
+
* 分组归客户端纯呈现。层级 schema = 表单引导**非写入门禁**(P16——写路径校验权威恒在谓词
|
|
16
|
+
* #7 键集 + ajv 凭据链;root 级口径差:#7 = 引用厂商并集,表单 = 层生效厂商单厂商作用域)。
|
|
17
|
+
*
|
|
18
|
+
* v4.6(D36/D37 additive 精化):**原始投影**(loadApp/loadModuleIntegrationSchemas)原样保留
|
|
19
|
+
* 为诊断面 / registry 输入;**消费投影** = buildIntegrationFieldRegistry(字段级合并代数
|
|
20
|
+
* R1/R2/R3 服务端单源,conflict 折叠 schema:null);**轴向假想绑定** = formProviderOf
|
|
21
|
+
* (override ?? 实际解析——双投影同源同口径,formViews 装配层一次计算两投影)。
|
|
22
|
+
* 单源三锚:层级 schema 计算 = resolveLayerIntegrationSchemas / 生效厂商解析 = formProviderOf /
|
|
23
|
+
* 合并代数 = buildIntegrationFieldRegistry。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** 核心纯函数:显式作用域(provider null → 整字段 null;services 逐项 resolveServiceConfigSchema
|
|
27
|
+
* 复合解析——v4.7 D38 收口,零新读取路径;供给在场未声明 = null,mock 合法) */
|
|
28
|
+
export function resolveLayerIntegrationSchemas(snapshot: ViewSnapshot, provider: string, services: readonly string[]): LayerIntegrationSchemas;
|
|
29
|
+
export function resolveLayerIntegrationSchemas(snapshot: ViewSnapshot, provider: string | null, services: readonly string[]): LayerIntegrationSchemas | null;
|
|
30
|
+
export function resolveLayerIntegrationSchemas(
|
|
31
|
+
snapshot: ViewSnapshot,
|
|
32
|
+
provider: string | null,
|
|
33
|
+
services: readonly string[],
|
|
34
|
+
): LayerIntegrationSchemas | null {
|
|
35
|
+
if (provider === null) return null;
|
|
36
|
+
const out: LayerIntegrationSchemas['services'] = {};
|
|
37
|
+
for (const s of services) {
|
|
38
|
+
out[s] = { configSchema: resolveServiceConfigSchema(snapshot, provider, s) };
|
|
39
|
+
}
|
|
40
|
+
return { provider, services: out };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── v4.6 D36/D37:合并代数助手(内联——深相等判定基)──
|
|
44
|
+
|
|
45
|
+
/** 稳定序列化(键序归一——R2 深相等 / R1 签名比较统一基;数组保序、对象键 sort) */
|
|
46
|
+
function stableSerialize(v: unknown): string {
|
|
47
|
+
if (v === null || typeof v !== 'object') return JSON.stringify(v ?? null) as string;
|
|
48
|
+
if (Array.isArray(v)) return `[${v.map(stableSerialize).join(',')}]`;
|
|
49
|
+
const keys = Object.keys(v as Record<string, unknown>).sort();
|
|
50
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableSerialize((v as Record<string, unknown>)[k])}`).join(',')}}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** enum 缺席哨兵(≠ 任何实际集签名——混布必不等) */
|
|
54
|
+
const NO_ENUM = '\u0000no-enum';
|
|
55
|
+
|
|
56
|
+
/** enum 集签名(去重排序——序不敏感) */
|
|
57
|
+
function enumSig(sub: Record<string, unknown>): string {
|
|
58
|
+
const e = sub.enum;
|
|
59
|
+
if (!Array.isArray(e)) return NO_ENUM;
|
|
60
|
+
return `[${[...new Set(e.map(stableSerialize))].sort().join(',')}]`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 顶层 type 签名(数组形态序敏感——严格全等);非对象(含布尔子 schema)/ type 缺席 → null(R3) */
|
|
64
|
+
function typeSig(sub: unknown): string | null {
|
|
65
|
+
if (!sub || typeof sub !== 'object' || Array.isArray(sub)) return null;
|
|
66
|
+
const t = (sub as Record<string, unknown>).type;
|
|
67
|
+
return t === undefined ? null : stableSerialize(t);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** D36 合并代数单点(R1/R2/R3——唯一扩展点;保守降级:不可调和 → null,客户端 RawJson 保真) */
|
|
71
|
+
function mergeFieldSources(sources: ReadonlyArray<{ subschema: unknown }>): object | null {
|
|
72
|
+
const sigs = sources.map((s) => typeSig(s.subschema));
|
|
73
|
+
if (sigs.some((s) => s === null) || new Set(sigs).size > 1) return null; // R3:非对象/type 缺席/type 不等
|
|
74
|
+
if (sigs[0] === '"object"') {
|
|
75
|
+
// R2:全源稳定序列化深相等 → 首源;异构 → null
|
|
76
|
+
const first = stableSerialize(sources[0].subschema);
|
|
77
|
+
return sources.every((s) => stableSerialize(s.subschema) === first) ? (sources[0].subschema as object) : null;
|
|
78
|
+
}
|
|
79
|
+
// R1:标量型——enum 集全等(含双无哨兵)→ 首源;不等/混布 → null
|
|
80
|
+
const es = new Set(sources.map((s) => enumSig(s.subschema as Record<string, unknown>)));
|
|
81
|
+
return es.size === 1 ? (sources[0].subschema as object) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** D36 registry 构造(纯函数——消费 resolveLayerIntegrationSchemas 产物,禁旁路直取 serviceSchemas)。
|
|
85
|
+
* fields 收集零过滤(布尔/非对象子 schema 入列,判定全权归 mergeFieldSources——S8 互证等式由
|
|
86
|
+
* 收集定义结构性闭合);schemaless 仅 configSchema === null(布尔/畸形整 schema 防御跳过——
|
|
87
|
+
* 不入 schemaless、零参与聚合;D32 运行时畸形,非契约面)。 */
|
|
88
|
+
export function buildIntegrationFieldRegistry(
|
|
89
|
+
layer: LayerIntegrationSchemas,
|
|
90
|
+
o?: { unsupplied?: string[] },
|
|
91
|
+
): IntegrationFieldRegistryView {
|
|
92
|
+
const schemaless: string[] = [];
|
|
93
|
+
let additionalPropertiesAllowed = true;
|
|
94
|
+
const buckets = new Map<string, Array<{ service: string; subschema: unknown; required: boolean }>>();
|
|
95
|
+
for (const [service, entry] of Object.entries(layer.services)) {
|
|
96
|
+
// 插入序 = scope 遍历序(app 字母序 / module demand 序)——fields 首声明序由此保证
|
|
97
|
+
const schema = entry.configSchema;
|
|
98
|
+
if (schema === null) {
|
|
99
|
+
schemaless.push(service);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) continue;
|
|
103
|
+
const s = schema as Record<string, unknown>;
|
|
104
|
+
if (s.additionalProperties === false) additionalPropertiesAllowed = false;
|
|
105
|
+
const props = s.properties;
|
|
106
|
+
if (!props || typeof props !== 'object' || Array.isArray(props)) continue; // ≡ 谓词 collectPropertyKeys 守卫
|
|
107
|
+
const required = Array.isArray(s.required) ? s.required : [];
|
|
108
|
+
for (const key of Object.keys(props)) {
|
|
109
|
+
const bucket = buckets.get(key) ?? [];
|
|
110
|
+
bucket.push({ service, subschema: (props as Record<string, unknown>)[key], required: required.includes(key) });
|
|
111
|
+
buckets.set(key, bucket);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const fields: IntegrationFieldRegistryView['fields'] = [];
|
|
115
|
+
for (const [key, sources] of buckets) {
|
|
116
|
+
fields.push({
|
|
117
|
+
key,
|
|
118
|
+
schema: mergeFieldSources(sources),
|
|
119
|
+
sources: sources.map(({ service, required }) => ({ service, required })),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
provider: layer.provider,
|
|
124
|
+
schemaless,
|
|
125
|
+
unsupplied: [...(o?.unsupplied ?? [])],
|
|
126
|
+
additionalPropertiesAllowed,
|
|
127
|
+
fields,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** D37 生效厂商解析单源(实际/假想同径):override(非空串)?? 实际解析。
|
|
132
|
+
* 空串归一(I3)单点;module 轴 D34 不可写/不在册 → null(override 不豁免——预览服务于保存)。 */
|
|
133
|
+
export function formProviderOf(
|
|
134
|
+
axis: 'app' | 'module',
|
|
135
|
+
snapshot: ViewSnapshot,
|
|
136
|
+
moduleId?: string,
|
|
137
|
+
override?: string,
|
|
138
|
+
): string | null {
|
|
139
|
+
const ovr = typeof override === 'string' && override.length > 0 ? override : undefined;
|
|
140
|
+
if (axis === 'app') {
|
|
141
|
+
if (ovr !== undefined) return ovr;
|
|
142
|
+
const p = (snapshot.integrations as { provider?: unknown } | null)?.provider;
|
|
143
|
+
return typeof p === 'string' && p.length > 0 ? p : null;
|
|
144
|
+
}
|
|
145
|
+
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
146
|
+
if (!mod) return null; // 视图已先行 404,此处防御
|
|
147
|
+
const w = moduleDomainWritable(snapshot.walk, mod);
|
|
148
|
+
if (!w.writable) return null; // D34 不豁免
|
|
149
|
+
if (ovr !== undefined) return ovr;
|
|
150
|
+
return resolveDomainBinding(snapshot.integrations, w.domainKey).provider;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** D36/D37 表单层双投影(I1:同源同口径一次计算——双取单算 by construction) */
|
|
154
|
+
export interface LayerFormViews {
|
|
155
|
+
integrationSchemas: LayerIntegrationSchemas | null;
|
|
156
|
+
integrationFieldRegistry: IntegrationFieldRegistryView | null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 防御双 null(module 不在册/不可写/未绑定、app 未绑定——F3) */
|
|
160
|
+
const NULL_FORM_VIEWS: LayerFormViews = { integrationSchemas: null, integrationFieldRegistry: null };
|
|
161
|
+
|
|
162
|
+
/** app 轴装配:scope = 供给足迹全量(≠ AppView.services——root.config 流向包括模块服务在内的所有服务);
|
|
163
|
+
* unsupplied 恒 [](结构性:足迹 ⊆ 供给)。未知厂商/零足迹 → 非 null 空集(镜像 D32 mock 语义)。 */
|
|
164
|
+
export function loadAppFormViews(snapshot: ViewSnapshot, appProvider?: string): LayerFormViews {
|
|
165
|
+
const provider = formProviderOf('app', snapshot, undefined, appProvider);
|
|
166
|
+
if (provider === null) return NULL_FORM_VIEWS;
|
|
167
|
+
const layer = resolveLayerIntegrationSchemas(snapshot, provider, providerFootprint(snapshot.catalog, provider));
|
|
168
|
+
return { integrationSchemas: layer, integrationFieldRegistry: buildIntegrationFieldRegistry(layer) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** module 轴装配:仅 writable 单域(D34);scope = demand ∩ 该厂商供给;unsupplied 同点派生
|
|
172
|
+
* (demand − 供给,Set 去重保序——manifest demand 不去重防御)。 */
|
|
173
|
+
export function loadModuleFormViews(snapshot: ViewSnapshot, moduleId: string, moduleProvider?: string): LayerFormViews {
|
|
174
|
+
const provider = formProviderOf('module', snapshot, moduleId, moduleProvider);
|
|
175
|
+
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
176
|
+
if (provider === null || !mod) return NULL_FORM_VIEWS;
|
|
177
|
+
const demand = (mod.descriptor.contributes.services ?? []).map((s) => s.service);
|
|
178
|
+
const supplied = (s: string): boolean => isSupplied(snapshot.catalog, provider, s);
|
|
179
|
+
const layer = resolveLayerIntegrationSchemas(snapshot, provider, demand.filter(supplied));
|
|
180
|
+
return {
|
|
181
|
+
integrationSchemas: layer,
|
|
182
|
+
integrationFieldRegistry: buildIntegrationFieldRegistry(layer, {
|
|
183
|
+
unsupplied: [...new Set(demand.filter((s) => !supplied(s)))],
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** app 级 loader(v4.6 委托化——签名零改动,公开面保持;假想态归 loadAppFormViews override 参) */
|
|
189
|
+
export function loadAppIntegrationSchemas(snapshot: ViewSnapshot): LayerIntegrationSchemas | null {
|
|
190
|
+
return loadAppFormViews(snapshot).integrationSchemas;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** module 级 loader(v4.6 委托化——同上;多域/零域/共享键/未绑定 → 整字段 null,表单降级 = ExtraFields) */
|
|
194
|
+
export function loadModuleIntegrationSchemas(snapshot: ViewSnapshot, moduleId: string): LayerIntegrationSchemas | null {
|
|
195
|
+
return loadModuleFormViews(snapshot, moduleId).integrationSchemas;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 服务级 loader(v4.7 D38):四分支穷举(node.provider = 文件 services[s].provider 逐字原值;
|
|
199
|
+
* 实例层无条件排除——node 级口径);非空三分支归约一次复合解析。
|
|
200
|
+
* ① undefined/'' → resolveDomainBinding 域级联(域 + root 双缺 → 整字段 null)
|
|
201
|
+
* ② 非空 string → bare:catalog miss(未知厂商/未供给)→ configSchema:null 不 404(富化非门控)
|
|
202
|
+
* ③ 受控内联(isControlledInlineProvider 单源)→ 名 = p.provider;catalog 复合键 > p.configSchema ref
|
|
203
|
+
* (H9 逃逸 400 照抛——readInlineSchema 共享路径)
|
|
204
|
+
* ④ 其余(null/数组/原始值/坏对象)→ 整字段 null(≡ 谓词 #5 INLINE_PROVIDER_INVALID 同判——
|
|
205
|
+
* 不放行域级联,防错误 schema 污染坏节点) */
|
|
206
|
+
export function loadServiceIntegrationSchemas(
|
|
207
|
+
snapshot: ViewSnapshot,
|
|
208
|
+
service: string,
|
|
209
|
+
): ServiceIntegrationSchemas | null {
|
|
210
|
+
const p = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
|
|
211
|
+
?.services?.[service]?.provider;
|
|
212
|
+
if (p === undefined || p === '') {
|
|
213
|
+
const binding = resolveDomainBinding(snapshot.integrations, domainOfService(service));
|
|
214
|
+
return binding.provider === null
|
|
215
|
+
? null
|
|
216
|
+
: { provider: binding.provider, configSchema: resolveServiceConfigSchema(snapshot, binding.provider, service) };
|
|
217
|
+
}
|
|
218
|
+
if (typeof p === 'string') {
|
|
219
|
+
return { provider: p, configSchema: resolveServiceConfigSchema(snapshot, p, service) };
|
|
220
|
+
}
|
|
221
|
+
if (isControlledInlineProvider(p)) {
|
|
222
|
+
const decl = p as { provider: string; configSchema?: string };
|
|
223
|
+
return { provider: decl.provider, configSchema: resolveServiceConfigSchema(snapshot, decl.provider, service, decl.configSchema) };
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
package/src/views/modules.ts
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
|
-
import type { ModuleDeclaration, ModuleDetailView, ServiceDemand } from '../dto.js';
|
|
1
|
+
import type { ModuleDeclaration, ModuleDetailView, ProviderBinding, ServiceDemand } from '../dto.js';
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { moduleDomainWritable, resolveDomainBinding } from '../integrations/domain-binding.js';
|
|
4
|
+
import type { ViewSnapshot } from './context.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 需求轴/模块静态投影(views/modules;C4)。静态端点结构性零求值(铁律 #3——
|
|
7
8
|
* 无 resolution/issues/contracts 键,永不 503)。
|
|
9
|
+
* v4.4 D34:domainKeys/providerBindings/configEditable 显式化;integration 仅单域
|
|
10
|
+
* (writable)时在场——多域/零域/共享键无单键落点;多域回退兜底键机器拆除
|
|
11
|
+
* (派生统一 = domainOfService 去重集合,moduleDomainWritable 单源)。
|
|
12
|
+
* 2026-09-16 双缺陷批:loadModulesView 排除 provider 家族包(供给/实施载体非可配置
|
|
13
|
+
* 业务模块)——walk 声明集与 loadModuleDetailView 不变(装包仍合法,详情保持 200)。
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
|
-
/**
|
|
16
|
+
/** 家族名归一(cli deriveModuleId 同源语义——依赖方向禁 import,故内联):
|
|
17
|
+
* `@tbox.cn/app-provider-x` / `@app/provider-y` / `provider-z` → `provider-*` */
|
|
18
|
+
function familyName(s: string): string {
|
|
19
|
+
return s.replace(/^@tbox\.cn\/app-/, '').replace(/^@[^/]+\//, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** provider 家族包判定(模块清单排除供给/实施载体;生态三先例同源:verify-cli 排除判定 /
|
|
23
|
+
* guard-vendor-neutrality「provider-* 包」/ cli deriveModuleId 家族推导)。install id 与
|
|
24
|
+
* 包名 basename 双查 = 防 .tbox/app.json 登记漂移纵深;命名而非结构(provider-custom 零贡献
|
|
25
|
+
* 空壳与「零贡献合法」形态不可结构区分);未来收敛 = manifest 显式角色标记(届时整体退场)。 */
|
|
26
|
+
function isProviderFamily(mod: { id: string; pkg: string }): boolean {
|
|
27
|
+
return familyName(mod.id).startsWith('provider-') || familyName(mod.pkg).startsWith('provider-');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** GET /modules:纯模块清单(demand 不在——卡片展开 = GET /modules/:m;provider 家族包不在清单) */
|
|
11
31
|
export function loadModulesView(snapshot: ViewSnapshot): { modules: ModuleDeclaration[] } {
|
|
12
32
|
const modules: ModuleDeclaration[] = snapshot.walk.modules
|
|
33
|
+
.filter((mod) => !isProviderFamily(mod))
|
|
13
34
|
.map((mod) => ({
|
|
14
35
|
id: mod.id,
|
|
15
36
|
name: mod.descriptor.name,
|
|
@@ -22,12 +43,6 @@ export function loadModulesView(snapshot: ViewSnapshot): { modules: ModuleDeclar
|
|
|
22
43
|
return { modules };
|
|
23
44
|
}
|
|
24
45
|
|
|
25
|
-
/** 域键换算(模块服务集派生域键集;单域 = 该域,多域/零域 = 模块 id——与 write.moduleDomainKey 同约定) */
|
|
26
|
-
export function moduleDomainKeyOf(services: ServiceDemand[], moduleId: string): string {
|
|
27
|
-
const domainKeys = new Set(services.map((s) => domainOfService(s.service)));
|
|
28
|
-
return domainKeys.size === 1 ? [...domainKeys][0] : moduleId;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
46
|
/** GET /modules/:module:模块静态一站式(integration 节点原值 + demand;PUT 读对偶) */
|
|
32
47
|
export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string): ModuleDetailView {
|
|
33
48
|
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
@@ -38,11 +53,15 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
|
|
|
38
53
|
service: need.service,
|
|
39
54
|
...(need.title ? { title: need.title } : {}),
|
|
40
55
|
...(need.description ? { description: need.description } : {}),
|
|
41
|
-
required: need.optional
|
|
56
|
+
required: need.optional === false,
|
|
42
57
|
}));
|
|
58
|
+
const w = moduleDomainWritable(snapshot.walk, mod);
|
|
43
59
|
const domains = (snapshot.integrations as { domains?: Record<string, unknown> } | null)?.domains ?? {};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
60
|
+
// integration = domains[换算键] 节点原值(D21 编辑预填源);未配置 = null;多域/零域/共享键 = null(D34)
|
|
61
|
+
const integration = w.writable ? ((domains[w.domainKey] as ModuleDetailView['integration']) ?? null) : null;
|
|
62
|
+
const providerBindings: ProviderBinding[] = w.domainKeys.map((d) => {
|
|
63
|
+
const binding = resolveDomainBinding(snapshot.integrations, d);
|
|
64
|
+
return { domain: d, effective: binding.provider, layer: binding.layer };
|
|
65
|
+
});
|
|
66
|
+
return { domainKeys: [...w.domainKeys], integration, providerBindings, configEditable: w.writable, services };
|
|
48
67
|
}
|
package/src/views/providers.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { ProviderDetail, ProviderEntry, ProviderServiceDetail, ProvidersVie
|
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
3
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
-
import { walkManifests } from '../assembly/walk-manifests.js';
|
|
6
5
|
import type { ViewSnapshot } from './context.js';
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -27,8 +26,9 @@ export function loadProvidersView(snapshot: ViewSnapshot): ProvidersView {
|
|
|
27
26
|
return { providers: providers.sort((a, b) => a.provider.localeCompare(b.provider)) };
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
/** GET /providers/:provider:供给足迹(implementations 恒数组——H1
|
|
31
|
-
|
|
29
|
+
/** GET /providers/:provider:供给足迹(implementations 恒数组——H1 多实现钉版数据源;
|
|
30
|
+
* v4.4 F5:include=integrationSchemas 切换态批量——services[s] += { configSchema }(纯 config)) */
|
|
31
|
+
export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string, include?: ReadonlySet<string>): ProviderDetail {
|
|
32
32
|
const impls = snapshot.catalog.providers[provider];
|
|
33
33
|
if (!impls) {
|
|
34
34
|
throw new AppToolkitError('PROVIDER_NOT_FOUND', 404, `厂商 ${provider} 不 ∈ catalog(内联声明厂商无包级档案——身份与表单经组合端点暴露)`);
|
|
@@ -40,6 +40,11 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string)
|
|
|
40
40
|
bucket.implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
if (include?.has('integrationSchemas')) {
|
|
44
|
+
for (const [service, bucket] of Object.entries(services)) {
|
|
45
|
+
bucket.integrationSchemas = { configSchema: resolveServiceConfigSchema(snapshot, provider, service) };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
43
48
|
const entries = Object.values(impls);
|
|
44
49
|
return {
|
|
45
50
|
provider,
|
|
@@ -87,33 +92,43 @@ export function loadProviderServiceDetailView(
|
|
|
87
92
|
implementations.push({ implementation: configured[service]?.implementation ?? `${provider}-inline@1`, credentialType: inlineDeclaration.credentialType });
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
// integrationSchemas:
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
}
|
|
95
|
+
// integrationSchemas:schema 解析收口 resolveServiceConfigSchema 单源(v4.7 D38——catalog 复合键 >
|
|
96
|
+
// 内联 ref,等价重构 O6/O7/O15 零改动即证);内联 ref 来源 = 节点声明(provider.configSchema,
|
|
97
|
+
// 仅受控内联声明匹配本厂商名时);路径约束 H9——resolve 落 config/schemas/ 内 + realpath 复核,违例 400
|
|
98
|
+
const configSchema = resolveServiceConfigSchema(
|
|
99
|
+
snapshot,
|
|
100
|
+
provider,
|
|
101
|
+
service,
|
|
102
|
+
inlineDeclaration ? (inline as { configSchema?: string } | undefined)?.configSchema : undefined,
|
|
103
|
+
);
|
|
106
104
|
|
|
107
105
|
return {
|
|
108
106
|
provider,
|
|
109
107
|
service,
|
|
110
108
|
implementations,
|
|
111
|
-
integrationSchemas: { configSchema
|
|
109
|
+
integrationSchemas: { configSchema },
|
|
112
110
|
};
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
/**
|
|
116
|
-
|
|
113
|
+
/** per-(provider, service) schema 复合解析单源(v4.7 D38):catalog 复合键(多 impl 声明序首个)
|
|
114
|
+
* > 内联 ref(readInlineSchema 保持私有——H9 双钉子不扩暴露);miss / 未声明 / 文件缺失 → null。
|
|
115
|
+
* views 层 \u0000 复合键唯一消费点(组合单体 / 供给轴批量 / 层级核心 / 服务级 include 同源)。
|
|
116
|
+
* (D36 registry 批若需跨包消费再升 index 导出——additive 零成本) */
|
|
117
|
+
export function resolveServiceConfigSchema(
|
|
118
|
+
snapshot: ViewSnapshot,
|
|
119
|
+
provider: string,
|
|
120
|
+
service: string,
|
|
121
|
+
inlineRef?: string,
|
|
122
|
+
): object | null {
|
|
123
|
+
const entry = snapshot.walk.serviceSchemas[`${provider}\u0000${service}`];
|
|
124
|
+
if (entry) return readSchemaRef(snapshot.appDir, entry.packageDir, entry.configSchema);
|
|
125
|
+
if (typeof inlineRef === 'string') return readInlineSchema(snapshot.appDir, inlineRef);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** catalog schema 引用读取(相对包根;缺席 → null——SCHEMA_FILE_MISSING 归谓词 #6)。
|
|
130
|
+
* v4.4 导出共享:层级内嵌(integration-schemas.ts)/ 凭据结构清单(credential-types.ts)同源消费。 */
|
|
131
|
+
export function readSchemaRef(_appDir: string, packageDir: string, ref: string | undefined): object | null {
|
|
117
132
|
if (!ref) return null;
|
|
118
133
|
const file = join(packageDir, ref);
|
|
119
134
|
if (!existsSync(file)) return null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ServiceDetail } from '../dto.js';
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
|
+
import { isKnownService } from '../integrations/read.js';
|
|
3
4
|
import { suppliedByVendors, type ViewSnapshot } from './context.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -10,10 +11,8 @@ import { suppliedByVendors, type ViewSnapshot } from './context.js';
|
|
|
10
11
|
export function loadServiceDetailView(snapshot: ViewSnapshot, service: string): ServiceDetail {
|
|
11
12
|
const configured = (snapshot.integrations as { services?: Record<string, unknown> } | null)?.services ?? {};
|
|
12
13
|
const demand = snapshot.catalog.services[service];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// 404 判据与 /service-resolutions/:service 单源(P9)
|
|
16
|
-
if (!inVocabulary && !isConfigured) {
|
|
14
|
+
// 404 判据与写路径单源(P9——A2 isKnownService;词汇成员零 demand/供给/config 仍 200)
|
|
15
|
+
if (!isKnownService(snapshot.walk.vocabulary, Object.keys(configured), service)) {
|
|
17
16
|
throw new AppToolkitError('SERVICE_NOT_FOUND', 404, `服务 ${service} 不在服务词汇(已装契约包 ∪ 已配置)`);
|
|
18
17
|
}
|
|
19
18
|
// integration = 文件 services[s] 逐字原值(编辑预填源 D21);无声明 = null
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BindingLayer,
|
|
3
3
|
ContractsResolution,
|
|
4
|
-
CredentialEcho,
|
|
5
4
|
EffectiveBinding,
|
|
6
5
|
IntegrationIssue,
|
|
7
6
|
ResolvedInstance,
|
|
@@ -11,13 +10,15 @@ import type {
|
|
|
11
10
|
ServiceResolutionDetail,
|
|
12
11
|
ServiceStatus,
|
|
13
12
|
} from '../dto.js';
|
|
14
|
-
import {
|
|
13
|
+
import { AppToolkitError } from '../core/errors.js';
|
|
14
|
+
import { resolveDomainBinding } from '../integrations/domain-binding.js';
|
|
15
15
|
import type { ResolvedContracts } from '../core/contracts-resolver.js';
|
|
16
16
|
import type { WalkCatalog } from '../assembly/walk-manifests.js';
|
|
17
17
|
import { domainOfService, isSupplied, type ViewSnapshot } from './context.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* 求值轴投影(views/service-resolutions;C4;FX-2c——B6 effective
|
|
20
|
+
* 求值轴投影(views/service-resolutions;C4;FX-2c——B6 effective 填充 + F2 layer 推导;
|
|
21
|
+
* v4.4——D31 Entry.layer 双缺席语义 + D33 凭据内嵌删净(求值路径零凭据文件 I/O))。
|
|
21
22
|
*
|
|
22
23
|
* status(service, instanceId) = contracts.resolveEffectiveService(integrations, service, instanceId,
|
|
23
24
|
* supplies).status——动态(应用自身 contracts 求值单源)。
|
|
@@ -26,9 +27,9 @@ import { domainOfService, isSupplied, type ViewSnapshot } from './context.js';
|
|
|
26
27
|
* required(service) = 任一需求方 required(OR 聚合——demandedServices.optional AND 合并的补语义;
|
|
27
28
|
* 平台/孤儿恒 false)——集成就绪判定单源:pending = Σ (required && status !== 'ok')。
|
|
28
29
|
* **P14 单一形状(结构保证)**:resolveServiceSummary() 单源函数——/service-resolutions map 值 ≡
|
|
29
|
-
* /service-resolutions/:s 的 resolution 摘要基(ServiceResolution extends ServiceResolutionEntry
|
|
30
|
-
*
|
|
31
|
-
* +
|
|
30
|
+
* /service-resolutions/:s 的 resolution 摘要基(ServiceResolution extends ServiceResolutionEntry)
|
|
31
|
+
* ≡ /app 内嵌 map 值 ≡ /modules/:m 内嵌 map 值(v4.4 等价面四处)。
|
|
32
|
+
* **实例条目**:effective(合并绑定投影 + layer 推导——contracts EffectiveService 无 layer,F2)。
|
|
32
33
|
*/
|
|
33
34
|
|
|
34
35
|
function toContractsResolution(contracts: ResolvedContracts): ContractsResolution {
|
|
@@ -51,8 +52,9 @@ interface InstanceEvaluation {
|
|
|
51
52
|
|
|
52
53
|
/** 最特异声明层推导(F2):inst 位 provider 在场 → 'instance';槽级 → 'service';域级 → 'module';
|
|
53
54
|
* root → 'app'。实例位查找复刻 TIER0 语义(精确键优先、'*' 通配兜底——与 resolveEffectiveService
|
|
54
|
-
* 的 instances 查找层同轴)。
|
|
55
|
-
|
|
55
|
+
* 的 instances 查找层同轴)。v4.4 F1:域级段消费 resolveDomainBinding 单源;返回扩 null
|
|
56
|
+
* (四层全无 provider 声明 → null——Entry.layer「未配置双缺席」口径,D31)。 */
|
|
57
|
+
function deriveBindingLayer(snapshot: ViewSnapshot, service: string, instanceId: string): BindingLayer | null {
|
|
56
58
|
const integrations = snapshot.integrations as {
|
|
57
59
|
provider?: unknown;
|
|
58
60
|
domains?: Record<string, { provider?: unknown }>;
|
|
@@ -63,17 +65,9 @@ function deriveBindingLayer(snapshot: ViewSnapshot, service: string, instanceId:
|
|
|
63
65
|
const inst = slot?.instances?.[instanceId] ?? slot?.instances?.['*'];
|
|
64
66
|
if (inst && inst.provider !== undefined) return 'instance';
|
|
65
67
|
if (slot?.provider !== undefined) return 'service';
|
|
66
|
-
if (integrations
|
|
67
|
-
return 'app';
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
/** 凭据掩码回显(O4:GET 永无真值——maskCredentialValues 单源;文件缺席省略) */
|
|
71
|
-
function echoForRef(appDir: string, ref: string | undefined): CredentialEcho | undefined {
|
|
72
|
-
if (ref === undefined || !ref.startsWith('secret://')) return undefined;
|
|
73
|
-
const stem = ref.slice('secret://'.length);
|
|
74
|
-
const file = readCredentialFile(appDir, stem);
|
|
75
|
-
if (!file) return undefined;
|
|
76
|
-
return { type: file.type, ref, file: stemToFilePath(stem), masked: maskCredentialValues(file.values) };
|
|
68
|
+
if (resolveDomainBinding(integrations, domain).layer === 'module') return 'module';
|
|
69
|
+
if (typeof integrations?.provider === 'string' && integrations.provider.length > 0) return 'app';
|
|
70
|
+
return null;
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
/** 求值单实例(resolveEffectiveService 动态求值;ok 态携带完整 effective——layer 推导附) */
|
|
@@ -102,7 +96,8 @@ function evaluateInstance(
|
|
|
102
96
|
implementation: r.effective.implementation,
|
|
103
97
|
...(r.effective.config !== undefined ? { config: { ...(r.effective.config as Record<string, unknown>) } } : {}),
|
|
104
98
|
...(r.effective.credentialRef !== undefined ? { credentialRef: r.effective.credentialRef } : {}),
|
|
105
|
-
|
|
99
|
+
// ok 态可证非空(effective.provider 必有声明来源层)——null 兜底为防御性
|
|
100
|
+
layer: deriveBindingLayer(snapshot, service, instanceId) ?? 'app',
|
|
106
101
|
},
|
|
107
102
|
};
|
|
108
103
|
}
|
|
@@ -117,9 +112,11 @@ function evaluationContracts(snapshot: ViewSnapshot) {
|
|
|
117
112
|
normalizeInstances?: (input: unknown) => { instances: Array<{ id: string }>; defaultInstanceId: string | null };
|
|
118
113
|
buildSupplyLookup?: (catalog: unknown) => unknown;
|
|
119
114
|
};
|
|
120
|
-
// 门控(工厂边缘)保证 evaluation/strictValidation
|
|
115
|
+
// 门控(工厂边缘)保证 evaluation/strictValidation 后此断言恒真;A2:裸 Error →
|
|
116
|
+
// AppToolkitError 503(predicateIssues 的 config === null 早退绕过工厂 assertFn 后,
|
|
117
|
+
// 本函数是畸形包(严格面在场+求值面缺席)唯一后续拦截点——不再 500)
|
|
121
118
|
if (!mod.resolveEffectiveService || !mod.normalizeInstances) {
|
|
122
|
-
throw new
|
|
119
|
+
throw new AppToolkitError('CONTRACTS_NOT_RESOLVED', 503, 'contracts 求值面缺席(契约包形态异常)——重新安装或升级应用 @tbox.cn/app-contracts 后重试');
|
|
123
120
|
}
|
|
124
121
|
return mod as unknown as {
|
|
125
122
|
resolveEffectiveService: (
|
|
@@ -167,6 +164,10 @@ export function resolveServiceSummary(
|
|
|
167
164
|
// 跨实例求值异常聚合(statusMessage 直投)
|
|
168
165
|
const evaluation = evaluateInstance(snapshot, contracts, service, defaultInstanceId, supplies);
|
|
169
166
|
|
|
167
|
+
// v4.4 D31:layer = provider 字段来源声明层(默认实例口径,与 status 同轴);
|
|
168
|
+
// 未配置(provider 缺席)→ 双缺席——layer 单独在场即「无 provider 的孤儿层」,违 D31 口径
|
|
169
|
+
const layer = deriveBindingLayer(snapshot, service, defaultInstanceId);
|
|
170
|
+
|
|
170
171
|
// provider:effective 求值厂商(最特异声明层——跨层事实);缺席 = 未配置/未解析
|
|
171
172
|
const declaredProvider = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
|
|
172
173
|
?.services?.[service]?.provider;
|
|
@@ -193,6 +194,7 @@ export function resolveServiceSummary(
|
|
|
193
194
|
status: evaluation.status,
|
|
194
195
|
...(evaluation.statusMessage ? { statusMessage: evaluation.statusMessage } : {}),
|
|
195
196
|
...(provider !== undefined ? { provider } : {}),
|
|
197
|
+
...(provider !== undefined && layer !== null ? { layer } : {}),
|
|
196
198
|
...(supplied !== undefined ? { supplied } : {}),
|
|
197
199
|
...(module !== undefined ? { module } : {}),
|
|
198
200
|
...(required !== undefined ? { required } : {}),
|
|
@@ -255,13 +257,12 @@ export function loadServiceResolutionDetail(
|
|
|
255
257
|
|
|
256
258
|
const instances: ResolvedInstance[] = instanceIds.map((id) => {
|
|
257
259
|
const evalResult = evaluateInstance(snapshot, contracts, service, id, supplies);
|
|
258
|
-
|
|
260
|
+
// v4.4 D33:instances[].credential 删净(凭据掩码回显归 GET /credentials——求值路径零凭据文件 I/O)
|
|
259
261
|
return {
|
|
260
262
|
instanceId: id,
|
|
261
263
|
status: evalResult.status,
|
|
262
264
|
...(evalResult.statusMessage ? { statusMessage: evalResult.statusMessage } : {}),
|
|
263
265
|
...(evalResult.effective ? { effective: evalResult.effective } : {}),
|
|
264
|
-
...(echo ? { credential: echo } : {}),
|
|
265
266
|
};
|
|
266
267
|
});
|
|
267
268
|
|
|
@@ -303,9 +304,14 @@ function buildInheritance(
|
|
|
303
304
|
});
|
|
304
305
|
const defaultInstanceId = normalized.defaultInstanceId ?? '*';
|
|
305
306
|
const defaultInstance = slot?.instances?.[defaultInstanceId] ?? slot?.instances?.['*'];
|
|
307
|
+
// module 段消费 resolveDomainBinding 单源(v4.4 F1——与 ProviderBinding / 谓词 #7 同源)
|
|
308
|
+
const domainBinding = resolveDomainBinding(integrations as Record<string, unknown> | null, domain);
|
|
306
309
|
return [
|
|
307
310
|
{ layer: 'app' as const, ...(integrations?.provider ? { provider: integrations.provider } : {}) },
|
|
308
|
-
{
|
|
311
|
+
{
|
|
312
|
+
layer: 'module' as const,
|
|
313
|
+
...(domainBinding.layer === 'module' && domainBinding.provider ? { provider: domainBinding.provider } : {}),
|
|
314
|
+
},
|
|
309
315
|
{
|
|
310
316
|
layer: 'service' as const,
|
|
311
317
|
...(typeof slot?.provider === 'string' ? { provider: slot.provider } : {}),
|