@tbox.cn/app-toolkit 0.3.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 +382 -129
- package/dist/index.js +9 -8
- 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 +216 -96
- package/src/core/contracts-expected.ts +2 -2
- package/src/core/contracts-resolver.ts +12 -1
- package/src/core/env-expansion.ts +14 -12
- package/src/core/module-schema.ts +88 -12
- package/src/dto.ts +53 -25
- package/src/factory.ts +62 -18
- package/src/index.ts +18 -7
- package/src/integrations/evaluate-app.ts +41 -0
- 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 +205 -0
- package/src/integrations/write.ts +43 -27
- package/src/views/app.ts +8 -4
- package/src/views/context.ts +26 -7
- package/src/views/integration-schemas.ts +61 -17
- package/src/views/modules.ts +45 -27
- package/src/views/providers.ts +13 -1
- package/src/views/service-detail.ts +5 -3
- package/src/views/service-resolutions.ts +14 -13
- package/tests/credentials-view.test.ts +2 -2
- package/tests/demo-app.ts +34 -5
- package/tests/dev-manifest-catalog.test.ts +86 -6
- package/tests/file-cache.test.ts +5 -5
- package/tests/module-schema.test.ts +117 -16
- package/tests/resolve-key.test.ts +154 -0
- package/tests/vendor-schema-keywords.test.ts +86 -0
- package/tests/views.test.ts +425 -139
- 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
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ResolvedContracts } from '../core/contracts-resolver.js';
|
|
4
|
+
import { resolveContractsForApp } from '../core/contracts-resolver.js';
|
|
5
|
+
import { expandEnvVars } from '../core/env-expansion.js';
|
|
6
|
+
import { writeAtomic } from '../core/fskit.js';
|
|
7
|
+
import { walkManifests } from '../assembly/walk-manifests.js';
|
|
8
|
+
import { readIntegrationsConfig } from './read.js';
|
|
9
|
+
import { isControlledInlineProvider } from './predicate.js';
|
|
10
|
+
import { reconcileOrphanCredentials } from './credentials.js';
|
|
11
|
+
import { evaluateAppIntegrationIssues } from './evaluate-app.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* integrations.json 死绑定清理(cli-remove-prune-and-new D2 核心件)。
|
|
15
|
+
*
|
|
16
|
+
* 不变式:**删除判据 ≡ doctor integration-services 报错判据(构造性同源)**——消费 doctor 同一
|
|
17
|
+
* 共享求值器 evaluateAppIntegrationIssues 的结构化 issue,code 白名单
|
|
18
|
+
* {VENDOR_NOT_INSTALLED, BINDING_UNRESOLVED, MODULE_CONFIG_DEAD} + 守卫(绑定引用链候选
|
|
19
|
+
* provider ∈ installedVendors 即保留,排除活厂商错配)→ 删除域严格 = "供给载体消亡"。
|
|
20
|
+
* v6:模块卸载 → modules[m] **整节点确定性清理**(provider+config+resources——D4:
|
|
21
|
+
* 模块消亡清理 vs 集成位编辑保全不矛盾;MODULE_CONFIG_DEAD 为判据源)。
|
|
22
|
+
*
|
|
23
|
+
* 由此推论(均已实证):需求侧孤儿绑定(demand 消失、供给在场)保留——doctor 判其合法;用户
|
|
24
|
+
* 预配置绑定受保护;一次 remove 自愈全文件历史死债;凭据类 issue(CREDENTIAL_FILE_MISSING 等)
|
|
25
|
+
* 刻意不进白名单,原样保留。
|
|
26
|
+
*
|
|
27
|
+
* 写侧三防线(镜像 write core):归一化(空节点删键)→ parseIntegrationsConfig
|
|
28
|
+
* 严格校验(失败弃写)→ writeAtomic。凭据孤儿只报告不清理(D9——与 DELETE 管线现行为一致)。
|
|
29
|
+
* 全空(无任何存活绑定)→ 删文件——ensureMockBindings(缺席即生成)在重装供给时再生闭环。
|
|
30
|
+
*
|
|
31
|
+
* 最佳努力语义:谓词不可用(contracts 未安装/降级、env 缺失、求值 throw)→ skipped 返回 +
|
|
32
|
+
* 文件原样,绝不阻断 remove 本体。
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export interface PruneResult {
|
|
36
|
+
/** 被清除的绑定节点描述(remove 日志行,如 'root.provider=mock' / 'services.member.account') */
|
|
37
|
+
removed: string[];
|
|
38
|
+
/** 孤儿凭据(D9 只报告不清理——reconcileOrphanCredentials 产物 message) */
|
|
39
|
+
orphanCredentials: string[];
|
|
40
|
+
/** 跳过原因(正常路径 undefined;not-configured = integrations.json 缺席,兼容态零动作) */
|
|
41
|
+
skipped?: 'predicate-unavailable' | 'validation-failed' | 'not-configured';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 宽松形状(谓词输入同族子集——doctor 读裸 JSON 先例,严格 zod 归模板 loader) */
|
|
45
|
+
interface PruneDoc {
|
|
46
|
+
provider?: string;
|
|
47
|
+
config?: Record<string, unknown>;
|
|
48
|
+
configByInstance?: Record<string, Record<string, unknown>>;
|
|
49
|
+
credentialRef?: string;
|
|
50
|
+
credentialRefByInstance?: Record<string, string>;
|
|
51
|
+
instances?: Array<{ id: string }>;
|
|
52
|
+
defaultInstanceId?: string | null;
|
|
53
|
+
modules?: Record<string, unknown>;
|
|
54
|
+
services?: Record<
|
|
55
|
+
string,
|
|
56
|
+
{
|
|
57
|
+
provider?: string | Record<string, unknown>;
|
|
58
|
+
implementation?: unknown;
|
|
59
|
+
config?: Record<string, unknown>;
|
|
60
|
+
credentialRef?: string;
|
|
61
|
+
instances?: Record<string, { enabled?: boolean; provider?: unknown; implementation?: unknown; credentialRef?: string }>;
|
|
62
|
+
}
|
|
63
|
+
>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 候选 provider 字符串提取(string 直取;受控内联对象取 .provider;其余 undefined) */
|
|
67
|
+
function providerOf(v: unknown): string | undefined {
|
|
68
|
+
if (typeof v === 'string') return v.length > 0 ? v : undefined;
|
|
69
|
+
if (v && typeof v === 'object' && isControlledInlineProvider(v)) {
|
|
70
|
+
const p = (v as { provider: string }).provider;
|
|
71
|
+
return p.length > 0 ? p : undefined;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function pruneIntegrationBindings(appDir: string): Promise<PruneResult> {
|
|
77
|
+
const file = join(appDir, 'config', 'integrations.json');
|
|
78
|
+
const raw = readIntegrationsConfig(appDir);
|
|
79
|
+
if (raw === null) return { removed: [], orphanCredentials: [], skipped: 'not-configured' };
|
|
80
|
+
|
|
81
|
+
const contracts: ResolvedContracts = await resolveContractsForApp(appDir);
|
|
82
|
+
if (!contracts.strictValidation) {
|
|
83
|
+
return { removed: [], orphanCredentials: [], skipped: 'predicate-unavailable' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// env 展开(tolerant——doctor 走 fail-fast 外壳,prune 缺变量即降级跳过)
|
|
87
|
+
let integrations: Record<string, unknown>;
|
|
88
|
+
try {
|
|
89
|
+
const expanded = expandEnvVars(JSON.stringify(raw), process.env);
|
|
90
|
+
if (expanded.missing.length > 0 || expanded.text === undefined) {
|
|
91
|
+
return { removed: [], orphanCredentials: [], skipped: 'predicate-unavailable' };
|
|
92
|
+
}
|
|
93
|
+
integrations = JSON.parse(expanded.text) as Record<string, unknown>;
|
|
94
|
+
} catch {
|
|
95
|
+
return { removed: [], orphanCredentials: [], skipped: 'predicate-unavailable' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let issues;
|
|
99
|
+
try {
|
|
100
|
+
issues = await evaluateAppIntegrationIssues(appDir, integrations, contracts, process.env);
|
|
101
|
+
} catch {
|
|
102
|
+
return { removed: [], orphanCredentials: [], skipped: 'predicate-unavailable' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const walk = walkManifests(appDir);
|
|
106
|
+
const installedVendors = new Set(Object.keys(walk.catalog.providers));
|
|
107
|
+
const walkOwnership = walk.ownership;
|
|
108
|
+
const next = structuredClone(raw) as PruneDoc;
|
|
109
|
+
next.services ??= {};
|
|
110
|
+
const removed: string[] = [];
|
|
111
|
+
|
|
112
|
+
for (const issue of issues) {
|
|
113
|
+
if (issue.severity !== 'error') continue;
|
|
114
|
+
if (issue.code === 'VENDOR_NOT_INSTALLED') {
|
|
115
|
+
if (issue.module) {
|
|
116
|
+
// v6:modules[owner].provider 死厂商 → 整节点删除前提下的 provider 键清理;
|
|
117
|
+
// 仅 provider(无 config/resources)→ 节点删除(N1 归一)
|
|
118
|
+
const modulesNode = (next.modules ?? {}) as Record<string, Record<string, unknown>>;
|
|
119
|
+
const node = modulesNode[issue.module];
|
|
120
|
+
if (node?.provider) {
|
|
121
|
+
removed.push(`modules.${issue.module}.provider=${String(node.provider)}`);
|
|
122
|
+
delete node.provider;
|
|
123
|
+
if (Object.keys(node).length === 0) delete next.modules![issue.module]; // 归一
|
|
124
|
+
}
|
|
125
|
+
} else if (next.provider) {
|
|
126
|
+
removed.push(`root.provider=${next.provider}`);
|
|
127
|
+
delete next.provider;
|
|
128
|
+
}
|
|
129
|
+
} else if (issue.code === 'MODULE_CONFIG_DEAD' && issue.module) {
|
|
130
|
+
// v6 D4:模块卸载 → modules[m] 整节点确定性清理(provider+config+resources)
|
|
131
|
+
const modulesNode = (next.modules ?? {}) as Record<string, Record<string, unknown>>;
|
|
132
|
+
if (modulesNode[issue.module]) {
|
|
133
|
+
const node = modulesNode[issue.module];
|
|
134
|
+
const parts = [
|
|
135
|
+
node.provider !== undefined ? 'provider' : undefined,
|
|
136
|
+
node.config !== undefined ? 'config' : undefined,
|
|
137
|
+
node.resources !== undefined ? 'resources' : undefined,
|
|
138
|
+
].filter(Boolean);
|
|
139
|
+
removed.push(`modules.${issue.module}(${parts.join('+')} 整节点)`);
|
|
140
|
+
delete next.modules![issue.module];
|
|
141
|
+
}
|
|
142
|
+
} else if (issue.code === 'BINDING_UNRESOLVED' && issue.service) {
|
|
143
|
+
const s = next.services[issue.service];
|
|
144
|
+
if (!s) continue;
|
|
145
|
+
// 守卫:引用链任一候选 ∈ installedVendors → 活厂商错配,保留(doctor 报错语义不变;
|
|
146
|
+
// v6 引用链 = 实例级/槽级/modules[ownerOf]/root 四层)
|
|
147
|
+
const owner = walkOwnership[issue.service];
|
|
148
|
+
const candidates = [
|
|
149
|
+
issue.instance ? providerOf(s.instances?.[issue.instance]?.provider) : undefined,
|
|
150
|
+
providerOf(s.provider),
|
|
151
|
+
owner !== undefined ? providerOf((next.modules as Record<string, { provider?: unknown }> | undefined)?.[owner]?.provider) : undefined,
|
|
152
|
+
providerOf(next.provider),
|
|
153
|
+
].filter((v): v is string => typeof v === 'string');
|
|
154
|
+
if (candidates.some((v) => installedVendors.has(v))) continue;
|
|
155
|
+
if (issue.instance) {
|
|
156
|
+
const inst = s.instances;
|
|
157
|
+
if (!inst || !(issue.instance in inst)) continue;
|
|
158
|
+
delete inst[issue.instance];
|
|
159
|
+
removed.push(`services.${issue.service}.instances.${issue.instance}`);
|
|
160
|
+
if (Object.keys(inst).length === 0) delete s.instances;
|
|
161
|
+
if (Object.keys(s).length === 0) delete next.services[issue.service];
|
|
162
|
+
} else {
|
|
163
|
+
delete next.services[issue.service];
|
|
164
|
+
removed.push(`services.${issue.service}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (Object.keys(next.services).length === 0) delete next.services;
|
|
169
|
+
if (removed.length === 0) return { removed, orphanCredentials: [] };
|
|
170
|
+
|
|
171
|
+
// 全空判定先行(**先于严格校验**——空文档 `{}` 缺必填 services 键必然被 zod 拒;删文件免校验)
|
|
172
|
+
const empty =
|
|
173
|
+
!next.provider &&
|
|
174
|
+
Object.keys(next.services ?? {}).length === 0 &&
|
|
175
|
+
Object.keys(next.config ?? {}).length === 0 &&
|
|
176
|
+
Object.keys(next.configByInstance ?? {}).length === 0 &&
|
|
177
|
+
!next.credentialRef &&
|
|
178
|
+
Object.keys(next.credentialRefByInstance ?? {}).length === 0 &&
|
|
179
|
+
(next.instances ?? []).length === 0 &&
|
|
180
|
+
next.defaultInstanceId == null &&
|
|
181
|
+
Object.keys(next.modules ?? {}).length === 0;
|
|
182
|
+
|
|
183
|
+
// ④ 严格校验(仅非空文档——services 为 schema 必填键,全服务剪除的存活文档补空 record 归一)
|
|
184
|
+
if (!empty) {
|
|
185
|
+
next.services ??= {};
|
|
186
|
+
if (typeof contracts.module.parseIntegrationsConfig === 'function') {
|
|
187
|
+
try {
|
|
188
|
+
contracts.module.parseIntegrationsConfig(next as never);
|
|
189
|
+
} catch {
|
|
190
|
+
return { removed: [], orphanCredentials: [], skipped: 'validation-failed' };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ⑤⑥ 写 + 孤儿报告(D9);空 → 删文件(ensureMockBindings 重装再生闭环)
|
|
196
|
+
let orphanCredentials: string[] = [];
|
|
197
|
+
if (empty) {
|
|
198
|
+
if (existsSync(file)) rmSync(file, { force: true });
|
|
199
|
+
} else {
|
|
200
|
+
writeAtomic(file, `${JSON.stringify(next, null, 2)}\n`);
|
|
201
|
+
}
|
|
202
|
+
orphanCredentials = reconcileOrphanCredentials(appDir).map((o) => o.message);
|
|
203
|
+
|
|
204
|
+
return { removed, orphanCredentials };
|
|
205
|
+
}
|
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
import type { IntegrationServiceIssue } from './predicate.js';
|
|
20
20
|
import { checkInstanceKeys } from './predicate.js';
|
|
21
21
|
import { isKnownService } from './read.js';
|
|
22
|
-
import {
|
|
22
|
+
import { moduleOwnershipWritable } from './module-binding.js';
|
|
23
23
|
import { AppToolkitError } from '../core/errors.js';
|
|
24
|
+
import { MODULE_INTEGRATION_NODE_KEYS } from '../dto.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* 写核心(C3-4 write;FX-1c 管线重排——F3 九步定序 + D-B2 语义 + F4 输入扩位):
|
|
@@ -231,7 +232,8 @@ export interface WriteCore {
|
|
|
231
232
|
warmup(): Promise<void>;
|
|
232
233
|
/** PUT /app/integration(root 节点全集) */
|
|
233
234
|
applyApp(node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
|
|
234
|
-
/** PUT /modules/:module/integration(
|
|
235
|
+
/** PUT /modules/:module/integration(v6 modules[owner] 集成位;键集纪律见 MODULE_INTEGRATION_NODE_KEYS;
|
|
236
|
+
* N1 {} 清 provider/config 保 resources;moduleOwnershipWritable 写守卫单源) */
|
|
235
237
|
applyModule(moduleId: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
|
|
236
238
|
/** PUT /services/:service/integration(整节点含 instances map;N3 {} 不归一化;v4.4 A2 身份判据) */
|
|
237
239
|
applyService(service: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
|
|
@@ -336,7 +338,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
336
338
|
message: 'config/integrations.json 缺席——已物化骨架(写时自动初始化;R13 语义翻转提示)',
|
|
337
339
|
});
|
|
338
340
|
}
|
|
339
|
-
// root 全量替换(D14 无例外语义):缺席根键删除(services/
|
|
341
|
+
// root 全量替换(D14 无例外语义):缺席根键删除(services/modules 域不在根键集——随 base 保留;v6 domains 键已退役),
|
|
340
342
|
// node 全键置入(未知键交严格 zod 拒——fail-visible;B10:原实现 spread+merge 残留 stale 根键)
|
|
341
343
|
const next: Record<string, unknown> = { ...base };
|
|
342
344
|
for (const k of APP_INTEGRATION_NODE_KEYS) if (!(k in node)) delete next[k];
|
|
@@ -370,37 +372,27 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
370
372
|
|
|
371
373
|
applyModule(moduleId: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
|
|
372
374
|
assertSafeNames(node, 'root');
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
const { modules, catalog } = walkManifests(appDir, cache);
|
|
375
|
+
// v6 写守卫(moduleOwnershipWritable 单源):404(模块在册)→ 400(zero-ownership——
|
|
376
|
+
// 模块级集成位作用于 declare.owns 服务,零归属模块无配置位)→ 既有管线。
|
|
377
|
+
// 守卫先于 no-op 短路——零归属模块 PUT {} 清位同 400(「写路径拒绝歧义操作」含清除)。
|
|
378
|
+
const { modules, catalog, ownership } = walkManifests(appDir, cache);
|
|
377
379
|
const mod = modules.find((m) => m.id === moduleId);
|
|
378
380
|
if (!mod) {
|
|
379
381
|
throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
|
|
380
382
|
}
|
|
381
|
-
const w =
|
|
383
|
+
const w = moduleOwnershipWritable({ modules, catalog, ownership }, mod);
|
|
382
384
|
if (!w.writable) {
|
|
383
|
-
if (w.reason === 'multi-domain') {
|
|
384
|
-
throw new AppToolkitError(
|
|
385
|
-
'VALIDATION_FAILED',
|
|
386
|
-
400,
|
|
387
|
-
`模块 ${moduleId} 跨 ${w.domainKeys.length} 个域(${w.domainKeys.join('、')})——模块级统一配置仅支持单域模块,请按服务级配置(编辑各服务行)`,
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
if (w.reason === 'zero-domain') {
|
|
391
|
-
throw new AppToolkitError('VALIDATION_FAILED', 400, `模块 ${moduleId} 无服务需求声明,无域配置位`);
|
|
392
|
-
}
|
|
393
385
|
throw new AppToolkitError(
|
|
394
386
|
'VALIDATION_FAILED',
|
|
395
387
|
400,
|
|
396
|
-
|
|
388
|
+
`模块 ${moduleId} 未声明任何 declare.owns——无模块级配置位(模块级 provider/config 作用于归属服务)`,
|
|
397
389
|
);
|
|
398
390
|
}
|
|
399
|
-
const domainKey = w.domainKey;
|
|
400
391
|
const issues: IntegrationServiceIssue[] = [];
|
|
401
392
|
const current = loadConfig(appDir, cache);
|
|
393
|
+
// N2 语义(v6):空节点 = 清 provider/config 两键,resources 随既有节点保全;
|
|
394
|
+
// 文件缺席 + 空节点 = no-op(幂等)
|
|
402
395
|
if (current === null && Object.keys(node).length === 0) {
|
|
403
|
-
// 空文件清空域 = no-op(N1 幂等)
|
|
404
396
|
return { written: false, restartScheduled: false, files: [], credentials: [], issues };
|
|
405
397
|
}
|
|
406
398
|
if (current === null) {
|
|
@@ -411,15 +403,39 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
411
403
|
});
|
|
412
404
|
}
|
|
413
405
|
const base: Record<string, unknown> = current ?? { services: {} };
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
406
|
+
const modulesNode = { ...((base.modules as Record<string, unknown>) ?? {}) };
|
|
407
|
+
const existing = { ...((modulesNode[moduleId] as Record<string, unknown> | undefined) ?? {}) };
|
|
408
|
+
// 键集纪律:node 内仅接受 MODULE_INTEGRATION_NODE_KEYS 成员(未知键交严格 zod 拒——fail-visible)
|
|
409
|
+
for (const k of Object.keys(node)) {
|
|
410
|
+
if (!(MODULE_INTEGRATION_NODE_KEYS as readonly string[]).includes(k)) {
|
|
411
|
+
throw new AppToolkitError(
|
|
412
|
+
'VALIDATION_FAILED',
|
|
413
|
+
400,
|
|
414
|
+
`模块集成位仅接受 ${MODULE_INTEGRATION_NODE_KEYS.join('/')} 键(收到 ${k})`,
|
|
415
|
+
[k],
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// 全量替换(键集内):缺席键删除;空对象/空串值 = 删除(≡ 缺席——模块节点不留空键);
|
|
420
|
+
// PUT {} = 清 provider/config,resources 恒随 existing 保全(N2:清集成位不杀资源绑定)
|
|
421
|
+
const nextNode: Record<string, unknown> = { ...existing };
|
|
422
|
+
for (const k of MODULE_INTEGRATION_NODE_KEYS) {
|
|
423
|
+
const v = node[k];
|
|
424
|
+
const isEmptyish = v === undefined || v === null || v === ''
|
|
425
|
+
|| (typeof v === 'object' && !Array.isArray(v) && Object.keys(v as object).length === 0);
|
|
426
|
+
if (isEmptyish) delete nextNode[k];
|
|
427
|
+
else nextNode[k] = v;
|
|
428
|
+
}
|
|
429
|
+
if (Object.keys(nextNode).length === 0) {
|
|
430
|
+
delete modulesNode[moduleId];
|
|
431
|
+
} else {
|
|
432
|
+
modulesNode[moduleId] = nextNode;
|
|
433
|
+
}
|
|
434
|
+
const next: Record<string, unknown> = { ...base, modules: modulesNode };
|
|
419
435
|
if (deepEqual(current, next)) {
|
|
420
436
|
return { written: false, restartScheduled: false, files: [], credentials: [], issues };
|
|
421
437
|
}
|
|
422
|
-
runStrictValidation(next, issues, `
|
|
438
|
+
runStrictValidation(next, issues, `modules.${moduleId}.`);
|
|
423
439
|
issues.push(...registryCheck(next));
|
|
424
440
|
return finalize(next, [], issues, writeOpts.dryRun);
|
|
425
441
|
},
|
package/src/views/app.ts
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import type { AppView, ServiceDemand } from '../dto.js';
|
|
2
2
|
import { APP_INTEGRATION_NODE_KEYS } from '../dto.js';
|
|
3
|
-
import { moduleOccupiedServices, type ViewSnapshot } from './context.js';
|
|
3
|
+
import { moduleOccupiedServices, resolveServiceDisplay, type ViewSnapshot } from './context.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* 应用静态投影(views/app;C4;FX-1c B10——root 节点投影修;v4.4 D29——services 双端点同型)。
|
|
7
7
|
* /app = 平台服务 + root 节点原值(纯静态):services = 服务词汇 − 模块占用
|
|
8
|
-
* (ServiceDemand[]——平台条目 required 恒 false、
|
|
8
|
+
* (ServiceDemand[]——平台条目 required 恒 false、name/description 缺席,D29);
|
|
9
9
|
* 平台服务状态不在此(→ /service-resolutions);模块服务不在此(→ /modules/:m)。
|
|
10
10
|
* integration = **root 节点投影**(APP_INTEGRATION_NODE_KEYS——D21/L1:GET 预填 ≡ PUT body;
|
|
11
|
-
* 文件另含 services/
|
|
11
|
+
* 文件另含 services/modules 域键,不进 root 节点投影——原实现整文件透传,
|
|
12
12
|
* PUT 回会级联写回域键);文件缺席 = null,文件在场 → 投影对象(可为空)。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
export function loadAppView(snapshot: ViewSnapshot): AppView {
|
|
16
16
|
const occupied = moduleOccupiedServices(snapshot.walk);
|
|
17
|
+
// 平台条目展示名走链(B1):词汇 meta > demand meta > id——name 恒发(id 兜底内聚)
|
|
17
18
|
const services: ServiceDemand[] = snapshot.walk.vocabulary
|
|
18
19
|
.filter((s) => !occupied.has(s))
|
|
19
|
-
.map((s) =>
|
|
20
|
+
.map((s) => {
|
|
21
|
+
const display = resolveServiceDisplay(snapshot.walk, s);
|
|
22
|
+
return { service: s, name: display.name, ...(display.description !== undefined ? { description: display.description } : {}), required: false };
|
|
23
|
+
});
|
|
20
24
|
const config = snapshot.integrations;
|
|
21
25
|
let integration: AppView['integration'] = null;
|
|
22
26
|
if (config) {
|
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
|
}
|
|
@@ -65,3 +64,23 @@ export function suppliedByVendors(catalog: WalkCatalog, service: string): string
|
|
|
65
64
|
}
|
|
66
65
|
return vendors.sort();
|
|
67
66
|
}
|
|
67
|
+
|
|
68
|
+
/** 服务展示名解析结果(name 恒有值——id 兜底内聚;v7 词汇统一 title→name) */
|
|
69
|
+
export interface ServiceDisplay {
|
|
70
|
+
name: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 服务展示名单源(B1):词汇 meta(slotMeta——contracts-mall 词汇锁保证 mall 域覆盖)
|
|
76
|
+
* > demand meta(catalog.services——域外槽兜底;首声明聚合)> service id。逐字段回落——
|
|
77
|
+
* 词汇条目仅含 description 时 demand name 仍可达(外部契约包部分 meta 前向容错)。
|
|
78
|
+
* 四视图唯一消费点(/modules/:m services、/app 平台条目、/services/:s、/providers/:p services)。
|
|
79
|
+
*/
|
|
80
|
+
export function resolveServiceDisplay(walk: WalkManifestsResult, service: string): ServiceDisplay {
|
|
81
|
+
const vocab = walk.slotMeta[service];
|
|
82
|
+
const demand = walk.catalog.services[service];
|
|
83
|
+
const name = vocab?.name ?? demand?.name ?? service;
|
|
84
|
+
const description = vocab?.description ?? demand?.description;
|
|
85
|
+
return { name, ...(description !== undefined ? { description } : {}) };
|
|
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
|
+
}
|