@tbox.cn/app-toolkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/chunk-VV3ERZXS.js +1 -0
  4. package/dist/contracts-resolver-QM4FBQQV.js +1 -0
  5. package/dist/index.d.ts +1909 -0
  6. package/dist/index.js +8 -0
  7. package/package.json +47 -0
  8. package/src/assembly/app-manifest.ts +132 -0
  9. package/src/assembly/provider-catalog.ts +167 -0
  10. package/src/assembly/walk-manifests.ts +461 -0
  11. package/src/core/contracts-expected.ts +31 -0
  12. package/src/core/contracts-resolver.ts +274 -0
  13. package/src/core/credential-format.ts +98 -0
  14. package/src/core/credential-mask.ts +19 -0
  15. package/src/core/env-expansion.ts +51 -0
  16. package/src/core/errors.ts +46 -0
  17. package/src/core/file-cache.ts +65 -0
  18. package/src/core/fskit.ts +21 -0
  19. package/src/core/module-schema.ts +139 -0
  20. package/src/dto.ts +298 -0
  21. package/src/factory.ts +297 -0
  22. package/src/index.ts +137 -0
  23. package/src/integrations/credentials.ts +153 -0
  24. package/src/integrations/mock-bindings.ts +55 -0
  25. package/src/integrations/predicate-io.ts +199 -0
  26. package/src/integrations/predicate.ts +598 -0
  27. package/src/integrations/read.ts +53 -0
  28. package/src/integrations/write.ts +448 -0
  29. package/src/views/app.ts +28 -0
  30. package/src/views/context.ts +70 -0
  31. package/src/views/modules.ts +48 -0
  32. package/src/views/providers.ts +165 -0
  33. package/src/views/service-detail.ts +32 -0
  34. package/src/views/service-resolutions.ts +323 -0
  35. package/tests/contracts-resolver.test.ts +203 -0
  36. package/tests/demo-app.ts +146 -0
  37. package/tests/dev-manifest-catalog.test.ts +114 -0
  38. package/tests/error-name-safety.test.ts +26 -0
  39. package/tests/file-cache.test.ts +242 -0
  40. package/tests/import-layers.test.ts +111 -0
  41. package/tests/module-schema.test.ts +84 -0
  42. package/tests/naming-alignment.test.ts +50 -0
  43. package/tests/views.test.ts +427 -0
  44. package/tests/write-core.test.ts +188 -0
  45. package/tsconfig.json +11 -0
  46. package/tsup.config.ts +29 -0
