@tbox.cn/app-toolkit 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-WUULQDOV.js +1 -0
- package/dist/contracts-resolver-PS2UH2G6.js +1 -0
- package/dist/index.d.ts +315 -144
- package/dist/index.js +9 -9
- package/package.json +2 -2
- package/src/assembly/provider-catalog.ts +1 -1
- package/src/assembly/resolve-key.ts +75 -0
- package/src/assembly/walk-manifests.ts +138 -85
- package/src/core/contracts-expected.ts +2 -2
- package/src/core/contracts-resolver.ts +12 -1
- package/src/core/module-schema.ts +87 -14
- package/src/dto.ts +48 -33
- package/src/factory.ts +62 -18
- package/src/index.ts +14 -7
- package/src/integrations/mock-bindings.ts +1 -1
- package/src/integrations/module-binding.ts +60 -0
- package/src/integrations/predicate-io.ts +26 -5
- package/src/integrations/predicate.ts +97 -57
- package/src/integrations/prune-bindings.ts +33 -14
- package/src/integrations/write.ts +43 -27
- package/src/views/app.ts +4 -4
- package/src/views/context.ts +11 -12
- package/src/views/integration-schemas.ts +61 -17
- package/src/views/modules.ts +29 -23
- package/src/views/providers.ts +5 -5
- package/src/views/service-detail.ts +1 -1
- package/src/views/service-resolutions.ts +14 -13
- package/tests/credentials-view.test.ts +2 -2
- package/tests/demo-app.ts +11 -6
- package/tests/dev-manifest-catalog.test.ts +14 -14
- package/tests/file-cache.test.ts +5 -5
- package/tests/module-schema.test.ts +106 -20
- package/tests/resolve-key.test.ts +154 -0
- package/tests/vendor-schema-keywords.test.ts +74 -0
- package/tests/views.test.ts +296 -166
- package/tests/write-core.test.ts +83 -0
- package/dist/chunk-KPD3LUH4.js +0 -1
- package/dist/contracts-resolver-E4SECBOG.js +0 -1
- package/src/integrations/domain-binding.ts +0 -81
package/src/views/context.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { readIntegrationsConfig } from '../integrations/read.js';
|
|
2
|
-
import { domainOfService } from '../integrations/domain-binding.js';
|
|
3
2
|
import { walkManifests, type WalkCatalog, type WalkManifestsResult } from '../assembly/walk-manifests.js';
|
|
4
3
|
import type { FileCache } from '../core/file-cache.js';
|
|
5
4
|
import type { ResolvedContracts } from '../core/contracts-resolver.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* 视图装配共享上下文(五投影共用;目录枚举零缓存——walkManifests 每次调用执行)。
|
|
9
|
-
*
|
|
8
|
+
* v6:域前缀换算退役(domainOfService 随归属模型退场)——模块占用判定改 declare 三键直读。
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
export interface ViewContext {
|
|
@@ -22,8 +21,6 @@ export interface ViewSnapshot {
|
|
|
22
21
|
contracts: ResolvedContracts;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
export { domainOfService };
|
|
26
|
-
|
|
27
24
|
/** 读取视图输入快照(每方法调用独立——Freshness Contract 单位 = 方法调用) */
|
|
28
25
|
export async function loadViewSnapshot(ctx: ViewContext): Promise<ViewSnapshot> {
|
|
29
26
|
const walk = walkManifests(ctx.appDir, ctx.cache);
|
|
@@ -40,12 +37,14 @@ export function loadViewSnapshotSync(ctx: ViewContext, contracts: ResolvedContra
|
|
|
40
37
|
return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts };
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
/** 模块占用服务集(demand
|
|
40
|
+
/** 模块占用服务集(v6——demand ∪ supply ∪ owns:归属模型下「平台服务」= 无主服务;
|
|
41
|
+
* mall-core owns auth.alipay-login → 移出 AppView.services 平台条目,入其模块页 ownedServices) */
|
|
44
42
|
export function moduleOccupiedServices(walk: WalkManifestsResult): Set<string> {
|
|
45
43
|
const occupied = new Set<string>();
|
|
46
44
|
for (const mod of walk.modules) {
|
|
47
|
-
for (const
|
|
48
|
-
for (const
|
|
45
|
+
for (const own of mod.descriptor.declare.owns ?? []) occupied.add(own.service);
|
|
46
|
+
for (const need of mod.descriptor.declare.consumes ?? []) occupied.add(need.service);
|
|
47
|
+
for (const slot of mod.descriptor.declare.providers?.slots ?? []) occupied.add(slot.service);
|
|
49
48
|
}
|
|
50
49
|
return occupied;
|
|
51
50
|
}
|
|
@@ -66,22 +65,22 @@ export function suppliedByVendors(catalog: WalkCatalog, service: string): string
|
|
|
66
65
|
return vendors.sort();
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
/** 服务展示名解析结果(
|
|
68
|
+
/** 服务展示名解析结果(name 恒有值——id 兜底内聚;v7 词汇统一 title→name) */
|
|
70
69
|
export interface ServiceDisplay {
|
|
71
|
-
|
|
70
|
+
name: string;
|
|
72
71
|
description?: string;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
/**
|
|
76
75
|
* 服务展示名单源(B1):词汇 meta(slotMeta——contracts-mall 词汇锁保证 mall 域覆盖)
|
|
77
76
|
* > demand meta(catalog.services——域外槽兜底;首声明聚合)> service id。逐字段回落——
|
|
78
|
-
* 词汇条目仅含 description 时 demand
|
|
77
|
+
* 词汇条目仅含 description 时 demand name 仍可达(外部契约包部分 meta 前向容错)。
|
|
79
78
|
* 四视图唯一消费点(/modules/:m services、/app 平台条目、/services/:s、/providers/:p services)。
|
|
80
79
|
*/
|
|
81
80
|
export function resolveServiceDisplay(walk: WalkManifestsResult, service: string): ServiceDisplay {
|
|
82
81
|
const vocab = walk.slotMeta[service];
|
|
83
82
|
const demand = walk.catalog.services[service];
|
|
84
|
-
const
|
|
83
|
+
const name = vocab?.name ?? demand?.name ?? service;
|
|
85
84
|
const description = vocab?.description ?? demand?.description;
|
|
86
|
-
return {
|
|
85
|
+
return { name, ...(description !== undefined ? { description } : {}) };
|
|
87
86
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IntegrationFieldRegistryView, LayerIntegrationSchemas, ServiceIntegrationSchemas } from '../dto.js';
|
|
2
2
|
import { isSupplied, type ViewSnapshot } from './context.js';
|
|
3
3
|
import { resolveServiceConfigSchema } from './providers.js';
|
|
4
|
-
import {
|
|
4
|
+
import { moduleOwnershipWritable, providerFootprint, resolveModuleBinding } from '../integrations/module-binding.js';
|
|
5
5
|
import { isControlledInlineProvider } from '../integrations/predicate.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -117,6 +117,12 @@ export function buildIntegrationFieldRegistry(
|
|
|
117
117
|
key,
|
|
118
118
|
schema: mergeFieldSources(sources),
|
|
119
119
|
sources: sources.map(({ service, required }) => ({ service, required })),
|
|
120
|
+
// G 批:x-tbox-global 聚合旗标(sources.some——subschema 属性标记直读,零新增 IO;
|
|
121
|
+
// 任一来源声明即 true——并集口径与 required 聚合同族)
|
|
122
|
+
global: sources.some(
|
|
123
|
+
(s) => !!s.subschema && typeof s.subschema === 'object'
|
|
124
|
+
&& (s.subschema as Record<string, unknown>)['x-tbox-global'] === true,
|
|
125
|
+
),
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
128
|
return {
|
|
@@ -129,7 +135,7 @@ export function buildIntegrationFieldRegistry(
|
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
/** D37 生效厂商解析单源(实际/假想同径):override(非空串)?? 实际解析。
|
|
132
|
-
* 空串归一(I3)单点;module 轴
|
|
138
|
+
* 空串归一(I3)单点;module 轴 v6 零归属 → null(override 不豁免——预览服务于保存)。 */
|
|
133
139
|
export function formProviderOf(
|
|
134
140
|
axis: 'app' | 'module',
|
|
135
141
|
snapshot: ViewSnapshot,
|
|
@@ -144,10 +150,10 @@ export function formProviderOf(
|
|
|
144
150
|
}
|
|
145
151
|
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
146
152
|
if (!mod) return null; // 视图已先行 404,此处防御
|
|
147
|
-
const w =
|
|
148
|
-
if (!w.writable) return null; //
|
|
153
|
+
const w = moduleOwnershipWritable(snapshot.walk, mod);
|
|
154
|
+
if (!w.writable) return null; // v6 零归属不豁免
|
|
149
155
|
if (ovr !== undefined) return ovr;
|
|
150
|
-
return
|
|
156
|
+
return resolveModuleBinding(snapshot.integrations, mod.id).provider;
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
/** D36/D37 表单层双投影(I1:同源同口径一次计算——双取单算 by construction) */
|
|
@@ -159,29 +165,39 @@ export interface LayerFormViews {
|
|
|
159
165
|
/** 防御双 null(module 不在册/不可写/未绑定、app 未绑定——F3) */
|
|
160
166
|
const NULL_FORM_VIEWS: LayerFormViews = { integrationSchemas: null, integrationFieldRegistry: null };
|
|
161
167
|
|
|
168
|
+
/** G 批表单收窄(消费投影单点):app/module 轴 registry.fields 只留 global===true——
|
|
169
|
+
* 未声明键落 ExtraFields 语义(P16 复合可见面:layer.properties 仍全量,字段级代数不受扰)。
|
|
170
|
+
* schemaless/unsupplied/additionalPropertiesAllowed 描述服务/Schema 面,不随字段过滤。 */
|
|
171
|
+
function narrowToGlobal(registry: IntegrationFieldRegistryView): IntegrationFieldRegistryView {
|
|
172
|
+
return { ...registry, fields: registry.fields.filter((f) => f.global) };
|
|
173
|
+
}
|
|
174
|
+
|
|
162
175
|
/** app 轴装配:scope = 供给足迹全量(≠ AppView.services——root.config 流向包括模块服务在内的所有服务);
|
|
163
176
|
* unsupplied 恒 [](结构性:足迹 ⊆ 供给)。未知厂商/零足迹 → 非 null 空集(镜像 D32 mock 语义)。 */
|
|
164
177
|
export function loadAppFormViews(snapshot: ViewSnapshot, appProvider?: string): LayerFormViews {
|
|
165
178
|
const provider = formProviderOf('app', snapshot, undefined, appProvider);
|
|
166
179
|
if (provider === null) return NULL_FORM_VIEWS;
|
|
167
180
|
const layer = resolveLayerIntegrationSchemas(snapshot, provider, providerFootprint(snapshot.catalog, provider));
|
|
168
|
-
return { integrationSchemas: layer, integrationFieldRegistry: buildIntegrationFieldRegistry(layer) };
|
|
181
|
+
return { integrationSchemas: layer, integrationFieldRegistry: narrowToGlobal(buildIntegrationFieldRegistry(layer)) };
|
|
169
182
|
}
|
|
170
183
|
|
|
171
|
-
/** module 轴装配:仅 writable
|
|
172
|
-
*
|
|
184
|
+
/** module 轴装配:仅 writable(v6 owns>0);scope = ownedServices ∩ 该厂商供给(v6——模块
|
|
185
|
+
* config 流向归属服务);unsupplied 同点派生(ownedServices − 供给,Set 去重保序——
|
|
186
|
+
* declare.owns 不去重防御)。 */
|
|
173
187
|
export function loadModuleFormViews(snapshot: ViewSnapshot, moduleId: string, moduleProvider?: string): LayerFormViews {
|
|
174
188
|
const provider = formProviderOf('module', snapshot, moduleId, moduleProvider);
|
|
175
189
|
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
176
190
|
if (provider === null || !mod) return NULL_FORM_VIEWS;
|
|
177
|
-
const
|
|
191
|
+
const owned = (mod.descriptor.declare.owns ?? []).map((o) => o.service);
|
|
178
192
|
const supplied = (s: string): boolean => isSupplied(snapshot.catalog, provider, s);
|
|
179
|
-
const layer = resolveLayerIntegrationSchemas(snapshot, provider,
|
|
193
|
+
const layer = resolveLayerIntegrationSchemas(snapshot, provider, owned.filter(supplied));
|
|
180
194
|
return {
|
|
181
195
|
integrationSchemas: layer,
|
|
182
|
-
integrationFieldRegistry:
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
integrationFieldRegistry: narrowToGlobal(
|
|
197
|
+
buildIntegrationFieldRegistry(layer, {
|
|
198
|
+
unsupplied: [...new Set(owned.filter((s) => !supplied(s)))],
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
185
201
|
};
|
|
186
202
|
}
|
|
187
203
|
|
|
@@ -195,9 +211,20 @@ export function loadModuleIntegrationSchemas(snapshot: ViewSnapshot, moduleId: s
|
|
|
195
211
|
return loadModuleFormViews(snapshot, moduleId).integrationSchemas;
|
|
196
212
|
}
|
|
197
213
|
|
|
214
|
+
/** 服务级生效绑定(v6 级联):显式 ?? modules[ownerOf[s]].provider ?? root.provider。
|
|
215
|
+
* 服务无主 → 仅 root(平台槽语义)。 */
|
|
216
|
+
function serviceLevelBindingProvider(snapshot: ViewSnapshot, service: string): string | null {
|
|
217
|
+
const owner = snapshot.walk.ownership[service];
|
|
218
|
+
if (owner !== undefined) {
|
|
219
|
+
return resolveModuleBinding(snapshot.integrations, owner).provider;
|
|
220
|
+
}
|
|
221
|
+
const p = (snapshot.integrations as { provider?: unknown } | null)?.provider;
|
|
222
|
+
return typeof p === 'string' && p.length > 0 ? p : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
198
225
|
/** 服务级 loader(v4.7 D38):四分支穷举(node.provider = 文件 services[s].provider 逐字原值;
|
|
199
226
|
* 实例层无条件排除——node 级口径);非空三分支归约一次复合解析。
|
|
200
|
-
* ① undefined/'' →
|
|
227
|
+
* ① undefined/'' → 模块级联(v6:modules[ownerOf[s]] → root 双缺 → 整字段 null)
|
|
201
228
|
* ② 非空 string → bare:catalog miss(未知厂商/未供给)→ configSchema:null 不 404(富化非门控)
|
|
202
229
|
* ③ 受控内联(isControlledInlineProvider 单源)→ 名 = p.provider;catalog 复合键 > p.configSchema ref
|
|
203
230
|
* (H9 逃逸 400 照抛——readInlineSchema 共享路径)
|
|
@@ -210,10 +237,10 @@ export function loadServiceIntegrationSchemas(
|
|
|
210
237
|
const p = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
|
|
211
238
|
?.services?.[service]?.provider;
|
|
212
239
|
if (p === undefined || p === '') {
|
|
213
|
-
const
|
|
214
|
-
return
|
|
240
|
+
const provider = serviceLevelBindingProvider(snapshot, service);
|
|
241
|
+
return provider === null
|
|
215
242
|
? null
|
|
216
|
-
: { provider
|
|
243
|
+
: { provider, configSchema: resolveServiceConfigSchema(snapshot, provider, service) };
|
|
217
244
|
}
|
|
218
245
|
if (typeof p === 'string') {
|
|
219
246
|
return { provider: p, configSchema: resolveServiceConfigSchema(snapshot, p, service) };
|
|
@@ -224,3 +251,20 @@ export function loadServiceIntegrationSchemas(
|
|
|
224
251
|
}
|
|
225
252
|
return null;
|
|
226
253
|
}
|
|
254
|
+
|
|
255
|
+
/** G 批:服务轴字段注册表(D38 四分支链复用——消费 loadServiceIntegrationSchemas 单源;
|
|
256
|
+
* 伪层 {provider, services: {[s]: {configSchema}}} → buildIntegrationFieldRegistry——
|
|
257
|
+
* fields = 该 schema 全部字段带 global 旗标;configSchema null → schemaless=[service]
|
|
258
|
+
* 非 null 空集;schemas 整体 null(全链未绑定/坏节点)→ registry null——镜像 D38 语义) */
|
|
259
|
+
export function loadServiceFieldRegistry(
|
|
260
|
+
snapshot: ViewSnapshot,
|
|
261
|
+
service: string,
|
|
262
|
+
): IntegrationFieldRegistryView | null {
|
|
263
|
+
const schemas = loadServiceIntegrationSchemas(snapshot, service);
|
|
264
|
+
if (schemas === null) return null;
|
|
265
|
+
const layer: LayerIntegrationSchemas = {
|
|
266
|
+
provider: schemas.provider,
|
|
267
|
+
services: { [service]: { configSchema: schemas.configSchema } },
|
|
268
|
+
};
|
|
269
|
+
return buildIntegrationFieldRegistry(layer);
|
|
270
|
+
}
|
package/src/views/modules.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type { ModuleDeclaration,
|
|
1
|
+
import type { ModuleDeclaration, ModuleBinding, ModuleDetailView, ServiceDemand } from '../dto.js';
|
|
2
2
|
import { AppToolkitError } from '../core/errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { moduleOwnershipWritable, resolveModuleBinding } from '../integrations/module-binding.js';
|
|
4
4
|
import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 需求轴/模块静态投影(views/modules;C4)。静态端点结构性零求值(铁律 #3——
|
|
8
8
|
* 无 resolution/issues/contracts 键,永不 503)。
|
|
9
|
-
*
|
|
10
|
-
* (
|
|
11
|
-
*
|
|
9
|
+
* v6 所有权模型:ownedServices(declare.owns 展平)/ binding 单数(resolveModuleBinding)/
|
|
10
|
+
* configEditable = owns>0(moduleOwnershipWritable 单源);integration = modules[install id]
|
|
11
|
+
* 集成位投影(provider/config;resources 不投影——N2)。v5 domainKeys/providerBindings 退役。
|
|
12
12
|
* 2026-09-16 双缺陷批:loadModulesView 排除 provider 家族包(供给/实施载体非可配置
|
|
13
13
|
* 业务模块)——walk 声明集与 loadModuleDetailView 不变(装包仍合法,详情保持 200)。
|
|
14
14
|
*/
|
|
@@ -33,46 +33,52 @@ export function loadModulesView(snapshot: ViewSnapshot): { modules: ModuleDeclar
|
|
|
33
33
|
.filter((mod) => !isProviderFamily(mod))
|
|
34
34
|
.map((mod) => ({
|
|
35
35
|
id: mod.id,
|
|
36
|
-
name
|
|
37
|
-
|
|
38
|
-
title: mod.descriptor.title ?? mod.descriptor.name,
|
|
36
|
+
// v7 词汇统一:name = 展示名(manifest name;兜底 id——恒有值)
|
|
37
|
+
name: mod.descriptor.name ?? mod.descriptor.id,
|
|
39
38
|
description: mod.descriptor.description ?? '',
|
|
40
39
|
}))
|
|
41
40
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
42
41
|
return { modules };
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
/** GET /modules/:module:模块静态一站式(
|
|
44
|
+
/** GET /modules/:module:模块静态一站式(modules[install id] 集成位原值 + demand;PUT 读对偶) */
|
|
46
45
|
export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string): ModuleDetailView {
|
|
47
46
|
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
48
47
|
if (!mod) {
|
|
49
48
|
throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
|
|
50
49
|
}
|
|
51
|
-
const services: ServiceDemand[] = (mod.descriptor.
|
|
50
|
+
const services: ServiceDemand[] = (mod.descriptor.declare.consumes ?? []).map((need) => {
|
|
52
51
|
// 展示名单源走链(B1):词汇 meta > demand meta > id——词汇主源防 manifest demand 漂移
|
|
53
52
|
const display = resolveServiceDisplay(snapshot.walk, need.service);
|
|
54
53
|
return {
|
|
55
54
|
service: need.service,
|
|
56
|
-
|
|
55
|
+
name: display.name,
|
|
57
56
|
...(display.description !== undefined ? { description: display.description } : {}),
|
|
58
57
|
required: need.optional === false,
|
|
59
58
|
};
|
|
60
59
|
});
|
|
61
|
-
const w =
|
|
62
|
-
const
|
|
63
|
-
// integration =
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
const w = moduleOwnershipWritable(snapshot.walk, mod);
|
|
61
|
+
const modulesNode = (snapshot.integrations as { modules?: Record<string, unknown> } | null)?.modules ?? {};
|
|
62
|
+
// integration = modules[install id] 集成位投影(D21 编辑预填源;provider/config 两键——
|
|
63
|
+
// resources 不投影不写,N2);未配置 = null
|
|
64
|
+
const rawNode = modulesNode[moduleId] as Record<string, unknown> | undefined;
|
|
65
|
+
const integration: ModuleDetailView['integration'] = rawNode
|
|
66
|
+
? {
|
|
67
|
+
...(typeof rawNode.provider === 'string' ? { provider: rawNode.provider } : {}),
|
|
68
|
+
...(rawNode.config && typeof rawNode.config === 'object' && !Array.isArray(rawNode.config)
|
|
69
|
+
? { config: rawNode.config as Record<string, unknown> }
|
|
70
|
+
: {}),
|
|
71
|
+
}
|
|
72
|
+
: null;
|
|
73
|
+
const binding: ModuleBinding = resolveModuleBinding(snapshot.integrations, moduleId);
|
|
69
74
|
return {
|
|
70
|
-
//
|
|
71
|
-
|
|
75
|
+
// v7 词汇统一:name = 展示名(兜底与 /modules 同语义——manifest name ?? id)
|
|
76
|
+
name: mod.descriptor.name ?? mod.descriptor.id,
|
|
72
77
|
description: mod.descriptor.description ?? '',
|
|
73
|
-
|
|
78
|
+
// v6:ownedServices(declare.owns 展平排序)——N13:展示名走 resolveServiceDisplay 链,不重复携带
|
|
79
|
+
ownedServices: w.ownedServices,
|
|
74
80
|
integration,
|
|
75
|
-
|
|
81
|
+
binding: { provider: binding.provider, layer: binding.layer },
|
|
76
82
|
configEditable: w.writable,
|
|
77
83
|
services,
|
|
78
84
|
};
|
package/src/views/providers.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function loadProvidersView(snapshot: ViewSnapshot): ProvidersView {
|
|
|
22
22
|
local: entries.every((e) => e.local === true),
|
|
23
23
|
owner: entries[0].owner,
|
|
24
24
|
// 厂商展示元数据(B1 walk 层传播——首条目 = 首声明,与 owner 同语义)
|
|
25
|
-
...(entries[0].
|
|
25
|
+
...(entries[0].name ? { name: entries[0].name } : {}),
|
|
26
26
|
...(entries[0].description ? { description: entries[0].description } : {}),
|
|
27
27
|
});
|
|
28
28
|
}
|
|
@@ -43,10 +43,10 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
|
|
|
43
43
|
bucket.implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
// 服务展示名走链(B1):词汇 meta > demand meta > id(
|
|
46
|
+
// 服务展示名走链(B1):词汇 meta > demand meta > id(name 恒发)
|
|
47
47
|
for (const [service, bucket] of Object.entries(services)) {
|
|
48
48
|
const display = resolveServiceDisplay(snapshot.walk, service);
|
|
49
|
-
bucket.
|
|
49
|
+
bucket.name = display.name;
|
|
50
50
|
if (display.description !== undefined) bucket.description = display.description;
|
|
51
51
|
}
|
|
52
52
|
if (include?.has('integrationSchemas')) {
|
|
@@ -59,8 +59,8 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
|
|
|
59
59
|
provider,
|
|
60
60
|
local: entries.every((e) => e.local === true),
|
|
61
61
|
owner: entries[0].owner,
|
|
62
|
-
// 厂商展示元数据(B1 walk
|
|
63
|
-
...(entries[0].
|
|
62
|
+
// 厂商展示元数据(B1 walk 层传播——首条目透传;v7 title→name)
|
|
63
|
+
...(entries[0].name ? { name: entries[0].name } : {}),
|
|
64
64
|
...(entries[0].description ? { description: entries[0].description } : {}),
|
|
65
65
|
services,
|
|
66
66
|
};
|
|
@@ -21,7 +21,7 @@ export function loadServiceDetailView(snapshot: ViewSnapshot, service: string):
|
|
|
21
21
|
const display = resolveServiceDisplay(snapshot.walk, service);
|
|
22
22
|
return {
|
|
23
23
|
service,
|
|
24
|
-
|
|
24
|
+
name: display.name,
|
|
25
25
|
...(display.description !== undefined ? { description: display.description } : {}),
|
|
26
26
|
required: demand ? !demand.optional : false,
|
|
27
27
|
// defaults 唯一落位(manifest 预填)
|
|
@@ -11,10 +11,10 @@ import type {
|
|
|
11
11
|
ServiceStatus,
|
|
12
12
|
} from '../dto.js';
|
|
13
13
|
import { AppToolkitError } from '../core/errors.js';
|
|
14
|
-
import {
|
|
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';
|
|
17
|
-
import {
|
|
17
|
+
import { isSupplied, type ViewSnapshot } from './context.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* 求值轴投影(views/service-resolutions;C4;FX-2c——B6 effective 填充 + F2 layer 推导;
|
|
@@ -50,22 +50,22 @@ interface InstanceEvaluation {
|
|
|
50
50
|
effective?: EffectiveBinding;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/** 最特异声明层推导(F2):inst 位 provider 在场 → 'instance';槽级 → 'service'
|
|
53
|
+
/** 最特异声明层推导(F2):inst 位 provider 在场 → 'instance';槽级 → 'service';模块级 → 'module';
|
|
54
54
|
* root → 'app'。实例位查找复刻 TIER0 语义(精确键优先、'*' 通配兜底——与 resolveEffectiveService
|
|
55
|
-
* 的 instances 查找层同轴)。
|
|
56
|
-
* (四层全无 provider 声明 → null——Entry.layer「未配置双缺席」口径,D31)。 */
|
|
55
|
+
* 的 instances 查找层同轴)。v6:模块级段消费 resolveModuleBinding 单源(ownerOf=walk.ownership);
|
|
56
|
+
* 返回扩 null(四层全无 provider 声明 → null——Entry.layer「未配置双缺席」口径,D31)。 */
|
|
57
57
|
function deriveBindingLayer(snapshot: ViewSnapshot, service: string, instanceId: string): BindingLayer | null {
|
|
58
58
|
const integrations = snapshot.integrations as {
|
|
59
59
|
provider?: unknown;
|
|
60
|
-
domains?: Record<string, { provider?: unknown }>;
|
|
61
60
|
services?: Record<string, { provider?: unknown; instances?: Record<string, { provider?: unknown }> }>;
|
|
62
61
|
} | null;
|
|
63
|
-
const domain = domainOfService(service);
|
|
64
62
|
const slot = integrations?.services?.[service];
|
|
65
63
|
const inst = slot?.instances?.[instanceId] ?? slot?.instances?.['*'];
|
|
66
64
|
if (inst && inst.provider !== undefined) return 'instance';
|
|
67
65
|
if (slot?.provider !== undefined) return 'service';
|
|
68
|
-
|
|
66
|
+
const owner = snapshot.walk.ownership[service];
|
|
67
|
+
if (owner !== undefined
|
|
68
|
+
&& resolveModuleBinding(snapshot.integrations, owner).layer === 'module') return 'module';
|
|
69
69
|
if (typeof integrations?.provider === 'string' && integrations.provider.length > 0) return 'app';
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
@@ -292,10 +292,8 @@ function buildInheritance(
|
|
|
292
292
|
): ServiceResolution['inheritance'] {
|
|
293
293
|
const integrations = snapshot.integrations as {
|
|
294
294
|
provider?: string;
|
|
295
|
-
domains?: Record<string, { provider?: string }>;
|
|
296
295
|
services?: Record<string, { provider?: unknown; implementation?: string; instances?: Record<string, { provider?: string; implementation?: string }> }>;
|
|
297
296
|
} | null;
|
|
298
|
-
const domain = domainOfService(service);
|
|
299
297
|
const slot = integrations?.services?.[service];
|
|
300
298
|
const cfg = snapshot.integrations as { instances?: unknown; defaultInstanceId?: unknown } | null;
|
|
301
299
|
const normalized = evaluationContracts(snapshot).normalizeInstances({
|
|
@@ -304,13 +302,16 @@ function buildInheritance(
|
|
|
304
302
|
});
|
|
305
303
|
const defaultInstanceId = normalized.defaultInstanceId ?? '*';
|
|
306
304
|
const defaultInstance = slot?.instances?.[defaultInstanceId] ?? slot?.instances?.['*'];
|
|
307
|
-
// module 段消费
|
|
308
|
-
const
|
|
305
|
+
// module 段消费 resolveModuleBinding 单源(v6——与 binding / 谓词 #7 同源;ownerOf=walk.ownership)
|
|
306
|
+
const owner = snapshot.walk.ownership[service];
|
|
307
|
+
const moduleBinding = owner !== undefined
|
|
308
|
+
? resolveModuleBinding(integrations as Record<string, unknown> | null, owner)
|
|
309
|
+
: { provider: null as string | null, layer: null as 'module' | 'app' | null };
|
|
309
310
|
return [
|
|
310
311
|
{ layer: 'app' as const, ...(integrations?.provider ? { provider: integrations.provider } : {}) },
|
|
311
312
|
{
|
|
312
313
|
layer: 'module' as const,
|
|
313
|
-
...(
|
|
314
|
+
...(moduleBinding.layer === 'module' && moduleBinding.provider ? { provider: moduleBinding.provider } : {}),
|
|
314
315
|
},
|
|
315
316
|
{
|
|
316
317
|
layer: 'service' as const,
|
|
@@ -31,10 +31,10 @@ function credHarness(): { app: TempApp; tk: AppToolkit } {
|
|
|
31
31
|
'packages/provider-w/tbox.module.json',
|
|
32
32
|
JSON.stringify({
|
|
33
33
|
schemaVersion: 1,
|
|
34
|
-
|
|
34
|
+
id: 'provider-w',
|
|
35
35
|
version: '0.1.0',
|
|
36
36
|
kind: 'business',
|
|
37
|
-
|
|
37
|
+
declare: {
|
|
38
38
|
providers: {
|
|
39
39
|
slots: [
|
|
40
40
|
{ service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c-signed-v1', credentialSchema: 'schemas/cred.json' },
|
package/tests/demo-app.ts
CHANGED
|
@@ -46,12 +46,15 @@ export interface FakeContractsOptions {
|
|
|
46
46
|
/** 严格校验失败 fixture(FX-1c T5):parseIntegrationsConfig 对含该子串文档 throw */
|
|
47
47
|
rejectMarker?: string;
|
|
48
48
|
/** resolveEffectiveService 自定义函数体(FX-2c O4 fixture:默认恒 service-not-configured;
|
|
49
|
-
* 入参 (integrations, service, instanceId, supplies),需 return EffectiveServiceResolution 形状) */
|
|
49
|
+
* 入参 (integrations, service, instanceId, supplies, ownerOf),需 return EffectiveServiceResolution 形状) */
|
|
50
50
|
resolveBody?: string;
|
|
51
|
+
/** v6 形状标记(OWNERSHIP_BINDING_V6)缺席 fixture——默认在场(v6 假件签名随动 N11);
|
|
52
|
+
* false = v5 contracts 形态(ownershipBindingV6: false → assertContractsUsable 503 第四分支) */
|
|
53
|
+
withMarker?: boolean;
|
|
51
54
|
/** service-slots.json 词汇(P9 身份判据/求值键集;缺省不物化 = 词汇空) */
|
|
52
55
|
slots?: string[];
|
|
53
|
-
/** 词汇 meta 节(per-service {
|
|
54
|
-
meta?: Record<string, {
|
|
56
|
+
/** 词汇 meta 节(per-service {name,description}——B1 展示名链 fixture) */
|
|
57
|
+
meta?: Record<string, { name?: string; description?: string }>;
|
|
55
58
|
/** service-slots.json 落位:'root' = 包根(缺省——夹具/旧布局形态);
|
|
56
59
|
* 'src' = 包内 src/(发布包 src 附带 / shim 链接形态——B0 探测跳 ②) */
|
|
57
60
|
slotsLayout?: 'root' | 'src';
|
|
@@ -63,7 +66,7 @@ export interface FakeContractsOptions {
|
|
|
63
66
|
/** 假 contracts 包:package.json exports 形态逐字段复刻发布面(types+import 双条件、无 require/default)
|
|
64
67
|
* ——resolver 不经 exports 解析(包根直探),该形态为 E4 类错误的机械防线。 */
|
|
65
68
|
export function writeFakeContractsPackage(packageRoot: string, opts: FakeContractsOptions = {}): void {
|
|
66
|
-
const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, slots, meta, slotsLayout = 'root', zodRejectPaths } = opts;
|
|
69
|
+
const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, withMarker = true, slots, meta, slotsLayout = 'root', zodRejectPaths } = opts;
|
|
67
70
|
const strictBody = withStrict
|
|
68
71
|
? `export function parseIntegrationsConfig(raw) { ${
|
|
69
72
|
rejectMarker ? `if (JSON.stringify(raw).includes(${JSON.stringify(rejectMarker)})) throw new Error("fake strict reject"); ` : ''
|
|
@@ -73,9 +76,10 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
|
|
|
73
76
|
: ''
|
|
74
77
|
}return raw; }`
|
|
75
78
|
: '';
|
|
76
|
-
const resolveLine = `export function resolveEffectiveService(integrations, service, instanceId, supplies) { ${
|
|
79
|
+
const resolveLine = `export function resolveEffectiveService(integrations, service, instanceId, supplies, ownerOf) { ${
|
|
77
80
|
resolveBody ?? 'return { status: "service-not-configured" };'
|
|
78
81
|
} }`;
|
|
82
|
+
const markerLine = withMarker ? 'export const OWNERSHIP_BINDING_V6 = true;' : '';
|
|
79
83
|
const exportsBody = missingEvaluationExports
|
|
80
84
|
? `export const marker = 1;\n${strictBody}`
|
|
81
85
|
: [
|
|
@@ -84,6 +88,7 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
|
|
|
84
88
|
'export function moduleDomainOf(service) { return String(service).split(".")[0] ?? ""; }',
|
|
85
89
|
'export function normalizeInstances(config) { return { instances: (config && config.instances) || [], defaultInstanceId: (config && config.defaultInstanceId) ?? null }; }',
|
|
86
90
|
'export const RESOURCE_TYPE_IDS = ["knowledge"];',
|
|
91
|
+
markerLine,
|
|
87
92
|
strictBody,
|
|
88
93
|
'',
|
|
89
94
|
]
|
|
@@ -135,7 +140,7 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
|
|
|
135
140
|
export function writeSelfSrcContractPackage(
|
|
136
141
|
appDir: string,
|
|
137
142
|
name: string,
|
|
138
|
-
content: { slots: string[]; meta?: Record<string, {
|
|
143
|
+
content: { slots: string[]; meta?: Record<string, { name?: string; description?: string }> },
|
|
139
144
|
): string {
|
|
140
145
|
const dir = join(appDir, 'packages', name, 'src');
|
|
141
146
|
mkdirSync(dir, { recursive: true });
|
|
@@ -11,20 +11,20 @@ import { createTempApp, linkContracts, writeFakeContractsPackage, writeSelfSrcCo
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/** strict descriptor(形状对齐 provider-custom 出厂 manifest——parseModuleDescriptor 严格面) */
|
|
14
|
-
function descriptor(
|
|
14
|
+
function descriptor(id: string, provider: string, service: string): Record<string, unknown> {
|
|
15
15
|
return {
|
|
16
16
|
schemaVersion: 1,
|
|
17
|
-
|
|
17
|
+
id,
|
|
18
18
|
version: '1.0.0',
|
|
19
19
|
kind: 'business',
|
|
20
20
|
risk: { level: 'low', writeBoundary: 'source' },
|
|
21
21
|
distribution: { defaultMode: 'codegen' },
|
|
22
|
-
|
|
23
|
-
handlers: [], tools: [], cards: [], routes: [], pages: [], tabs: [],
|
|
22
|
+
declare: {
|
|
23
|
+
handlers: [], tools: [], cards: [], routes: [], pages: [], tabs: [], owns: [], consumes: [],
|
|
24
24
|
providers: {
|
|
25
25
|
slots: [{ service, provider, implementation: `${provider}-impl@1`, credentialType: 'apikey' }],
|
|
26
26
|
},
|
|
27
|
-
|
|
27
|
+
resources: [],
|
|
28
28
|
},
|
|
29
29
|
dependencies: { modules: [] },
|
|
30
30
|
env: [],
|
|
@@ -125,13 +125,13 @@ describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
|
|
|
125
125
|
const store = join(app.appDir, '_store-a');
|
|
126
126
|
writeFakeContractsPackage(store, {
|
|
127
127
|
slots: ['auth.alipay-login', 'mall.info'],
|
|
128
|
-
meta: { 'auth.alipay-login': {
|
|
128
|
+
meta: { 'auth.alipay-login': { name: '支付宝登录', description: '登录换码(code2Session)' } },
|
|
129
129
|
});
|
|
130
130
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
131
131
|
|
|
132
132
|
const walk = walkManifests(app.appDir);
|
|
133
133
|
expect(walk.vocabulary).toEqual(['auth.alipay-login', 'mall.info']);
|
|
134
|
-
expect(walk.slotMeta['auth.alipay-login']).toEqual({
|
|
134
|
+
expect(walk.slotMeta['auth.alipay-login']).toEqual({ name: '支付宝登录', description: '登录换码(code2Session)' });
|
|
135
135
|
expect(walk.slotMeta['mall.info']).toBeUndefined(); // meta 缺席槽不入表
|
|
136
136
|
});
|
|
137
137
|
|
|
@@ -149,25 +149,25 @@ describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
|
|
|
149
149
|
app = createTempApp('tbox-probe-');
|
|
150
150
|
writeSelfSrcContractPackage(app.appDir, 'contracts-mall', {
|
|
151
151
|
slots: ['member.account', 'parking.payment'],
|
|
152
|
-
meta: { 'member.account': {
|
|
152
|
+
meta: { 'member.account': { name: '会员账户' } },
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
const walk = walkManifests(app.appDir);
|
|
156
156
|
expect(walk.vocabulary).toEqual(['member.account', 'parking.payment']);
|
|
157
|
-
expect(walk.slotMeta['member.account']).toEqual({
|
|
157
|
+
expect(walk.slotMeta['member.account']).toEqual({ name: '会员账户' });
|
|
158
158
|
});
|
|
159
159
|
|
|
160
160
|
it('P4: 多包同槽 meta → 发现序首声明胜出(apps/* 先于 packages/* 候选序)', () => {
|
|
161
161
|
app = createTempApp('tbox-probe-');
|
|
162
162
|
const storeA = join(app.appDir, '_store-first');
|
|
163
|
-
writeFakeContractsPackage(storeA, { slots: ['x.y'], meta: { 'x.y': {
|
|
163
|
+
writeFakeContractsPackage(storeA, { slots: ['x.y'], meta: { 'x.y': { name: '甲' } } });
|
|
164
164
|
linkContracts(app.appDir, storeA, { subdir: 'apps/server' });
|
|
165
165
|
const storeB = join(app.appDir, '_store-second');
|
|
166
|
-
writeFakeContractsPackage(storeB, { slots: ['x.y'], meta: { 'x.y': {
|
|
166
|
+
writeFakeContractsPackage(storeB, { slots: ['x.y'], meta: { 'x.y': { name: '乙' } } });
|
|
167
167
|
linkContracts(app.appDir, storeB, { subdir: 'packages/mock' });
|
|
168
168
|
|
|
169
169
|
const walk = walkManifests(app.appDir);
|
|
170
|
-
expect(walk.slotMeta['x.y']).toEqual({
|
|
170
|
+
expect(walk.slotMeta['x.y']).toEqual({ name: '甲' });
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
it('P5: 坏 meta 条目(非对象/非 string/空串)→ 宽松跳过,slots 正常', () => {
|
|
@@ -176,13 +176,13 @@ describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
|
|
|
176
176
|
writeFakeContractsPackage(store, { slots: ['a.b'] });
|
|
177
177
|
writeFileSync(
|
|
178
178
|
join(store, 'service-slots.json'),
|
|
179
|
-
JSON.stringify({ slots: ['a.b', 'c.d', 'e.f'], meta: { 'a.b': '字符串', 'c.d': {
|
|
179
|
+
JSON.stringify({ slots: ['a.b', 'c.d', 'e.f'], meta: { 'a.b': '字符串', 'c.d': { name: 42 }, 'e.f': { name: '甲', description: '' } } }),
|
|
180
180
|
);
|
|
181
181
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
182
182
|
|
|
183
183
|
const walk = walkManifests(app.appDir);
|
|
184
184
|
expect(walk.vocabulary).toEqual(['a.b', 'c.d', 'e.f']);
|
|
185
|
-
expect(walk.slotMeta).toEqual({ 'e.f': {
|
|
185
|
+
expect(walk.slotMeta).toEqual({ 'e.f': { name: '甲' } }); // 空串 description 不入表(与 title 守卫对称)
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
it('P6: 无契约包(退化态)→ vocabulary 空 + slotMeta 空(P9 退化路径语义锁)', () => {
|
package/tests/file-cache.test.ts
CHANGED
|
@@ -132,7 +132,7 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
|
|
|
132
132
|
mkdirSync(join(app.appDir, 'packages', 'module-new'), { recursive: true });
|
|
133
133
|
writeFileSync(
|
|
134
134
|
join(app.appDir, 'packages', 'module-new', 'tbox.module.json'),
|
|
135
|
-
JSON.stringify({ schemaVersion: 1,
|
|
135
|
+
JSON.stringify({ schemaVersion: 1, id: 'module-new', declare: { owns: [{ service: 'new.slot' }], consumes: [{ service: 'new.slot' }] } }),
|
|
136
136
|
);
|
|
137
137
|
const walk = walkManifests(app.appDir);
|
|
138
138
|
expect(walk.modules.map((m) => m.id)).toContain('module-new');
|
|
@@ -172,10 +172,10 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
|
|
|
172
172
|
join(app.appDir, 'packages', 'provider-x', 'tbox.module.json'),
|
|
173
173
|
JSON.stringify({
|
|
174
174
|
schemaVersion: 1,
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
id: 'provider-x',
|
|
176
|
+
declare: {
|
|
177
177
|
providers: { slots: [{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c' }] },
|
|
178
|
-
|
|
178
|
+
consumes: [{ service: 'member.account' }],
|
|
179
179
|
},
|
|
180
180
|
}),
|
|
181
181
|
);
|
|
@@ -196,7 +196,7 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
|
|
|
196
196
|
mkdirSync(sdkDir, { recursive: true });
|
|
197
197
|
writeFileSync(
|
|
198
198
|
join(sdkDir, 'tbox.module.json'),
|
|
199
|
-
JSON.stringify({ schemaVersion: 1,
|
|
199
|
+
JSON.stringify({ schemaVersion: 1, id: 'module-sdk-y', declare: { owns: [{ service: 'sdk.slot-y' }], consumes: [{ service: 'sdk.slot-y', name: 'Sdk 槽' }] } }),
|
|
200
200
|
);
|
|
201
201
|
const modules2 = await tk.loadModules();
|
|
202
202
|
expect(modules2.modules.map((m) => m.id)).toContain('module-sdk-y');
|