@tbox.cn/app-toolkit 0.4.0 → 0.6.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 +374 -150
- 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 +90 -14
- package/src/dto.ts +80 -33
- package/src/factory.ts +78 -26
- package/src/index.ts +16 -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 +47 -27
- package/src/views/app.ts +4 -4
- package/src/views/context.ts +15 -14
- package/src/views/integration-schemas.ts +76 -18
- package/src/views/modules.ts +93 -25
- 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-resources.test.ts +148 -0
- package/tests/module-schema.test.ts +120 -20
- package/tests/resolve-key.test.ts +154 -0
- package/tests/vendor-schema-keywords.test.ts +74 -0
- package/tests/views.test.ts +585 -247
- package/tests/write-core.test.ts +117 -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/tests/write-core.test.ts
CHANGED
|
@@ -277,6 +277,89 @@ describe('写核心管线(FX-1c)', () => {
|
|
|
277
277
|
expect(existsSync(h.credFile('p-x-mall-1-global'))).toBe(true);
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
+
// ── v6 守卫真值族(P5):modules 集成位键集纪律 + resources 保全(N2)──
|
|
281
|
+
describe('v6 applyModule(modules[owner] 集成位)', () => {
|
|
282
|
+
function harnessWithModule(): Harness & { writeManifest(): void } {
|
|
283
|
+
const base = harness();
|
|
284
|
+
const writeManifest = (): void => {
|
|
285
|
+
writeAppFile(
|
|
286
|
+
base.app.appDir,
|
|
287
|
+
'packages/module-parking/tbox.module.json',
|
|
288
|
+
JSON.stringify({
|
|
289
|
+
schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
|
|
290
|
+
// v7 词汇统一:展示名 name——双键写侧寻址(展示名 → 落盘键恒真实 id)用例数据
|
|
291
|
+
name: '停车缴费',
|
|
292
|
+
declare: { owns: [{ service: 'parking.query' }] },
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
};
|
|
296
|
+
writeManifest();
|
|
297
|
+
return { ...base, writeManifest };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
it('v6-G1 PUT {provider} 落位 modules[install id];既有 resources 保全(N2)', async () => {
|
|
301
|
+
const h2 = harnessWithModule();
|
|
302
|
+
writeAppFile(h2.app.appDir, INTEGRATIONS_FILE, JSON.stringify({
|
|
303
|
+
services: {},
|
|
304
|
+
modules: { 'module-parking': { resources: { kb: { type: 'knowledge', datasetId: 'd' } } } },
|
|
305
|
+
}, null, 2));
|
|
306
|
+
const r = await h2.tk.writeModuleIntegration('module-parking', { provider: 'wanda' });
|
|
307
|
+
expect(r.written).toBe(true);
|
|
308
|
+
const cfg = h2.readConfig();
|
|
309
|
+
expect(cfg.modules).toEqual({
|
|
310
|
+
'module-parking': { provider: 'wanda', resources: { kb: { type: 'knowledge', datasetId: 'd' } } },
|
|
311
|
+
});
|
|
312
|
+
h2.app.dispose();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('v7 双键写侧:展示名寻址 PUT → 落盘键恒解析回真实 id', async () => {
|
|
316
|
+
const h2 = harnessWithModule();
|
|
317
|
+
writeAppFile(h2.app.appDir, INTEGRATIONS_FILE, JSON.stringify({ services: {} }, null, 2));
|
|
318
|
+
const r = await h2.tk.writeModuleIntegration('停车缴费', { provider: 'wanda' });
|
|
319
|
+
expect(r.written).toBe(true);
|
|
320
|
+
const cfg = h2.readConfig();
|
|
321
|
+
expect(Object.keys(cfg.modules ?? {})).toEqual(['module-parking']);
|
|
322
|
+
expect((cfg.modules as Record<string, unknown>)['module-parking']).toEqual({ provider: 'wanda' });
|
|
323
|
+
h2.app.dispose();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('v6-G2 PUT {} 清 provider/config 双键、resources 恒保全(N2 golden);节点净空 → 删除', async () => {
|
|
327
|
+
const h2 = harnessWithModule();
|
|
328
|
+
writeAppFile(h2.app.appDir, INTEGRATIONS_FILE, JSON.stringify({
|
|
329
|
+
services: {},
|
|
330
|
+
modules: {
|
|
331
|
+
'module-parking': { provider: 'wanda', config: { baseUrl: 'x' }, resources: { kb: { type: 'knowledge', datasetId: 'd' } } },
|
|
332
|
+
'module-bare': { provider: 'mock' },
|
|
333
|
+
},
|
|
334
|
+
}, null, 2));
|
|
335
|
+
writeAppFile(h2.app.appDir, 'packages/module-bare/tbox.module.json', JSON.stringify({
|
|
336
|
+
schemaVersion: 1, id: 'module-bare', version: '0.1.0', kind: 'business',
|
|
337
|
+
declare: { owns: [{ service: 'member.account' }] },
|
|
338
|
+
}));
|
|
339
|
+
await h2.tk.writeModuleIntegration('module-parking', {});
|
|
340
|
+
await h2.tk.writeModuleIntegration('module-bare', {});
|
|
341
|
+
const cfg = h2.readConfig();
|
|
342
|
+
expect(cfg.modules).toEqual({
|
|
343
|
+
'module-parking': { resources: { kb: { type: 'knowledge', datasetId: 'd' } } }, // resources 保全
|
|
344
|
+
// module-bare 节点净空 → 键删除
|
|
345
|
+
});
|
|
346
|
+
h2.app.dispose();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('v6-G3 键集纪律:node 含 provider/config 外键 → 400(fail-visible);零归属模块 → 400', async () => {
|
|
350
|
+
const h2 = harnessWithModule();
|
|
351
|
+
await expect(h2.tk.writeModuleIntegration('module-parking', { provider: 'x', resources: { k: {} } } as never))
|
|
352
|
+
.rejects.toMatchObject({ code: 'VALIDATION_FAILED', httpStatus: 400 });
|
|
353
|
+
// 零归属模块(declare.owns 空)
|
|
354
|
+
writeAppFile(h2.app.appDir, 'packages/module-zero/tbox.module.json', JSON.stringify({
|
|
355
|
+
schemaVersion: 1, id: 'module-zero', version: '0.1.0', kind: 'business', declare: {},
|
|
356
|
+
}));
|
|
357
|
+
await expect(h2.tk.writeModuleIntegration('module-zero', { provider: 'x' }))
|
|
358
|
+
.rejects.toMatchObject({ code: 'VALIDATION_FAILED', httpStatus: 400, message: expect.stringContaining('declare.owns') });
|
|
359
|
+
h2.app.dispose();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
280
363
|
it('AppToolkitError 面:code/httpStatus/fields 结构化(消费面属性分发)', () => {
|
|
281
364
|
const err = new AppToolkitError('VALIDATION_FAILED', 400, 'm', ['a.b']);
|
|
282
365
|
expect(err.name).toBe('AppToolkitError');
|
|
@@ -285,3 +368,37 @@ describe('写核心管线(FX-1c)', () => {
|
|
|
285
368
|
expect(err.fields).toEqual(['a.b']);
|
|
286
369
|
});
|
|
287
370
|
});
|
|
371
|
+
|
|
372
|
+
describe('孤儿凭据写后对账(PUT 路径追平——finalize 单点)', () => {
|
|
373
|
+
let h: Harness;
|
|
374
|
+
beforeEach(() => {
|
|
375
|
+
h = harness();
|
|
376
|
+
});
|
|
377
|
+
afterEach(() => {
|
|
378
|
+
h.app.dispose();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('P1 PUT 替换弃 credentialRef → ORPHANED_CREDENTIAL warning(message 含被弃 stem)', async () => {
|
|
382
|
+
writeAppFile(h.app.appDir, 'config/credentials/wd-parking.json', JSON.stringify({ type: 'wanda-c', k: 'v' }));
|
|
383
|
+
await h.tk.writeServiceIntegration('parking.query', {
|
|
384
|
+
provider: 'wanda', implementation: 'w@1', credentialRef: 'secret://wd-parking',
|
|
385
|
+
} as never);
|
|
386
|
+
const r = await h.tk.writeServiceIntegration('parking.query', {} as never);
|
|
387
|
+
expect(r.written).toBe(true);
|
|
388
|
+
const orphan = r.issues.find((i) => i.code === 'ORPHANED_CREDENTIAL');
|
|
389
|
+
expect(orphan).toBeTruthy();
|
|
390
|
+
expect(orphan?.message).toContain('wd-parking');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('P2 真写报全量孤儿 / dryRun 不报 / no-diff 不报', async () => {
|
|
394
|
+
writeAppFile(h.app.appDir, 'config/credentials/stray.json', JSON.stringify({ type: 'x', k: 'v' }));
|
|
395
|
+
const node = { provider: 'wanda', implementation: 'w@1' } as never;
|
|
396
|
+
const dry = await h.tk.writeServiceIntegration('parking.query', node, { dryRun: true });
|
|
397
|
+
expect(dry.issues.some((i) => i.code === 'ORPHANED_CREDENTIAL')).toBe(false);
|
|
398
|
+
const real = await h.tk.writeServiceIntegration('parking.query', node);
|
|
399
|
+
expect(real.issues.some((i) => i.code === 'ORPHANED_CREDENTIAL' && i.message.includes('stray'))).toBe(true);
|
|
400
|
+
const nodiff = await h.tk.writeServiceIntegration('parking.query', node);
|
|
401
|
+
expect(nodiff.written).toBe(false);
|
|
402
|
+
expect(nodiff.issues.some((i) => i.code === 'ORPHANED_CREDENTIAL')).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
});
|
package/dist/chunk-KPD3LUH4.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{existsSync as l,readdirSync as F,realpathSync as m}from"fs";import{join as c}from"path";import{pathToFileURL as w}from"url";import{existsSync as T,readFileSync as k,statSync as x}from"fs";function p(){let e=new Map,n=0;function r(t){if(!T(t))return null;let i=x(t);return`${i.mtimeMs}:${i.size}`}return{read(t,i){let o=r(t);if(o===null){e.delete(t);return}let s=e.get(t);if(s&&s.fingerprint===o)return s.value;n+=1;let a=i(k(t,"utf8"));return e.set(t,{fingerprint:o,value:a}),a},invalidate(t){if(t===void 0){e.clear();return}for(let i of t)e.delete(i)},get parseCount(){return n}}}var g=["resolveEffectiveService","buildSupplyLookup","moduleDomainOf","normalizeInstances","RESOURCE_TYPE_IDS"],M="parseIntegrationsConfig",v={version:null,evaluation:!1,strictValidation:!1,module:{},notes:[]};function C(e){let n=[e];for(let r of["apps","packages"]){let t=c(e,r);if(!l(t))continue;let i=F(t,{withFileTypes:!0}).filter(o=>o.isDirectory()).map(o=>o.name);r==="apps"?i.sort((o,s)=>o==="server"?-1:s==="server"?1:o.localeCompare(s)):i.sort((o,s)=>o.localeCompare(s));for(let o of i)n.push(c(t,o))}return n}var d=new Map,h=p();function y(e){return h.read(e,n=>{let r=JSON.parse(n);return typeof r.version=="string"?r.version:null})??null}function b(e){if(!l(e))return"gone";let n;try{n=m(e)}catch{n=e}return l(c(n,"dist","runtime.js"))?"dist":l(c(n,"src","runtime.ts"))?"src":"none"}function R(e){let n=c(e,"package.json");return{pkgPath:n,candidateDir:e,version:y(n),artifact:b(e)}}function E(e){for(let n of C(e)){let r=c(n,"node_modules","@tbox.cn","app-contracts"),t=c(r,"package.json");if(l(t))return{pkgPath:t,candidateDir:r,version:y(t),artifact:b(r)}}return null}function u(e){return e===null?"none":`${e.pkgPath}|${e.version??"null"}|${e.artifact}`}function $(e,n){let r=E(e);if(u(r)!==u(n.first))return!1;if(n.resolved===null)return!0;let t=r!==null&&r.candidateDir===n.resolved.candidateDir?r:R(n.resolved.candidateDir);return u(t)===u(n.resolved)}async function O(e,n){if(!l(e))return null;let r;try{r=m(e)}catch{return null}let i=[c(r,"dist","runtime.js"),c(r,"src","runtime.ts")].find(s=>l(s)),o=h.read(c(e,"package.json"),s=>{let a=JSON.parse(s);return typeof a.version=="string"?a.version:null})??null;if(i===void 0)return n.push(`contracts ${o??"(版本未知)"} 探测命中但无构建产物(dist/runtime.js 缺失)——workspace 源码态未构建:执行 pnpm --filter @tbox.cn/app-contracts build 后重试`),null;try{let s=o!==null?`?v=${encodeURIComponent(o)}`:"",a=await import(w(i).href+s),f=g.every(S=>a[S]!==void 0),P=a[M]!==void 0;return{version:o,evaluation:f,strictValidation:P,module:a,notes:f?[]:[`${i} 命中但求值面具名导出缺席(${g.join("/")})——契约包形态异常`]}}catch(s){return n.push(`contracts ${o??"(版本未知)"} 构建产物导入失败(${s instanceof Error?s.message:String(s)})——workspace 源码态未构建或产物损坏:执行 pnpm --filter @tbox.cn/app-contracts build 后重试`),null}}function U(e){let n=d.get(e);if(n&&$(e,n))return n.promise;let r=E(e),t={promise:Promise.resolve({...v}),first:r,resolved:r};return t.promise=(async()=>{let i=[];for(let o of C(e)){let s=c(o,"node_modules","@tbox.cn","app-contracts"),a=await O(s,i);if(a)return a.version!==null&&!a.strictValidation&&a.notes.push(`contracts ${a.version} 无严格校验面(需 ≥0.9.0)——升级应用 @tbox.cn/app-contracts 依赖后可用`),t.resolved=R(s),a}return t.resolved=null,i.length===0&&i.push("contracts 不可解析(未安装)——检查应用依赖安装后重试"),{...v,notes:i}})(),d.set(e,t),t.promise}function L(e){e===void 0?d.clear():d.delete(e)}export{p as a,U as b,L as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as a,c as b}from"./chunk-KPD3LUH4.js";export{b as invalidateContractsResolver,a as resolveContractsForApp};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { WalkCatalog, WalkManifestsResult, WalkModuleInfo } from '../assembly/walk-manifests.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 域级联绑定单源(v4.4 F1/F3——integrations 层落位依据:谓词(同层)/ 写核(同层)/
|
|
5
|
-
* views(下层可向上 import)三向消费的唯一公共下层;import 前缀 guard 覆盖)。
|
|
6
|
-
*
|
|
7
|
-
* 知识锚(module-tab note 单源锚表):
|
|
8
|
-
* 域级联(值 + 层) = resolveDomainBinding —— 谓词 #7(等价重构,语义 ≡
|
|
9
|
-
* `domains[d].provider ?? root.provider`)/ deriveBindingLayer 域级段 /
|
|
10
|
-
* buildInheritance module 段 / ProviderBinding
|
|
11
|
-
* 来源层四层推导 = deriveBindingLayer(views/service-resolutions,域级段改调本文件)
|
|
12
|
-
* 写通道可用性 = moduleDomainWritable —— configEditable 读标志 / applyModule 400 守卫共用
|
|
13
|
-
* (D34:读标志与写守卫一函数)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/** 域前缀换算(service → 首点段;自 views/context.ts 迁入——文件格式词汇,静态面零 contracts 依赖) */
|
|
17
|
-
export function domainOfService(service: string): string {
|
|
18
|
-
const i = service.indexOf('.');
|
|
19
|
-
return i === -1 ? service : service.slice(0, i);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** 域级联绑定解析(值 + 来源层):provider 取非空字符串(域级优先 → root 兜底;空串 = 缺席)。
|
|
23
|
-
* layer:域级声明 → 'module';root 兜底 → 'app';双缺 → null(ProviderBinding 双缺诚实态)。 */
|
|
24
|
-
export function resolveDomainBinding(
|
|
25
|
-
config: Record<string, unknown> | null,
|
|
26
|
-
domainKey: string,
|
|
27
|
-
): { provider: string | null; layer: 'module' | 'app' | null } {
|
|
28
|
-
const domainProvider = (config?.domains as Record<string, { provider?: unknown }> | undefined)?.[domainKey]?.provider;
|
|
29
|
-
if (typeof domainProvider === 'string' && domainProvider.length > 0) {
|
|
30
|
-
return { provider: domainProvider, layer: 'module' };
|
|
31
|
-
}
|
|
32
|
-
const rootProvider = config?.provider;
|
|
33
|
-
if (typeof rootProvider === 'string' && rootProvider.length > 0) {
|
|
34
|
-
return { provider: rootProvider, layer: 'app' };
|
|
35
|
-
}
|
|
36
|
-
return { provider: null, layer: null };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** 多域模块可写性(D34 判别联合——configEditable 读标志与 applyModule 写守卫共用单源) */
|
|
40
|
-
export type ModuleDomainWritability =
|
|
41
|
-
| { writable: true; domainKeys: [string]; domainKey: string }
|
|
42
|
-
| { writable: false; reason: 'zero-domain'; domainKeys: [] }
|
|
43
|
-
| { writable: false; reason: 'multi-domain'; domainKeys: string[] }
|
|
44
|
-
| { writable: false; reason: 'shared-key'; domainKeys: [string]; domainKey: string; coOwners: string[] };
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 多域模块可写性判定(v4.4 F3):domainKeys = demand 服务首段去重;
|
|
48
|
-
* shared-key 判据 = 域级 owner 并集计数(catalog.services 聚合含 dev-manifest 通道需求——
|
|
49
|
-
* P13 真源;>1 个 distinct owner ⟺ 共享。免 owner↔moduleId 身份映射——本地模块 catalog
|
|
50
|
-
* owner = entry.package ≠ mod.pkg `@app/<id>`,身份比较必踩;「他方与本模块需求同一服务」
|
|
51
|
-
* 形态亦被并集计数天然覆盖)。
|
|
52
|
-
*/
|
|
53
|
-
export function moduleDomainWritable(
|
|
54
|
-
walk: Pick<WalkManifestsResult, 'modules' | 'catalog'>,
|
|
55
|
-
mod: WalkModuleInfo,
|
|
56
|
-
): ModuleDomainWritability {
|
|
57
|
-
const domainKeys = [...new Set((mod.descriptor.contributes.services ?? []).map((s) => domainOfService(s.service)))].sort();
|
|
58
|
-
if (domainKeys.length === 0) return { writable: false, reason: 'zero-domain', domainKeys: [] };
|
|
59
|
-
if (domainKeys.length > 1) return { writable: false, reason: 'multi-domain', domainKeys };
|
|
60
|
-
const domainKey = domainKeys[0];
|
|
61
|
-
const owners = new Set<string>();
|
|
62
|
-
for (const [service, demand] of Object.entries(walk.catalog.services)) {
|
|
63
|
-
if (domainOfService(service) === domainKey) for (const o of demand.owners) owners.add(o);
|
|
64
|
-
}
|
|
65
|
-
if (owners.size > 1) {
|
|
66
|
-
return { writable: false, reason: 'shared-key', domainKeys: [domainKey], domainKey, coOwners: [...owners].sort() };
|
|
67
|
-
}
|
|
68
|
-
return { writable: true, domainKeys: [domainKey], domainKey };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** owner 串 → 显示名(尽力映射 module install id;未命中回退原始串——message 质量问题非正确性) */
|
|
72
|
-
export function displayOwnerName(modules: ReadonlyArray<{ id: string; pkg: string }>, owner: string): string {
|
|
73
|
-
const mod = modules.find((m) => m.pkg === owner || `@app/${m.id}` === owner);
|
|
74
|
-
if (mod) return mod.id;
|
|
75
|
-
return owner.startsWith('@app/') ? owner.slice('@app/'.length) : owner;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** 供给足迹(该厂商全部供给服务并集——应用级 integrationSchemas 作用域,D32) */
|
|
79
|
-
export function providerFootprint(catalog: WalkCatalog, provider: string): string[] {
|
|
80
|
-
return [...new Set(Object.values(catalog.providers[provider] ?? {}).flatMap((e) => e.services))].sort();
|
|
81
|
-
}
|