@@ -0,0 +1,461 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { FileCache } from '../core/file-cache.js';
4
+ import { parseModuleDescriptor, type ModuleDescriptor, type ProviderSlotContrib } from '../core/module-schema.js';
5
+ import { readAppManifestFull, type NpmModuleEntry, type AppManifestFull } from './app-manifest.js';
6
+
7
+ /**
8
+ * walkManifests(appDir)——单次遍历四产物(D24/D26;ai-build C3-1)。
9
+ *
10
+ * manifest 候选路径探测(packages/<id>/tbox.module.json 优先 → node_modules/<pkg>/tbox.module.json;
11
+ * tbox-app dev 真身经 .tbox-dev.local/dev-manifest.json redirects 独立 catalog 通道直达)一次遍历同时产出:
12
+ * 1. modules:已装模块清单(declared;demand meta title/description 透传)
13
+ * 2. catalog(富形态):providers 嵌套(vendor→impl→entry,per-service {implementation, credentialType}
14
+ * 保真——同厂商异构/多实现合法,D25/H1)+ 需求侧聚合
15
+ * 3. serviceSchemas:per-service schema 复合键表——键 `${provider}\u0000${service}`(H2,同一服务
16
+ * 多厂商供给各自 schema 零碰撞);多 impl 同 (provider, service) 异 schema → 声明序首个
17
+ * 4. vocabulary:所有携带 service-slots.json 的已装契约包该文件 `.slots` 键并集(fs 直读、
18
+ * `$comment` 忽略;探测链 = readdir(<候选>/node_modules/@tbox.cn) 过滤携带该文件的包——
19
+ * 零缓存枚举,与 manifest 探测同机制;多行业 contracts-* 天然扩展 F8);缺席 → [](退化 demanded ∪ configured)
20
+ * 5. domainKeyIssues:跨模块域键冲突 issue(两模块派生同一 domains 键——P13)
21
+ *
22
+ * **约束(D26;FX-3/F7 边界)**:目录枚举零缓存——readdir / node_modules 候选探测 / `.tbox/app.json` 读取 /
23
+ * dev-manifest 读取每次执行(枚举输入保持 fresh——agent 装卸模块下一笔即见);manifest/service-slots
24
+ * **文件内容**经 file-cache(`cache` 可选参——工厂/写内核传本实例 cache(stat 指纹自愈 + 写后失效);
25
+ * CLI 传 undefined 即全 fresh)。
26
+ * 宽松解析语义(坏文件静默 + 重复键首声明保留)零改动。
27
+ */
28
+
29
+ export interface WalkManifestsResult {
30
+ /** declared 产物(install id + descriptor;demand meta 透传) */
31
+ modules: WalkModuleInfo[];
32
+ /** 富形态 catalog(providers 嵌套保真 + 需求聚合 + credentialTypes) */
33
+ catalog: WalkCatalog;
34
+ /** per-service schema 复合键表:`${provider}\u0000${service}` → schema 引用 + 包根 */
35
+ serviceSchemas: Record<string, { configSchema?: string; credentialSchema?: string; packageDir: string }>;
36
+ /** 服务词汇 = 所有携带 service-slots.json 的已装契约包 `.slots` 键并集(缺席 → []) */
37
+ vocabulary: string[];
38
+ /** 跨模块域键冲突(P13——两模块的 service 集派生同一 domains 键) */
39
+ domainKeyIssues: Array<{ severity: 'warning'; code: 'DOMAIN_KEY_CONFLICT'; message: string; module?: string }>;
40
+ }
41
+
42
+ export interface WalkModuleInfo {
43
+ /** install id(packages/ 目录名 / npmModules entry.id) */
44
+ id: string;
45
+ /** 模块目录(本地 packages/<id> 优先;sdk 包 = node_modules 解析位) */
46
+ dir: string;
47
+ /** 发布源包名 */
48
+ pkg: string;
49
+ descriptor: ModuleDescriptor;
50
+ }
51
+
52
+ /** 富形态供给条目:per-service {implementation, credentialType} 恒数组保真(H1) */
53
+ export interface WalkProviderEntry {
54
+ provider: string;
55
+ implementation: string;
56
+ credentialType: string;
57
+ owner: string;
58
+ local?: boolean;
59
+ configSchema?: string;
60
+ credentialSchema?: string;
61
+ services: string[];
62
+ }
63
+
64
+ export interface WalkCatalog {
65
+ providers: Record<string, Record<string, WalkProviderEntry>>;
66
+ services: Record<
67
+ string,
68
+ {
69
+ optional: boolean;
70
+ defaults: Array<{ provider: string; implementation: string }>;
71
+ /** demand meta(title/description 首声明保留——模块 manifest demand 侧 additive) */
72
+ title?: string;
73
+ description?: string;
74
+ owners: string[];
75
+ }
76
+ >;
77
+ credentialTypes: Record<string, { owner: string; credentialSchema?: string }>;
78
+ }
79
+
80
+ /** loose 允许的 contributes 局部结构(本地声明——解跨模块类型身份耦合,TS2719 防线) */
81
+ interface LooseContributes {
82
+ providers?: { slots?: ProviderSlotContrib[] };
83
+ services?: Array<{
84
+ service: string;
85
+ optional?: boolean;
86
+ defaults?: Array<{ provider: string; implementation: string }>;
87
+ title?: string;
88
+ description?: string;
89
+ }>;
90
+ }
91
+
92
+ /** manifest 读取结果(判别联合——strict = 完整 descriptor;loose = 部分形态宽容提取) */
93
+ type ReadManifestResult =
94
+ | { kind: 'strict'; name: string; version: string; descriptor: ModuleDescriptor; contributes: ModuleDescriptor['contributes'] }
95
+ | { kind: 'loose'; name?: string; contributes: LooseContributes };
96
+
97
+ /** 读单文件 manifest(坏文件 → null;宽松——strict 解析失败但有 contributes → loose 兜底,
98
+ * 保留旧 SDK loadProviderCatalog 对部分形态 manifest 的容忍语义,C3-5 桥等价性)。
99
+ * FX-3(F7 边界):**文件内容**经 file-cache(stat 指纹——外部写下一笔自愈);目录枚举仍零缓存。 */
100
+ function readManifestFile(file: string, cache?: FileCache): ReadManifestResult | null {
101
+ const parse = (raw: string): ReadManifestResult | null => {
102
+ try {
103
+ const parsedRaw: unknown = JSON.parse(raw);
104
+ const parsed = parseModuleDescriptor(parsedRaw);
105
+ if (parsed.ok) {
106
+ return { kind: 'strict', name: parsed.value.name, version: parsed.value.version, descriptor: parsed.value, contributes: parsed.value.contributes };
107
+ }
108
+ // loose 兜底:strict 拒(部分形态 manifest——如仅 contributes)但有 contributes → 宽松提取
109
+ if (parsedRaw && typeof parsedRaw === 'object' && 'contributes' in parsedRaw) {
110
+ const obj = parsedRaw as { name?: unknown; contributes?: ModuleDescriptor['contributes'] };
111
+ return {
112
+ kind: 'loose',
113
+ ...(typeof obj.name === 'string' ? { name: obj.name } : {}),
114
+ contributes: {
115
+ providers: (obj.contributes as LooseContributes | undefined)?.providers,
116
+ services: (obj.contributes as LooseContributes | undefined)?.services,
117
+ },
118
+ };
119
+ }
120
+ return null;
121
+ } catch {
122
+ return null;
123
+ }
124
+ };
125
+ if (cache) return cache.read(file, parse) ?? null;
126
+ if (!existsSync(file)) return null;
127
+ try {
128
+ return parse(readFileSync(file, 'utf8'));
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /** npmModules 清单读取(纯 .tbox/app.json——dev-manifest 独立通道见 readDevManifestRedirects;
135
+ * 宽松语义:不要求 templateVersion——严格读取归 readAppManifest(CLI 装配);空/坏文件 → 空) */
136
+ function readNpmModules(appDir: string): NpmModuleEntry[] {
137
+ const file = join(appDir, '.tbox', 'app.json');
138
+ if (!existsSync(file)) return [];
139
+ try {
140
+ const parsed = JSON.parse(readFileSync(file, 'utf8')) as { npmModules?: unknown };
141
+ if (!Array.isArray(parsed.npmModules)) return [];
142
+ return parsed.npmModules.filter(
143
+ (e): e is NpmModuleEntry =>
144
+ !!e && typeof e === 'object' && typeof (e as NpmModuleEntry).id === 'string' && typeof (e as NpmModuleEntry).package === 'string',
145
+ );
146
+ } catch {
147
+ return [];
148
+ }
149
+ }
150
+
151
+ /** dev-manifest 通道输入:{pkg, 真身目录} 对——redirect 相对 stageDir 直达(join 型消费方
152
+ * 不吃 symlink,数据面喂食(架构定律 B);免疫 pnpm 中层剪链(R-7))。
153
+ * 仅 catalog 聚合消费(聚合宽/声明窄不变量)——modules 声明集永不含 dev 真身(防 dev 会话
154
+ * 残留期 module sync 污染受跟踪装配文件)。非 dev 环境 marker 永不在场 → 零影响;坏文件静默空。 */
155
+ interface DevManifestRedirect {
156
+ pkg: string;
157
+ dir: string;
158
+ }
159
+
160
+ function readDevManifestRedirects(appDir: string): DevManifestRedirect[] {
161
+ const stageDir = join(appDir, '.tbox-dev.local');
162
+ const devManifest = join(stageDir, 'dev-manifest.json');
163
+ if (!existsSync(devManifest)) return [];
164
+ try {
165
+ const parsed = JSON.parse(readFileSync(devManifest, 'utf8')) as { redirects?: Record<string, unknown> };
166
+ const out: DevManifestRedirect[] = [];
167
+ for (const [pkg, rel] of Object.entries(parsed.redirects ?? {})) {
168
+ if (typeof rel !== 'string' || !pkg.startsWith('@tbox.cn/app-')) continue;
169
+ out.push({ pkg, dir: join(stageDir, rel) });
170
+ }
171
+ return out;
172
+ } catch {
173
+ return [];
174
+ }
175
+ }
176
+
177
+ /** 契约包发现链:readdir(node_modules/@tbox.cn) 过滤携带 service-slots.json 的包(目录枚举零缓存,F8;
178
+ * FX-3:slots 文件**内容**经 file-cache) */
179
+ function discoverContractPackages(appDir: string, cache?: FileCache): Array<{ dir: string; slots: string[] }> {
180
+ const found: Array<{ dir: string; slots: string[] }> = [];
181
+ const candidates: Array<string> = [appDir];
182
+ for (const sub of ['apps', 'packages']) {
183
+ const parent = join(appDir, sub);
184
+ if (!existsSync(parent)) continue;
185
+ try {
186
+ for (const d of readdirSync(parent, { withFileTypes: true })) {
187
+ if (d.isDirectory()) candidates.push(join(parent, d.name));
188
+ }
189
+ } catch {
190
+ /* ignore */
191
+ }
192
+ }
193
+ const seenRoots = new Set<string>();
194
+ for (const dir of candidates) {
195
+ const scopeDir = join(dir, 'node_modules', '@tbox.cn');
196
+ if (!existsSync(scopeDir)) continue;
197
+ let names: string[] = [];
198
+ try {
199
+ names = readdirSync(scopeDir);
200
+ } catch {
201
+ continue;
202
+ }
203
+ for (const name of names) {
204
+ const pkgDir = join(scopeDir, name);
205
+ if (seenRoots.has(pkgDir)) continue;
206
+ seenRoots.add(pkgDir);
207
+ const slotsFile = join(pkgDir, 'service-slots.json');
208
+ if (!existsSync(slotsFile)) continue;
209
+ const parseSlots = (raw: string): string[] | null => {
210
+ try {
211
+ const parsed = JSON.parse(raw) as { slots?: unknown };
212
+ if (Array.isArray(parsed.slots)) return parsed.slots.filter((s): s is string => typeof s === 'string');
213
+ } catch {
214
+ /* 宽松:坏文件跳过 */
215
+ }
216
+ return null;
217
+ };
218
+ const slots = cache ? cache.read(slotsFile, parseSlots) : parseSlots(readFileSync(slotsFile, 'utf8'));
219
+ if (slots !== null && slots !== undefined) found.push({ dir: pkgDir, slots });
220
+ }
221
+ }
222
+ return found;
223
+ }
224
+
225
+ /** walkManifests 主体(单次遍历四产物;目录枚举零缓存——每次调用执行) */
226
+ export function walkManifests(appDir: string, cache?: FileCache): WalkManifestsResult {
227
+ const modules: WalkModuleInfo[] = [];
228
+ const catalog: WalkCatalog = { providers: {}, services: {}, credentialTypes: {} };
229
+ const serviceSchemas: WalkManifestsResult['serviceSchemas'] = {};
230
+ const domainKeyIssues: WalkManifestsResult['domainKeyIssues'] = [];
231
+ const seen = new Set<string>();
232
+ /** 域键 → 首个声明模块(P13 冲突检测) */
233
+ const domainOwners = new Map<string, string>();
234
+
235
+ const addSlots = (slots: readonly ProviderSlotContrib[], owner: string, packageDir: string): void => {
236
+ for (const slot of slots) {
237
+ // 富形态:per-service {implementation, credentialType} 保真(同厂商多 impl 合法——声明序)
238
+ const vendorSlots = (catalog.providers[slot.provider] ??= {});
239
+ const existing = vendorSlots[slot.implementation];
240
+ if (existing) {
241
+ if (!existing.services.includes(slot.service)) existing.services.push(slot.service);
242
+ } else {
243
+ vendorSlots[slot.implementation] = {
244
+ provider: slot.provider,
245
+ implementation: slot.implementation,
246
+ credentialType: slot.credentialType,
247
+ owner,
248
+ ...(slot.local ? { local: true } : {}),
249
+ ...(slot.configSchema ? { configSchema: slot.configSchema } : {}),
250
+ ...(slot.credentialSchema ? { credentialSchema: slot.credentialSchema } : {}),
251
+ services: [slot.service],
252
+ };
253
+ catalog.credentialTypes[slot.credentialType] ??= {
254
+ owner,
255
+ ...(slot.credentialSchema ? { credentialSchema: slot.credentialSchema } : {}),
256
+ };
257
+ }
258
+ // per-service schema 复合键(H2):同 (provider, service) 异 schema → 声明序首个
259
+ const schemaKey = `${slot.provider}\u0000${slot.service}`;
260
+ if (!serviceSchemas[schemaKey]) {
261
+ serviceSchemas[schemaKey] = {
262
+ ...(slot.configSchema ? { configSchema: slot.configSchema } : {}),
263
+ ...(slot.credentialSchema ? { credentialSchema: slot.credentialSchema } : {}),
264
+ packageDir,
265
+ };
266
+ }
267
+ }
268
+ };
269
+
270
+ const addServiceNeed = (
271
+ need: { service: string; optional?: boolean; defaults?: Array<{ provider: string; implementation: string }>; title?: string; description?: string },
272
+ owner: string,
273
+ ): void => {
274
+ const optional = need.optional !== false;
275
+ const defaults = need.defaults ?? [];
276
+ const existing = catalog.services[need.service];
277
+ if (existing) {
278
+ existing.optional = existing.optional && optional;
279
+ const seenDefaults = new Set(existing.defaults.map((d) => `${d.provider}\u0000${d.implementation}`));
280
+ for (const d of defaults) {
281
+ const k = `${d.provider}\u0000${d.implementation}`;
282
+ if (!seenDefaults.has(k)) {
283
+ existing.defaults.push(d);
284
+ seenDefaults.add(k);
285
+ }
286
+ }
287
+ if (!existing.owners.includes(owner)) existing.owners.push(owner);
288
+ return;
289
+ }
290
+ catalog.services[need.service] = {
291
+ optional,
292
+ defaults: [...defaults],
293
+ ...(need.title ? { title: need.title } : {}),
294
+ ...(need.description ? { description: need.description } : {}),
295
+ owners: [owner],
296
+ };
297
+ // P13 域键冲突检测(首声明占用;跨模块冲突 → issue)
298
+ const domain = need.service.includes('.') ? need.service.slice(0, need.service.indexOf('.')) : need.service;
299
+ const firstOwner = domainOwners.get(domain);
300
+ if (firstOwner === undefined) domainOwners.set(domain, owner);
301
+ else if (firstOwner !== owner) {
302
+ domainKeyIssues.push({
303
+ severity: 'warning',
304
+ code: 'DOMAIN_KEY_CONFLICT',
305
+ module: owner,
306
+ message: `域键 ${domain} 被多模块声明(${firstOwner} 与 ${owner})——domains 换算歧义(P13)`,
307
+ });
308
+ }
309
+ };
310
+
311
+ // 通道 1:npmModules 登记(packages/<id> 本地优先 → node_modules/<pkg>)
312
+ for (const entry of readNpmModules(appDir)) {
313
+ if (seen.has(entry.id)) continue;
314
+ const local = join(appDir, 'packages', entry.id);
315
+ const nm = join(appDir, 'node_modules', ...entry.package.split('/'));
316
+ const manifest = readManifestFile(join(local, 'tbox.module.json'), cache) ?? readManifestFile(join(nm, 'tbox.module.json'), cache);
317
+ if (!manifest) continue;
318
+ seen.add(entry.id);
319
+ const dir = existsSync(join(local, 'tbox.module.json')) ? local : nm;
320
+ const contributes = manifest.contributes;
321
+ // loose 形态(部分 manifest)不进 modules 清单(descriptor 缺名不可装配化)——仅 catalog 聚合(旧语义等价)
322
+ if (manifest.kind === 'strict') {
323
+ modules.push({ id: entry.id, dir, pkg: entry.mode === 'sdk' ? entry.package : `@app/${entry.id}`, descriptor: manifest.descriptor });
324
+ }
325
+ const slots = contributes.providers?.slots ?? [];
326
+ if (slots.length) addSlots(slots, entry.package, dir);
327
+ const needs = contributes.services ?? [];
328
+ if (needs.length) {
329
+ for (const need of needs) {
330
+ addServiceNeed(need, entry.package);
331
+ }
332
+ }
333
+ }
334
+
335
+ // 通道 1.5:tbox-app dev manifest 真身(catalog-only——聚合宽/声明窄不变量:dev 模块
336
+ // 永不进 modules 清单,防 sync 污染受跟踪装配文件;登记真源优先(seen 按模块名去重))
337
+ for (const { pkg, dir } of readDevManifestRedirects(appDir)) {
338
+ const manifest = readManifestFile(join(dir, 'tbox.module.json'), cache);
339
+ if (!manifest?.name || seen.has(manifest.name)) continue;
340
+ seen.add(manifest.name);
341
+ const slots = manifest.contributes.providers?.slots ?? [];
342
+ if (slots.length) addSlots(slots, pkg, dir);
343
+ const needs = manifest.contributes.services ?? [];
344
+ if (needs.length) {
345
+ for (const need of needs) addServiceNeed(need, pkg);
346
+ }
347
+ }
348
+
349
+ // 通道 2:packages/* 约定发现(本地模块——npmModules 未登记形态,如模板天气模块)
350
+ const packagesDir = join(appDir, 'packages');
351
+ if (existsSync(packagesDir)) {
352
+ let entries: string[] = [];
353
+ try {
354
+ entries = readdirSync(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
355
+ } catch {
356
+ /* ignore */
357
+ }
358
+ for (const name of entries) {
359
+ if (seen.has(name)) continue;
360
+ const dir = join(packagesDir, name);
361
+ const manifest = readManifestFile(join(dir, 'tbox.module.json'), cache);
362
+ if (!manifest) continue;
363
+ const modName = manifest.name ?? name;
364
+ if (seen.has(modName)) continue;
365
+ seen.add(modName);
366
+ const contributes = manifest.contributes;
367
+ if (manifest.kind === 'strict') {
368
+ modules.push({ id: modName, dir, pkg: `@app/${modName}`, descriptor: manifest.descriptor });
369
+ }
370
+ const slots = contributes.providers?.slots ?? [];
371
+ if (slots.length) addSlots(slots, `@app/${modName}`, dir);
372
+ const needs = contributes.services ?? [];
373
+ if (needs.length) {
374
+ for (const need of needs) {
375
+ addServiceNeed(need, `@app/${modName}`);
376
+ }
377
+ }
378
+ }
379
+ }
380
+
381
+ // 产物 4:服务词汇(契约包 service-slots.json 并集——零缓存枚举)
382
+ const vocabulary = new Set<string>();
383
+ for (const pkg of discoverContractPackages(appDir, cache)) {
384
+ for (const s of pkg.slots) vocabulary.add(s);
385
+ }
386
+
387
+ return { modules, catalog, serviceSchemas, vocabulary: [...vocabulary].sort(), domainKeyIssues };
388
+ }
389
+
390
+ // ── CLI collectDeclared 迁入(C5——声明集合 + 双端入口探测 + declaration.moduleId 提取)──
391
+
392
+ export interface DeclaredModuleInfo {
393
+ id: string;
394
+ /** 装配包名(codegen/local = @app/<id>;sdk = 发布包名) */
395
+ pkg: string;
396
+ mode: 'local' | 'sdk' | 'codegen';
397
+ hasServerEntry: boolean;
398
+ hasClientEntry: boolean;
399
+ hasCards: boolean;
400
+ /** declaration.moduleId(deployment 对账真源;老模块无 declaration → undefined) */
401
+ moduleId?: string;
402
+ }
403
+
404
+ /** 探测模块双端描述对象入口(src/{server,client}/index.ts export const serverModule/clientModule) */
405
+ function probeModuleObjectExport(dir: string, sub: 'server' | 'client', symbol: string): boolean {
406
+ const file = join(dir, 'src', sub, 'index.ts');
407
+ if (!existsSync(file)) return false;
408
+ try {
409
+ const src = readFileSync(file, 'utf8');
410
+ return new RegExp(`export\\s+const\\s+${symbol}\\b`).test(src);
411
+ } catch {
412
+ return false;
413
+ }
414
+ }
415
+
416
+ /** 读模块 server 入口源码(无入口 → '') */
417
+ function readServerSource(dir: string): string {
418
+ const file = join(dir, 'src', 'server', 'index.ts');
419
+ if (!existsSync(file)) return '';
420
+ try {
421
+ return readFileSync(file, 'utf8');
422
+ } catch {
423
+ return '';
424
+ }
425
+ }
426
+
427
+ /** 提取 declaration.moduleId(宽松正则——容忍任意嵌套/注释;未声明 → undefined) */
428
+ export function extractModuleIdFromSource(source: string): string | undefined {
429
+ const match = source.match(/declaration\s*:\s*\{[\s\S]*?moduleId\s*:\s*['"]([^'"]+)['"]/);
430
+ return match?.[1];
431
+ }
432
+
433
+ /**
434
+ * 计算声明集合(含 npm 清单 + 本地模块约定发现)。
435
+ * hasServerEntry/hasClientEntry 决定模块参与哪些装配文件(入口探测,非 post-hoc 修复)。
436
+ * 单源 = walkManifests(npmModules 登记 + packages/* 约定发现双通道;strict 形态进清单)。
437
+ */
438
+ export function collectDeclaredModules(appDir: string): DeclaredModuleInfo[] {
439
+ const declared: DeclaredModuleInfo[] = [];
440
+ const walk = walkManifests(appDir);
441
+ const manifest: AppManifestFull | null = readAppManifestFull(appDir);
442
+ const modeById = new Map<string, 'local' | 'sdk' | 'codegen'>();
443
+ for (const entry of manifest?.npmModules ?? []) {
444
+ modeById.set(entry.id, entry.mode);
445
+ }
446
+ for (const mod of walk.modules) {
447
+ const mode = modeById.get(mod.id) ?? 'local';
448
+ const hasServerEntry = probeModuleObjectExport(mod.dir, 'server', 'serverModule');
449
+ const c = mod.descriptor.contributes;
450
+ declared.push({
451
+ id: mod.id,
452
+ pkg: mode === 'sdk' ? mod.pkg : `@app/${mod.id}`,
453
+ mode,
454
+ hasServerEntry,
455
+ hasClientEntry: probeModuleObjectExport(mod.dir, 'client', 'clientModule'),
456
+ hasCards: (c.cards?.length ?? 0) > 0,
457
+ ...(hasServerEntry ? { moduleId: extractModuleIdFromSource(readServerSource(mod.dir)) } : {}),
458
+ });
459
+ }
460
+ return declared.sort((a, b) => a.id.localeCompare(b.id));
461
+ }
@@ -0,0 +1,31 @@
1
+ import type {
2
+ buildSupplyLookup,
3
+ moduleDomainOf,
4
+ normalizeInstances,
5
+ resolveEffectiveService,
6
+ IntegrationsConfig,
7
+ } from '@tbox.cn/app-contracts';
8
+
9
+ /**
10
+ * contracts 期望面编译期锁(satisfies 语义防接口漂移——contracts 加名/改签名即编译红)。
11
+ * resolver 运行时产物按本接口逐命名导出存在性检查(evaluation/strictValidation 按在场降级)。
12
+ * contracts 恒 devDep 且限 import type——运行时全经 resolver 动态 import(版本解耦,D3)。
13
+ */
14
+
15
+ export interface EvaluationExports {
16
+ /** 求值单源(TIER0;v3 显式激活 vs v4 通配语义随应用版本) */
17
+ resolveEffectiveService: typeof resolveEffectiveService;
18
+ /** 供给查表派生(谓词 #12 与视图供给轴共用输入) */
19
+ buildSupplyLookup: typeof buildSupplyLookup;
20
+ /** service → 首点段域前缀(谓词 #14 域键匹配) */
21
+ moduleDomainOf: typeof moduleDomainOf;
22
+ /** 部署实例注册表归一(默认实例委托——null → '*' 通配口径) */
23
+ normalizeInstances: typeof normalizeInstances;
24
+ /** 模块资源类型词汇表(v5;descriptor type 校验经 resolver 取词表) */
25
+ RESOURCE_TYPE_IDS: readonly string[];
26
+ /** 严格 zod 面(0.9+ 可选)——strictValidation 门控判据;缺席 = 求值/写 503 */
27
+ parseIntegrationsConfig?: (raw: unknown) => IntegrationsConfig;
28
+ }
29
+
30
+ /** resolver 运行时产物类型(动态 import 的命名导出子集) */
31
+ export type ResolvedModule = Partial<EvaluationExports>;