@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
|
@@ -9,15 +9,23 @@ import { readAppManifestFull, type NpmModuleEntry, type AppManifestFull } from '
|
|
|
9
9
|
*
|
|
10
10
|
* manifest 候选路径探测(packages/<id>/tbox.module.json 优先 → node_modules/<pkg>/tbox.module.json;
|
|
11
11
|
* tbox-app dev 真身经 .tbox-dev.local/dev-manifest.json redirects 独立 catalog 通道直达)一次遍历同时产出:
|
|
12
|
-
* 1. modules:已装模块清单(declared;demand meta
|
|
12
|
+
* 1. modules:已装模块清单(declared;demand meta name/description 透传)
|
|
13
13
|
* 2. catalog(富形态):providers 嵌套(vendor→impl→entry,per-service {implementation, credentialType}
|
|
14
14
|
* 保真——同厂商异构/多实现合法,D25/H1)+ 需求侧聚合
|
|
15
15
|
* 3. serviceSchemas:per-service schema 复合键表——键 `${provider}\u0000${service}`(H2,同一服务
|
|
16
16
|
* 多厂商供给各自 schema 零碰撞);多 impl 同 (provider, service) 异 schema → 声明序首个
|
|
17
17
|
* 4. vocabulary:所有携带 service-slots.json 的已装契约包该文件 `.slots` 键并集(fs 直读、
|
|
18
|
-
* `$comment`
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* `$comment` 忽略;探测三跳 = ① <候选>/node_modules/@tbox.cn/<pkg>/service-slots.json(包根)
|
|
19
|
+
* ② 同包 src/service-slots.json(发布包 src 附带 / 模板 shim 链接)
|
|
20
|
+
* ③ 候选自身 {service-slots.json, src/service-slots.json}(local/codegen 全量副本——
|
|
21
|
+
* mall 生成应用主形态,不依赖 install);零缓存枚举,与 manifest 探测同机制;
|
|
22
|
+
* 多行业 contracts-* 天然扩展 F8);缺席 → [](退化 demanded ∪ configured)
|
|
23
|
+
* 5. slotMeta:词汇 meta 节(per-service {name,description} 展示元数据——消费端兜底链
|
|
24
|
+
* 词汇 > demand > id,views/context resolveServiceDisplay 单源);多包发现序首声明胜出
|
|
25
|
+
* (与 catalog addSlots 同语义);contracts-mall 词汇锁保证 mall 域全量覆盖;缺席 → {}
|
|
26
|
+
* 6. ownership:service → moduleId 归属表(v6——declare.owns 聚合;install id 键控;
|
|
27
|
+
* 双 owner 冲突 → declarationIssues OWNERSHIP_CONFLICT + 先声明胜出)
|
|
28
|
+
* 7. declarationIssues:声明面诊断(OWNERSHIP_CONFLICT / OLD_MANIFEST_KEY——P3 通道)
|
|
21
29
|
*
|
|
22
30
|
* **约束(D26;FX-3/F7 边界)**:目录枚举零缓存——readdir / node_modules 候选探测 / `.tbox/app.json` 读取 /
|
|
23
31
|
* dev-manifest 读取每次执行(枚举输入保持 fresh——agent 装卸模块下一笔即见);manifest/service-slots
|
|
@@ -26,6 +34,11 @@ import { readAppManifestFull, type NpmModuleEntry, type AppManifestFull } from '
|
|
|
26
34
|
* 宽松解析语义(坏文件静默 + 重复键首声明保留)零改动。
|
|
27
35
|
*/
|
|
28
36
|
|
|
37
|
+
/** 声明面诊断(v6 P3 通道——OWNERSHIP_CONFLICT / OLD_MANIFEST_KEY;error 级) */
|
|
38
|
+
export type DeclarationIssue =
|
|
39
|
+
| { severity: 'error'; code: 'OWNERSHIP_CONFLICT'; message: string; module?: string }
|
|
40
|
+
| { severity: 'error'; code: 'OLD_MANIFEST_KEY'; message: string; module?: string };
|
|
41
|
+
|
|
29
42
|
export interface WalkManifestsResult {
|
|
30
43
|
/** declared 产物(install id + descriptor;demand meta 透传) */
|
|
31
44
|
modules: WalkModuleInfo[];
|
|
@@ -35,8 +48,49 @@ export interface WalkManifestsResult {
|
|
|
35
48
|
serviceSchemas: Record<string, { configSchema?: string; credentialSchema?: string; packageDir: string }>;
|
|
36
49
|
/** 服务词汇 = 所有携带 service-slots.json 的已装契约包 `.slots` 键并集(缺席 → []) */
|
|
37
50
|
vocabulary: string[];
|
|
38
|
-
/**
|
|
39
|
-
|
|
51
|
+
/** 词汇 meta 节:per-service {name,description} 展示元数据(多包首声明胜出;缺席 → {}) */
|
|
52
|
+
slotMeta: Record<string, SlotMeta>;
|
|
53
|
+
/** v6 服务归属表:service → moduleId(install id 键控——integrations modules 节键同轴;
|
|
54
|
+
* 双 owner 冲突 → declarationIssues + 先声明胜出;dev 通道以 manifest id 参与归属) */
|
|
55
|
+
ownership: Record<string, string>;
|
|
56
|
+
/** 声明面诊断(OWNERSHIP_CONFLICT / OLD_MANIFEST_KEY) */
|
|
57
|
+
declarationIssues: DeclarationIssue[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 槽位展示元数据(service-slots.json meta 节元素——宽松提取,字段皆可选;v7 词汇统一 title→name) */
|
|
61
|
+
export interface SlotMeta {
|
|
62
|
+
name?: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 词汇文件单次解析产物(slots 与 meta 同源同读——防双读漂移) */
|
|
67
|
+
interface SlotsFile {
|
|
68
|
+
slots: string[];
|
|
69
|
+
meta: Record<string, SlotMeta>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 词汇文件解析(宽松:坏文件/缺 slots 数组 → null;meta 非对象条目/无有效字段跳过) */
|
|
73
|
+
function parseSlotsFile(raw: string): SlotsFile | null {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(raw) as { slots?: unknown; meta?: unknown };
|
|
76
|
+
if (!Array.isArray(parsed.slots)) return null;
|
|
77
|
+
const slots = parsed.slots.filter((s): s is string => typeof s === 'string');
|
|
78
|
+
const meta: Record<string, SlotMeta> = {};
|
|
79
|
+
if (parsed.meta && typeof parsed.meta === 'object' && !Array.isArray(parsed.meta)) {
|
|
80
|
+
for (const [svc, entry] of Object.entries(parsed.meta as Record<string, unknown>)) {
|
|
81
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
82
|
+
const { name, description } = entry as { name?: unknown; description?: unknown };
|
|
83
|
+
const m: SlotMeta = {};
|
|
84
|
+
if (typeof name === 'string' && name.length > 0) m.name = name;
|
|
85
|
+
if (typeof description === 'string' && description.length > 0) m.description = description;
|
|
86
|
+
if (m.name !== undefined || m.description !== undefined) meta[svc] = m;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { slots, meta };
|
|
90
|
+
} catch {
|
|
91
|
+
/* 宽松:坏文件跳过 */
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
40
94
|
}
|
|
41
95
|
|
|
42
96
|
export interface WalkModuleInfo {
|
|
@@ -49,12 +103,17 @@ export interface WalkModuleInfo {
|
|
|
49
103
|
descriptor: ModuleDescriptor;
|
|
50
104
|
}
|
|
51
105
|
|
|
52
|
-
/** 富形态供给条目:per-service {implementation, credentialType} 恒数组保真(H1
|
|
106
|
+
/** 富形态供给条目:per-service {implementation, credentialType} 恒数组保真(H1);
|
|
107
|
+
* name/description = 厂商展示元数据(strict manifest descriptor 透传——多包同厂商
|
|
108
|
+
* 首声明胜出,与 owner 同语义;B1 walk 层传播;v7 词汇统一 title→name) */
|
|
53
109
|
export interface WalkProviderEntry {
|
|
54
110
|
provider: string;
|
|
55
111
|
implementation: string;
|
|
56
112
|
credentialType: string;
|
|
57
113
|
owner: string;
|
|
114
|
+
/** 厂商展示名(manifest name——多包首声明;缺席 = undefined) */
|
|
115
|
+
name?: string;
|
|
116
|
+
description?: string;
|
|
58
117
|
local?: boolean;
|
|
59
118
|
configSchema?: string;
|
|
60
119
|
credentialSchema?: string;
|
|
@@ -68,8 +127,8 @@ export interface WalkCatalog {
|
|
|
68
127
|
{
|
|
69
128
|
optional: boolean;
|
|
70
129
|
defaults: Array<{ provider: string; implementation: string }>;
|
|
71
|
-
/** demand meta(
|
|
72
|
-
|
|
130
|
+
/** demand meta(name/description 首声明保留——模块 manifest demand 侧 additive;v7 title→name) */
|
|
131
|
+
name?: string;
|
|
73
132
|
description?: string;
|
|
74
133
|
owners: string[];
|
|
75
134
|
}
|
|
@@ -77,25 +136,28 @@ export interface WalkCatalog {
|
|
|
77
136
|
credentialTypes: Record<string, { owner: string; packageDir: string; credentialSchema?: string }>;
|
|
78
137
|
}
|
|
79
138
|
|
|
80
|
-
/** loose 允许的
|
|
81
|
-
interface
|
|
139
|
+
/** loose 允许的 declare 局部结构(本地声明——解跨模块类型身份耦合,TS2719 防线) */
|
|
140
|
+
interface LooseDeclare {
|
|
82
141
|
providers?: { slots?: ProviderSlotContrib[] };
|
|
83
|
-
|
|
142
|
+
consumes?: Array<{
|
|
84
143
|
service: string;
|
|
85
144
|
optional?: boolean;
|
|
86
145
|
defaults?: Array<{ provider: string; implementation: string }>;
|
|
87
|
-
|
|
146
|
+
name?: string;
|
|
88
147
|
description?: string;
|
|
89
148
|
}>;
|
|
90
149
|
}
|
|
91
150
|
|
|
92
|
-
/** manifest 读取结果(判别联合——strict = 完整 descriptor;loose =
|
|
151
|
+
/** manifest 读取结果(判别联合——strict = 完整 descriptor;loose = 部分形态宽容提取;
|
|
152
|
+
* legacy = 旧键(v6 声明面 / v7 词汇统一,parseModuleDescriptor legacy 标志透传)——
|
|
153
|
+
* 拒 loose 静默兜底,定向 issue)。 */
|
|
93
154
|
type ReadManifestResult =
|
|
94
|
-
| { kind: 'strict';
|
|
95
|
-
| { kind: 'loose';
|
|
155
|
+
| { kind: 'strict'; id: string; version: string; descriptor: ModuleDescriptor; declare: ModuleDescriptor['declare'] }
|
|
156
|
+
| { kind: 'loose'; id?: string; declare: LooseDeclare }
|
|
157
|
+
| { kind: 'legacy'; detail: string };
|
|
96
158
|
|
|
97
|
-
/** 读单文件 manifest(坏文件 → null;宽松——strict
|
|
98
|
-
*
|
|
159
|
+
/** 读单文件 manifest(坏文件 → null;宽松——strict 解析失败但为新形态 declare → loose 兜底;
|
|
160
|
+
* 旧键 → legacy(定向 issue,静默兜底防线——单源 = parseModuleDescriptor legacy 标志)。
|
|
99
161
|
* FX-3(F7 边界):**文件内容**经 file-cache(stat 指纹——外部写下一笔自愈);目录枚举仍零缓存。 */
|
|
100
162
|
function readManifestFile(file: string, cache?: FileCache): ReadManifestResult | null {
|
|
101
163
|
const parse = (raw: string): ReadManifestResult | null => {
|
|
@@ -103,17 +165,19 @@ function readManifestFile(file: string, cache?: FileCache): ReadManifestResult |
|
|
|
103
165
|
const parsedRaw: unknown = JSON.parse(raw);
|
|
104
166
|
const parsed = parseModuleDescriptor(parsedRaw);
|
|
105
167
|
if (parsed.ok) {
|
|
106
|
-
return { kind: 'strict',
|
|
168
|
+
return { kind: 'strict', id: parsed.value.id, version: parsed.value.version, descriptor: parsed.value, declare: parsed.value.declare };
|
|
107
169
|
}
|
|
108
|
-
//
|
|
109
|
-
if (
|
|
110
|
-
|
|
170
|
+
// 旧键(v6/v7):parseModuleDescriptor 已定向检测(legacy 标志)——拒 loose 静默兜底
|
|
171
|
+
if (parsed.legacy) return { kind: 'legacy', detail: parsed.errors.join('; ') };
|
|
172
|
+
// loose 兜底:strict 拒(部分形态 manifest——如仅 declare)但有新形态 declare → 宽松提取
|
|
173
|
+
if (parsedRaw && typeof parsedRaw === 'object' && 'declare' in parsedRaw) {
|
|
174
|
+
const obj = parsedRaw as { id?: unknown; declare?: LooseDeclare };
|
|
111
175
|
return {
|
|
112
176
|
kind: 'loose',
|
|
113
|
-
...(typeof obj.
|
|
114
|
-
|
|
115
|
-
providers:
|
|
116
|
-
|
|
177
|
+
...(typeof obj.id === 'string' ? { id: obj.id } : {}),
|
|
178
|
+
declare: {
|
|
179
|
+
providers: obj.declare?.providers,
|
|
180
|
+
consumes: obj.declare?.consumes,
|
|
117
181
|
},
|
|
118
182
|
};
|
|
119
183
|
}
|
|
@@ -174,10 +238,15 @@ function readDevManifestRedirects(appDir: string): DevManifestRedirect[] {
|
|
|
174
238
|
}
|
|
175
239
|
}
|
|
176
240
|
|
|
177
|
-
/**
|
|
178
|
-
*
|
|
179
|
-
|
|
180
|
-
|
|
241
|
+
/** 契约包发现链(探测三跳——词汇源真实布局全覆盖,B0):候选集(appDir、apps/*、packages/*)×
|
|
242
|
+
* ① node_modules/@tbox.cn/<pkg>/service-slots.json(包根——夹具/旧布局)
|
|
243
|
+
* ② 同包 src/service-slots.json(发布包 src 随 files 附带 / 模板 shim 链接到仓库源 src/)
|
|
244
|
+
* ③ 候选自身 {service-slots.json, src/service-slots.json}(local/codegen 全量副本——mall 生成应用
|
|
245
|
+
* 主形态,stage rewriteExportsToSrc 展开后永驻 packages/*,不依赖 install)
|
|
246
|
+
* 同包双在场(异常态)root 优先。slots 与 meta 单次解析同产(目录枚举零缓存,F8;
|
|
247
|
+
* FX-3:文件**内容**经 file-cache)。 */
|
|
248
|
+
function discoverContractPackages(appDir: string, cache?: FileCache): Array<{ dir: string; slots: string[]; meta: Record<string, SlotMeta> }> {
|
|
249
|
+
const found: Array<{ dir: string; slots: string[]; meta: Record<string, SlotMeta> }> = [];
|
|
181
250
|
const candidates: Array<string> = [appDir];
|
|
182
251
|
for (const sub of ['apps', 'packages']) {
|
|
183
252
|
const parent = join(appDir, sub);
|
|
@@ -191,7 +260,12 @@ function discoverContractPackages(appDir: string, cache?: FileCache): Array<{ di
|
|
|
191
260
|
}
|
|
192
261
|
}
|
|
193
262
|
const seenRoots = new Set<string>();
|
|
263
|
+
const addDiscovered = (dir: string, file: string): void => {
|
|
264
|
+
const parsed = cache ? cache.read(file, parseSlotsFile) : parseSlotsFile(readFileSync(file, 'utf8'));
|
|
265
|
+
if (parsed !== null && parsed !== undefined) found.push({ dir, slots: parsed.slots, meta: parsed.meta });
|
|
266
|
+
};
|
|
194
267
|
for (const dir of candidates) {
|
|
268
|
+
// 跳 1/2:node_modules 位(包根 → src——显式安装真源优先)
|
|
195
269
|
const scopeDir = join(dir, 'node_modules', '@tbox.cn');
|
|
196
270
|
if (!existsSync(scopeDir)) continue;
|
|
197
271
|
let names: string[] = [];
|
|
@@ -204,19 +278,16 @@ function discoverContractPackages(appDir: string, cache?: FileCache): Array<{ di
|
|
|
204
278
|
const pkgDir = join(scopeDir, name);
|
|
205
279
|
if (seenRoots.has(pkgDir)) continue;
|
|
206
280
|
seenRoots.add(pkgDir);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
};
|
|
218
|
-
const slots = cache ? cache.read(slotsFile, parseSlots) : parseSlots(readFileSync(slotsFile, 'utf8'));
|
|
219
|
-
if (slots !== null && slots !== undefined) found.push({ dir: pkgDir, slots });
|
|
281
|
+
const rootFile = join(pkgDir, 'service-slots.json');
|
|
282
|
+
const srcFile = join(pkgDir, 'src', 'service-slots.json');
|
|
283
|
+
if (existsSync(rootFile)) addDiscovered(pkgDir, rootFile);
|
|
284
|
+
else if (existsSync(srcFile)) addDiscovered(pkgDir, srcFile);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// 跳 3:候选自身(local/codegen 全量副本形态——后置于 nm 位:真源 > 副本,meta 首声明序随此)
|
|
288
|
+
for (const dir of candidates) {
|
|
289
|
+
for (const file of [join(dir, 'service-slots.json'), join(dir, 'src', 'service-slots.json')]) {
|
|
290
|
+
if (existsSync(file)) addDiscovered(dir, file);
|
|
220
291
|
}
|
|
221
292
|
}
|
|
222
293
|
return found;
|
|
@@ -227,12 +298,11 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
|
|
|
227
298
|
const modules: WalkModuleInfo[] = [];
|
|
228
299
|
const catalog: WalkCatalog = { providers: {}, services: {}, credentialTypes: {} };
|
|
229
300
|
const serviceSchemas: WalkManifestsResult['serviceSchemas'] = {};
|
|
230
|
-
const
|
|
301
|
+
const declarationIssues: WalkManifestsResult['declarationIssues'] = [];
|
|
302
|
+
const ownership: Record<string, string> = {};
|
|
231
303
|
const seen = new Set<string>();
|
|
232
|
-
/** 域键 → 首个声明模块(P13 冲突检测) */
|
|
233
|
-
const domainOwners = new Map<string, string>();
|
|
234
304
|
|
|
235
|
-
const addSlots = (slots: readonly ProviderSlotContrib[], owner: string, packageDir: string): void => {
|
|
305
|
+
const addSlots = (slots: readonly ProviderSlotContrib[], owner: string, packageDir: string, modMeta?: SlotMeta): void => {
|
|
236
306
|
for (const slot of slots) {
|
|
237
307
|
// 富形态:per-service {implementation, credentialType} 保真(同厂商多 impl 合法——声明序)
|
|
238
308
|
const vendorSlots = (catalog.providers[slot.provider] ??= {});
|
|
@@ -245,6 +315,8 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
|
|
|
245
315
|
implementation: slot.implementation,
|
|
246
316
|
credentialType: slot.credentialType,
|
|
247
317
|
owner,
|
|
318
|
+
...(modMeta?.name ? { name: modMeta.name } : {}),
|
|
319
|
+
...(modMeta?.description ? { description: modMeta.description } : {}),
|
|
248
320
|
...(slot.local ? { local: true } : {}),
|
|
249
321
|
...(slot.configSchema ? { configSchema: slot.configSchema } : {}),
|
|
250
322
|
...(slot.credentialSchema ? { credentialSchema: slot.credentialSchema } : {}),
|
|
@@ -268,8 +340,50 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
|
|
|
268
340
|
}
|
|
269
341
|
};
|
|
270
342
|
|
|
271
|
-
|
|
272
|
-
|
|
343
|
+
/** 厂商展示元数据(strict manifest descriptor 透传——loose 形态无 descriptor → undefined) */
|
|
344
|
+
const modMetaOf = (manifest: ReadManifestResult): SlotMeta | undefined =>
|
|
345
|
+
manifest.kind === 'strict' ? { name: manifest.descriptor.name, description: manifest.descriptor.description } : undefined;
|
|
346
|
+
|
|
347
|
+
/** v6 归属聚合:declare.owns → ownership 表(先声明胜出;冲突 → OWNERSHIP_CONFLICT)。
|
|
348
|
+
* owner 键 = install id(integrations modules 节键同轴——npmModules 通道 = entry.id、
|
|
349
|
+
* packages/dev 通道 = manifest id)。 */
|
|
350
|
+
const addOwns = (services: readonly string[], owner: string): void => {
|
|
351
|
+
for (const service of services) {
|
|
352
|
+
const first = ownership[service];
|
|
353
|
+
if (first === undefined) {
|
|
354
|
+
ownership[service] = owner;
|
|
355
|
+
} else if (first !== owner) {
|
|
356
|
+
declarationIssues.push({
|
|
357
|
+
severity: 'error',
|
|
358
|
+
code: 'OWNERSHIP_CONFLICT',
|
|
359
|
+
module: owner,
|
|
360
|
+
message: `服务 ${service} 归属冲突:${first} 与 ${owner} 均声明 owns(先声明 ${first} 胜出)——修正 manifest declare.owns 互斥`,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/** 旧键 legacy manifest → 定向 issue(N1:拒 loose 静默兜底)。 */
|
|
367
|
+
const pushLegacyIssue = (manifest: ReadManifestResult, module: string): void => {
|
|
368
|
+
if (manifest.kind !== 'legacy') return;
|
|
369
|
+
declarationIssues.push({
|
|
370
|
+
severity: 'error',
|
|
371
|
+
code: 'OLD_MANIFEST_KEY',
|
|
372
|
+
module,
|
|
373
|
+
message: `${module}:${manifest.detail}`,
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/** declare 提取(strict/loose 双形态统一视图;legacy → null) */
|
|
378
|
+
const declareOf = (manifest: ReadManifestResult): LooseDeclare | null =>
|
|
379
|
+
manifest.kind === 'strict'
|
|
380
|
+
? manifest.declare
|
|
381
|
+
: manifest.kind === 'loose'
|
|
382
|
+
? manifest.declare
|
|
383
|
+
: null;
|
|
384
|
+
|
|
385
|
+
const addServiceConsume = (
|
|
386
|
+
need: { service: string; optional?: boolean; defaults?: Array<{ provider: string; implementation: string }>; name?: string; description?: string },
|
|
273
387
|
owner: string,
|
|
274
388
|
): void => {
|
|
275
389
|
const optional = need.optional !== false;
|
|
@@ -291,22 +405,10 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
|
|
|
291
405
|
catalog.services[need.service] = {
|
|
292
406
|
optional,
|
|
293
407
|
defaults: [...defaults],
|
|
294
|
-
...(need.
|
|
408
|
+
...(need.name ? { name: need.name } : {}),
|
|
295
409
|
...(need.description ? { description: need.description } : {}),
|
|
296
410
|
owners: [owner],
|
|
297
411
|
};
|
|
298
|
-
// P13 域键冲突检测(首声明占用;跨模块冲突 → issue)
|
|
299
|
-
const domain = need.service.includes('.') ? need.service.slice(0, need.service.indexOf('.')) : need.service;
|
|
300
|
-
const firstOwner = domainOwners.get(domain);
|
|
301
|
-
if (firstOwner === undefined) domainOwners.set(domain, owner);
|
|
302
|
-
else if (firstOwner !== owner) {
|
|
303
|
-
domainKeyIssues.push({
|
|
304
|
-
severity: 'warning',
|
|
305
|
-
code: 'DOMAIN_KEY_CONFLICT',
|
|
306
|
-
module: owner,
|
|
307
|
-
message: `域键 ${domain} 被多模块声明(${firstOwner} 与 ${owner})——domains 换算歧义(P13)`,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
412
|
};
|
|
311
413
|
|
|
312
414
|
// 通道 1:npmModules 登记(packages/<id> 本地优先 → node_modules/<pkg>)
|
|
@@ -315,39 +417,49 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
|
|
|
315
417
|
const local = join(appDir, 'packages', entry.id);
|
|
316
418
|
const nm = join(appDir, 'node_modules', ...entry.package.split('/'));
|
|
317
419
|
const manifest = readManifestFile(join(local, 'tbox.module.json'), cache) ?? readManifestFile(join(nm, 'tbox.module.json'), cache);
|
|
420
|
+
if (manifest?.kind === 'legacy') {
|
|
421
|
+
pushLegacyIssue(manifest, entry.id);
|
|
422
|
+
seen.add(entry.id); // 防重复报
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
318
425
|
if (!manifest) continue;
|
|
319
426
|
seen.add(entry.id);
|
|
320
427
|
const dir = existsSync(join(local, 'tbox.module.json')) ? local : nm;
|
|
321
|
-
const
|
|
428
|
+
const declare = declareOf(manifest);
|
|
322
429
|
// loose 形态(部分 manifest)不进 modules 清单(descriptor 缺名不可装配化)——仅 catalog 聚合(旧语义等价)
|
|
323
430
|
if (manifest.kind === 'strict') {
|
|
324
431
|
modules.push({ id: entry.id, dir, pkg: entry.mode === 'sdk' ? entry.package : `@app/${entry.id}`, descriptor: manifest.descriptor });
|
|
325
432
|
}
|
|
326
|
-
const slots =
|
|
327
|
-
if (slots.length) addSlots(slots, entry.package, dir);
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
433
|
+
const slots = declare?.providers?.slots ?? [];
|
|
434
|
+
if (slots.length) addSlots(slots, entry.package, dir, modMetaOf(manifest));
|
|
435
|
+
const consumes = declare?.consumes ?? [];
|
|
436
|
+
for (const need of consumes) addServiceConsume(need, entry.package);
|
|
437
|
+
// v6 归属:owner 键 = install id(integrations modules 节同轴)
|
|
438
|
+
const owns = manifest.kind === 'strict' ? manifest.descriptor.declare.owns : [];
|
|
439
|
+
if (owns.length) addOwns(owns.map((o) => o.service), entry.id);
|
|
334
440
|
}
|
|
335
441
|
|
|
336
442
|
// 通道 1.5:tbox-app dev manifest 真身(catalog-only——聚合宽/声明窄不变量:dev 模块
|
|
337
|
-
// 永不进 modules 清单,防 sync 污染受跟踪装配文件;登记真源优先(seen
|
|
443
|
+
// 永不进 modules 清单,防 sync 污染受跟踪装配文件;登记真源优先(seen 按模块名去重))。
|
|
444
|
+
// v6:归属以 manifest id 参与(dev 会话的 modules 集成位键同轴——N3 聚合宽/声明窄)。
|
|
338
445
|
for (const { pkg, dir } of readDevManifestRedirects(appDir)) {
|
|
339
446
|
const manifest = readManifestFile(join(dir, 'tbox.module.json'), cache);
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (slots.length) addSlots(slots, pkg, dir);
|
|
344
|
-
const needs = manifest.contributes.services ?? [];
|
|
345
|
-
if (needs.length) {
|
|
346
|
-
for (const need of needs) addServiceNeed(need, pkg);
|
|
447
|
+
if (manifest?.kind === 'legacy') {
|
|
448
|
+
pushLegacyIssue(manifest, pkg);
|
|
449
|
+
continue;
|
|
347
450
|
}
|
|
451
|
+
if (!manifest?.id || seen.has(manifest.id)) continue;
|
|
452
|
+
seen.add(manifest.id);
|
|
453
|
+
const declare = declareOf(manifest);
|
|
454
|
+
const slots = declare?.providers?.slots ?? [];
|
|
455
|
+
if (slots.length) addSlots(slots, pkg, dir, modMetaOf(manifest));
|
|
456
|
+
const consumes = declare?.consumes ?? [];
|
|
457
|
+
for (const need of consumes) addServiceConsume(need, pkg);
|
|
458
|
+
const owns = manifest.kind === 'strict' ? manifest.descriptor.declare.owns : [];
|
|
459
|
+
if (owns.length) addOwns(owns.map((o) => o.service), manifest.id);
|
|
348
460
|
}
|
|
349
461
|
|
|
350
|
-
// 通道 2:packages/* 约定发现(本地模块——npmModules
|
|
462
|
+
// 通道 2:packages/* 约定发现(本地模块——npmModules 未登记形态,如手工放入的本地模块)
|
|
351
463
|
const packagesDir = join(appDir, 'packages');
|
|
352
464
|
if (existsSync(packagesDir)) {
|
|
353
465
|
let entries: string[] = [];
|
|
@@ -360,32 +472,40 @@ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsR
|
|
|
360
472
|
if (seen.has(name)) continue;
|
|
361
473
|
const dir = join(packagesDir, name);
|
|
362
474
|
const manifest = readManifestFile(join(dir, 'tbox.module.json'), cache);
|
|
475
|
+
if (manifest?.kind === 'legacy') {
|
|
476
|
+
pushLegacyIssue(manifest, name);
|
|
477
|
+
seen.add(name);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
363
480
|
if (!manifest) continue;
|
|
364
|
-
const
|
|
365
|
-
if (seen.has(
|
|
366
|
-
seen.add(
|
|
367
|
-
const
|
|
481
|
+
const modId = manifest.id ?? name;
|
|
482
|
+
if (seen.has(modId)) continue;
|
|
483
|
+
seen.add(modId);
|
|
484
|
+
const declare = declareOf(manifest);
|
|
368
485
|
if (manifest.kind === 'strict') {
|
|
369
|
-
modules.push({ id:
|
|
370
|
-
}
|
|
371
|
-
const slots = contributes.providers?.slots ?? [];
|
|
372
|
-
if (slots.length) addSlots(slots, `@app/${modName}`, dir);
|
|
373
|
-
const needs = contributes.services ?? [];
|
|
374
|
-
if (needs.length) {
|
|
375
|
-
for (const need of needs) {
|
|
376
|
-
addServiceNeed(need, `@app/${modName}`);
|
|
377
|
-
}
|
|
486
|
+
modules.push({ id: modId, dir, pkg: `@app/${modId}`, descriptor: manifest.descriptor });
|
|
378
487
|
}
|
|
488
|
+
const slots = declare?.providers?.slots ?? [];
|
|
489
|
+
if (slots.length) addSlots(slots, `@app/${modId}`, dir, modMetaOf(manifest));
|
|
490
|
+
const consumes = declare?.consumes ?? [];
|
|
491
|
+
for (const need of consumes) addServiceConsume(need, `@app/${modId}`);
|
|
492
|
+
const owns = manifest.kind === 'strict' ? manifest.descriptor.declare.owns : [];
|
|
493
|
+
if (owns.length) addOwns(owns.map((o) => o.service), modId);
|
|
379
494
|
}
|
|
380
495
|
}
|
|
381
496
|
|
|
382
|
-
// 产物 4
|
|
497
|
+
// 产物 4/5:服务词汇 + 词汇 meta(契约包 service-slots.json 并集——零缓存枚举;
|
|
498
|
+
// meta 多包发现序首声明胜出,与 catalog addSlots 同语义)
|
|
383
499
|
const vocabulary = new Set<string>();
|
|
500
|
+
const slotMeta: Record<string, SlotMeta> = {};
|
|
384
501
|
for (const pkg of discoverContractPackages(appDir, cache)) {
|
|
385
502
|
for (const s of pkg.slots) vocabulary.add(s);
|
|
503
|
+
for (const [svc, m] of Object.entries(pkg.meta)) {
|
|
504
|
+
if (slotMeta[svc] === undefined) slotMeta[svc] = m;
|
|
505
|
+
}
|
|
386
506
|
}
|
|
387
507
|
|
|
388
|
-
return { modules, catalog, serviceSchemas, vocabulary: [...vocabulary].sort(),
|
|
508
|
+
return { modules, catalog, serviceSchemas, vocabulary: [...vocabulary].sort(), slotMeta, ownership, declarationIssues };
|
|
389
509
|
}
|
|
390
510
|
|
|
391
511
|
// ── CLI collectDeclared 迁入(C5——声明集合 + 双端入口探测 + declaration.moduleId 提取)──
|
|
@@ -447,7 +567,7 @@ export function collectDeclaredModules(appDir: string): DeclaredModuleInfo[] {
|
|
|
447
567
|
for (const mod of walk.modules) {
|
|
448
568
|
const mode = modeById.get(mod.id) ?? 'local';
|
|
449
569
|
const hasServerEntry = probeModuleObjectExport(mod.dir, 'server', 'serverModule');
|
|
450
|
-
const c = mod.descriptor.
|
|
570
|
+
const c = mod.descriptor.declare;
|
|
451
571
|
declared.push({
|
|
452
572
|
id: mod.id,
|
|
453
573
|
pkg: mode === 'sdk' ? mod.pkg : `@app/${mod.id}`,
|
|
@@ -13,11 +13,11 @@ import type {
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
export interface EvaluationExports {
|
|
16
|
-
/** 求值单源(TIER0;v3 显式激活 vs v4
|
|
16
|
+
/** 求值单源(TIER0;v3 显式激活 vs v4 通配语义随应用版本;v6 第五参 ownerOf) */
|
|
17
17
|
resolveEffectiveService: typeof resolveEffectiveService;
|
|
18
18
|
/** 供给查表派生(谓词 #12 与视图供给轴共用输入) */
|
|
19
19
|
buildSupplyLookup: typeof buildSupplyLookup;
|
|
20
|
-
/** service →
|
|
20
|
+
/** service → 首点段域前缀(v6 deprecated——级联键控退役,仅词汇组织消费) */
|
|
21
21
|
moduleDomainOf: typeof moduleDomainOf;
|
|
22
22
|
/** 部署实例注册表归一(默认实例委托——null → '*' 通配口径) */
|
|
23
23
|
normalizeInstances: typeof normalizeInstances;
|
|
@@ -38,13 +38,16 @@ export interface ResolvedContracts {
|
|
|
38
38
|
evaluation: boolean;
|
|
39
39
|
/** 严格 zod 面可用(parseIntegrationsConfig 在场——≥0.9 门控判据) */
|
|
40
40
|
strictValidation: boolean;
|
|
41
|
+
/** v6 形状标记在场(OWNERSHIP_BINDING_V6——modules[owner] 绑定形状支持;标记门 P1:
|
|
42
|
+
* v5 contracts + v6 toolkit 偏斜 → 503 第四分支,防静默忽略 ownerOf) */
|
|
43
|
+
ownershipBindingV6: boolean;
|
|
41
44
|
/** 动态 import 产物(命名导出子集——Partial<EvaluationExports>) */
|
|
42
45
|
module: ResolvedModule;
|
|
43
46
|
/** 解析诊断(N1 构建指引 message 三分支的输入:未安装 / 版本 <0.9 / 源码态未构建) */
|
|
44
47
|
notes: string[];
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
/** 求值面必需具名导出(在场 → evaluation: true
|
|
50
|
+
/** 求值面必需具名导出(在场 → evaluation: true;moduleDomainOf v6 deprecated 但仍在发布面) */
|
|
48
51
|
const EVALUATION_EXPORT_NAMES = [
|
|
49
52
|
'resolveEffectiveService',
|
|
50
53
|
'buildSupplyLookup',
|
|
@@ -56,10 +59,14 @@ const EVALUATION_EXPORT_NAMES = [
|
|
|
56
59
|
/** 严格校验面具名导出(在场 → strictValidation: true;0.9+) */
|
|
57
60
|
const STRICT_EXPORT_NAME = 'parseIntegrationsConfig';
|
|
58
61
|
|
|
62
|
+
/** v6 形状标记导出(在场 → ownershipBindingV6: true;判据先例 = STRICT_EXPORT_NAME 出口在场性) */
|
|
63
|
+
const OWNERSHIP_MARKER_NAME = 'OWNERSHIP_BINDING_V6';
|
|
64
|
+
|
|
59
65
|
const UNRESOLVED: ResolvedContracts = {
|
|
60
66
|
version: null,
|
|
61
67
|
evaluation: false,
|
|
62
68
|
strictValidation: false,
|
|
69
|
+
ownershipBindingV6: false,
|
|
63
70
|
module: {},
|
|
64
71
|
notes: [],
|
|
65
72
|
};
|
|
@@ -203,10 +210,14 @@ async function probeCandidate(
|
|
|
203
210
|
const mod = (await import(pathToFileURL(probeFile).href + versionQuery)) as Record<string, unknown>;
|
|
204
211
|
const evaluation = EVALUATION_EXPORT_NAMES.every((name) => mod[name] !== undefined);
|
|
205
212
|
const strictValidation = mod[STRICT_EXPORT_NAME] !== undefined;
|
|
213
|
+
// v6 形状标记(出口在场性——判据先例 STRICT_EXPORT_NAME);缺席时不在此 push note
|
|
214
|
+
// (503 文案归 factory assertContractsUsable 门控单点——P1)
|
|
215
|
+
const ownershipBindingV6 = mod[OWNERSHIP_MARKER_NAME] === true;
|
|
206
216
|
return {
|
|
207
217
|
version,
|
|
208
218
|
evaluation,
|
|
209
219
|
strictValidation,
|
|
220
|
+
ownershipBindingV6,
|
|
210
221
|
module: mod as ResolvedModule,
|
|
211
222
|
notes: evaluation
|
|
212
223
|
? []
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 配置 JSON 环境变量占位展开(纯数据形态——自 app-sdk env-expansion.ts 迁入改造)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* ${NAME}
|
|
6
|
-
*
|
|
4
|
+
* 语法(语义真源 = 本文件 + 共享向量 test-fixtures/env-expansion-cases.json 防漂移):
|
|
5
|
+
* ${NAME} 占位引用;NAME 限定 [A-Z][A-Z0-9_]*(UPPER_SNAKE,最小化与正文 `${...}` 误碰)
|
|
6
|
+
* ${NAME:def} POSIX 风格缺省值:NAME 未设置时用 def(字面量,不二次展开);设置时空串也算「已设置」
|
|
7
|
+
* $${NAME} 转义为字面量 ${NAME}(原样保留一次 $)
|
|
7
8
|
*
|
|
8
9
|
* 语义(评审 N1 钉死,壳层各自包装 throw/report):
|
|
9
10
|
* - 单遍非递归:env 值内若含 `${OTHER}` 不被二次展开(注入防线);String.replace 回调形态天然单遍。
|
|
11
|
+
* 默认值内含 `${...}` 同样不展开(POSIX 同限);默认值含 `}` 不支持(同 POSIX 同限——正则 [^}] 截断)。
|
|
10
12
|
* - 值经 JSON 字符串转义注入;不做数值/布尔强制转换。
|
|
11
|
-
* - **本版为纯数据返回形态** `{ text?: string; missing: string[] }
|
|
13
|
+
* - **本版为纯数据返回形态** `{ text?: string; missing: string[] }`——缺变量(无缺省值时)不 throw,
|
|
12
14
|
* 由壳层(CLI doctor 壳 / app-sdk 桥 throw 壳 / 沙箱宿主)决定 fail-fast 还是降级:
|
|
13
15
|
* SDK 桥壳 missing 非空即 throw(错误文案逐字等价 SDK 旧版);CLI 壳同款。
|
|
14
16
|
* - 无 ${ 的文本快速路径:text 原文返回,零替换零开销。
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
|
-
/** 占位匹配:$ 或 $$ 后接 {
|
|
18
|
-
const ENV_VAR_RE = /\$\$?\{([A-Z][A-Z0-9_]*)
|
|
19
|
+
/** 占位匹配:$ 或 $$ 后接 {NAME} 或 {NAME:default}($$ 形态同匹配,展开期判首字符去留) */
|
|
20
|
+
const ENV_VAR_RE = /\$\$?\{([A-Z][A-Z0-9_]*)(?::-([^}]*))?\}/g;
|
|
19
21
|
|
|
20
22
|
/** JSON 字符串字面量转义(值 → 可直接置于 "..." 内的安全文本)——
|
|
21
23
|
* 基于 JSON.stringify 切片:引号/反斜杠/控制字符(\b/\n/\r/\t/\f)转义语义与 JSON 精确一致 */
|
|
@@ -37,14 +39,14 @@ export function expandEnvVars(
|
|
|
37
39
|
): EnvExpansionResult {
|
|
38
40
|
const missing: string[] = [];
|
|
39
41
|
// 单遍替换:回调返回值不回扫(非递归语义;$${} 转义在回调内消去一个 $)
|
|
40
|
-
const out = text.replace(ENV_VAR_RE, (raw, name: string) => {
|
|
42
|
+
const out = text.replace(ENV_VAR_RE, (raw, name: string, fallback?: string) => {
|
|
41
43
|
if (raw.startsWith('$$')) return raw.slice(1);
|
|
42
44
|
const v = env[name];
|
|
43
|
-
if (v
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
45
|
+
if (v !== undefined) return jsonEscape(v);
|
|
46
|
+
// ${NAME:def}:NAME 未设置 → 缺省值(字面量;空串默认 = 显式空值语义)
|
|
47
|
+
if (fallback !== undefined) return jsonEscape(fallback);
|
|
48
|
+
missing.push(name);
|
|
49
|
+
return raw; // 占位保留(纯数据形态:missing 上报,不 throw)
|
|
48
50
|
});
|
|
49
51
|
if (missing.length > 0) return { missing };
|
|
50
52
|
return { text: out, missing: [] };
|