@tbox.cn/app-toolkit 0.1.0 → 0.2.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/src/factory.ts CHANGED
@@ -19,10 +19,15 @@ import { loadModulesView, loadModuleDetailView } from './views/modules.js';
19
19
  import { loadAppView } from './views/app.js';
20
20
  import { loadServiceDetailView } from './views/service-detail.js';
21
21
  import { loadServiceResolutionsView, loadServiceResolutionDetail } from './views/service-resolutions.js';
22
+ import { loadAppIntegrationSchemas, loadModuleIntegrationSchemas } from './views/integration-schemas.js';
23
+ import { loadCredentialsView } from './views/credentials.js';
24
+ import { loadCredentialTypesView } from './views/credential-types.js';
22
25
  import type { WalkManifestsResult } from './assembly/walk-manifests.js';
23
26
  import type {
24
27
  AppIntegrationNode,
25
28
  AppView,
29
+ CredentialTypesView,
30
+ CredentialsView,
26
31
  ModuleDetailView,
27
32
  ModuleIntegrationNode,
28
33
  ProviderDetail,
@@ -36,15 +41,17 @@ import type {
36
41
  } from './dto.js';
37
42
 
38
43
  /**
39
- * createAppToolkit(C4 工厂):22 方法 = 9 读(HTTP 直通)+ 4 写(HTTP 直通)+ 9 迁移面(Internals)。
44
+ * createAppToolkit(C4 工厂;v4.4——24 方法 = 11 读(含 credentials/credential-types 双端点)
45
+ * + 4 写 + 9 迁移面;loadApp/loadModule/loadProvider 增 include 选项)。
40
46
  * AppToolkit = AppToolkitRead & AppToolkitWrite & AppToolkitInternals——
41
47
  * Read+Write = HTTP 直通面 additive 冻结(L1 端点↔方法 1:1,L3 命名镜像:Integration=节点级 /
42
48
  * Config=文件级);Internals = CLI/doctor/桥消费(自由演进)。
43
49
  *
44
50
  * **门控断言单点(policy at edge)**:求值(2)+ 写(4)+ ensureMockBindings / writeIntegrationsConfig /
45
51
  * evaluateIntegrationServices 入口 assertContractsUsable——strictValidation === false → 503
46
- * CONTRACTS_NOT_RESOLVED(message 三分支);静态方法不门控(永不 503——铁律 #3);
47
- * integrations/ views/ 机制层保持纯函数。
52
+ * CONTRACTS_NOT_RESOLVED;静态方法不门控(永不 503——铁律 #3);**include 装配为条件分支非门控**
53
+ * (D31——serviceResolutions 按 evaluation && strictValidation 双条件装配,<0.9/形态异常 → null,
54
+ * 静态端点永不 503;integrationSchemas 纯静态轴零 contracts);integrations/ views/ 机制层保持纯函数。
48
55
  *
49
56
  * 并发:进程内写串行(per-factory promise chain)+ 原子写 + 写后本实例缓存即刻失效(D26)。
50
57
  * onApplied(D7):toolkit 判定「需要重启」(实际落盘且非 dryRun/no-diff)→ 调用一次 +
@@ -56,17 +63,45 @@ export interface AppToolkitOptions {
56
63
  onApplied?: () => void;
57
64
  }
58
65
 
59
- /** ── HTTP 直通面(9 读;L1 镜像律:方法名 = load + URL 资源段)── */
66
+ /** include 协议白名单(v4.4 D30/P15——键名 = 产出字段名 1:1;per 端点;注册表 additive-only) */
67
+ const INCLUDE_WHITELIST = {
68
+ app: ['serviceResolutions', 'integrationSchemas'],
69
+ module: ['serviceResolutions', 'integrationSchemas'],
70
+ provider: ['integrationSchemas'],
71
+ } as const;
72
+
73
+ /** include 校验(P15 单点在 toolkit 方法内——壳层仅解析可重复参数,零校验纪律不变):
74
+ * 未知/越权键 → 400 VALIDATION_FAILED fields ['include'];重复键静默去重。 */
75
+ function validateInclude(kind: keyof typeof INCLUDE_WHITELIST, include?: string[]): Set<string> {
76
+ if (!include || include.length === 0) return new Set();
77
+ const allowed = INCLUDE_WHITELIST[kind] as readonly string[];
78
+ const rejected = [...new Set(include)].filter((k) => !allowed.includes(k));
79
+ if (rejected.length > 0) {
80
+ throw new AppToolkitError(
81
+ 'VALIDATION_FAILED',
82
+ 400,
83
+ `include 含未知或不适用于本端点的键:${rejected.join('、')}(白名单:${allowed.join('、')})`,
84
+ ['include'],
85
+ );
86
+ }
87
+ return new Set(include);
88
+ }
89
+
90
+ /** ── HTTP 直通面(11 读;L1 镜像律:方法名 = load + URL 资源段;query → 选项字段 L4)── */
60
91
  export interface AppToolkitRead {
61
92
  loadModules(): Promise<ModulesView>;
62
- loadModule(moduleId: string): Promise<ModuleDetailView>;
63
- loadApp(): Promise<AppView>;
93
+ loadModule(moduleId: string, o?: { include?: string[] }): Promise<ModuleDetailView>;
94
+ loadApp(o?: { include?: string[] }): Promise<AppView>;
64
95
  loadService(service: string): Promise<ServiceDetail>;
65
96
  loadServiceResolutions(services?: string[]): Promise<ServiceResolutionsView>;
66
97
  loadServiceResolution(service: string): Promise<ServiceResolutionDetail>;
67
98
  loadProviders(): Promise<ProvidersView>;
68
- loadProvider(provider: string): Promise<ProviderDetail>;
99
+ loadProvider(provider: string, o?: { include?: string[] }): Promise<ProviderDetail>;
69
100
  loadProviderService(provider: string, service: string): Promise<ProviderServiceDetail>;
101
+ /** GET /credentials(v4.4 D33——应用状态轴;零门控,永不 503) */
102
+ loadCredentials(): Promise<CredentialsView>;
103
+ /** GET /credential-types[?provider=](v4.4 D33——供给轴静态;零门控,永不 503) */
104
+ loadCredentialTypes(provider?: string): Promise<CredentialTypesView>;
70
105
  }
71
106
 
72
107
  /** ── HTTP 直通面(4 写;body ≡ 文件节点全量替换;DELETE 无 dryRun/无 options)── */
@@ -163,13 +198,39 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
163
198
  const contracts = await resolveContractsForApp(appDir);
164
199
  return loadModulesView(snapshot(contracts));
165
200
  },
166
- async loadModule(moduleId: string) {
201
+ // v4.4 D31:门控与内嵌分离——静态方法零门控分支;include=serviceResolutions 按
202
+ // evaluation && strictValidation 双条件装配(<0.9/形态异常 → null,永不 503——技术方案 §2.4:
203
+ // 仅判 strictValidation 会被「严格面在场+求值面缺席」畸形包击穿)
204
+ async loadModule(moduleId: string, o?: { include?: string[] }): Promise<ModuleDetailView> {
205
+ const include = validateInclude('module', o?.include); // ① 输入校验先行(400 先于 404)
167
206
  const contracts = await resolveContractsForApp(appDir);
168
- return loadModuleDetailView(snapshot(contracts), moduleId);
207
+ const snap = snapshot(contracts);
208
+ const view = loadModuleDetailView(snap, moduleId); // ② MODULE_NOT_FOUND
209
+ if (include.has('serviceResolutions')) {
210
+ view.serviceResolutions = contracts.evaluation && contracts.strictValidation
211
+ ? await predicateIssues().then((issues) =>
212
+ loadServiceResolutionsView(snap, issues, view.services.map((s) => s.service)))
213
+ : null; // module = demand 服务过滤(≡ 批量 ?service= 语义;issues 恒全量)
214
+ }
215
+ if (include.has('integrationSchemas')) {
216
+ view.integrationSchemas = loadModuleIntegrationSchemas(snap, moduleId);
217
+ }
218
+ return view;
169
219
  },
170
- async loadApp() {
220
+ async loadApp(o?: { include?: string[] }): Promise<AppView> {
221
+ const include = validateInclude('app', o?.include);
171
222
  const contracts = await resolveContractsForApp(appDir);
172
- return loadAppView(snapshot(contracts));
223
+ const snap = snapshot(contracts);
224
+ const view = loadAppView(snap);
225
+ if (include.has('serviceResolutions')) {
226
+ view.serviceResolutions = contracts.evaluation && contracts.strictValidation
227
+ ? await predicateIssues().then((issues) => loadServiceResolutionsView(snap, issues)) // app = 全量键集
228
+ : null;
229
+ }
230
+ if (include.has('integrationSchemas')) {
231
+ view.integrationSchemas = loadAppIntegrationSchemas(snap);
232
+ }
233
+ return view;
173
234
  },
174
235
  async loadService(service: string) {
175
236
  const contracts = await resolveContractsForApp(appDir);
@@ -191,14 +252,24 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
191
252
  const contracts = await resolveContractsForApp(appDir);
192
253
  return loadProvidersView(snapshot(contracts));
193
254
  },
194
- async loadProvider(provider: string) {
255
+ async loadProvider(provider: string, o?: { include?: string[] }): Promise<ProviderDetail> {
256
+ const include = validateInclude('provider', o?.include);
195
257
  const contracts = await resolveContractsForApp(appDir);
196
- return loadProviderDetailView(snapshot(contracts), provider);
258
+ return loadProviderDetailView(snapshot(contracts), provider, include);
197
259
  },
198
260
  async loadProviderService(provider: string, service: string) {
199
261
  const contracts = await resolveContractsForApp(appDir);
200
262
  return loadProviderServiceDetailView(snapshot(contracts), provider, service);
201
263
  },
264
+ // v4.4 D33:凭据域双端点(应用状态轴 / 供给轴静态——零门控零求值,永不 503)
265
+ async loadCredentials(): Promise<CredentialsView> {
266
+ const contracts = await resolveContractsForApp(appDir);
267
+ return loadCredentialsView(snapshot(contracts));
268
+ },
269
+ async loadCredentialTypes(provider?: string): Promise<CredentialTypesView> {
270
+ const contracts = await resolveContractsForApp(appDir);
271
+ return loadCredentialTypesView(snapshot(contracts), provider);
272
+ },
202
273
  };
203
274
 
204
275
  const core = createWriteCore(appDir, {
package/src/index.ts CHANGED
@@ -79,19 +79,22 @@ export type { AppliedCredential, CredentialInput, OrphanIssue, PlannedCredential
79
79
  export { AppToolkitError, contractsNotResolvedMessage } from './core/errors.js';
80
80
  export type { AppToolkitErrorCode } from './core/errors.js';
81
81
 
82
- // ── views(五投影——静态四轴 + 求值轴;铁律 #3 静态/求值分离由投影结构承载)──
83
- export { loadModulesView, loadModuleDetailView, moduleDomainKeyOf } from './views/modules.js';
82
+ // ── views(静态四轴 + 求值轴 + v4.4 层级 schema/凭据域;铁律 #3 静态/求值分离由投影结构承载)──
83
+ export { loadModulesView, loadModuleDetailView } from './views/modules.js';
84
84
  export { loadAppView } from './views/app.js';
85
85
  export { loadServiceDetailView } from './views/service-detail.js';
86
86
  export { loadProvidersView, loadProviderDetailView, loadProviderServiceDetailView } from './views/providers.js';
87
87
  export { loadServiceResolutionsView, loadServiceResolutionDetail, resolveServiceSummary } from './views/service-resolutions.js';
88
+ export { loadAppIntegrationSchemas, loadModuleIntegrationSchemas, resolveLayerIntegrationSchemas } from './views/integration-schemas.js';
89
+ export { echoForRef, loadCredentialsView } from './views/credentials.js';
90
+ export { loadCredentialTypesView } from './views/credential-types.js';
88
91
  export type { ViewContext, ViewSnapshot } from './views/context.js';
89
92
 
90
- // ── 工厂(22 方法 = 9 读 + 4 写 + 9 Internals;门控 policy at edge;写串行)──
93
+ // ── 工厂(v4.4:24 方法 = 11 读 + 4 写 + 9 Internals;门控 policy at edge;include 条件装配非门控)──
91
94
  export { createAppToolkit } from './factory.js';
92
95
  export type { AppToolkit, AppToolkitRead, AppToolkitWrite, AppToolkitInternals, AppToolkitOptions } from './factory.js';
93
96
 
94
- // ── DTO(api.md §3 字段级单源——28 类型;agtcodingbox/tbox-ai-coding import type 消费)──
97
+ // ── DTO(api.md §3 字段级单源——34 类型;agtcodingbox/tbox-ai-coding import type 消费)──
95
98
  export type {
96
99
  ServiceStatus,
97
100
  BindingLayer,
@@ -101,6 +104,12 @@ export type {
101
104
  ModuleDetailView,
102
105
  AppView,
103
106
  ServiceDetail,
107
+ ProviderBinding,
108
+ LayerIntegrationSchemas,
109
+ CredentialsView,
110
+ CredentialBindingEcho,
111
+ CredentialTypesView,
112
+ CredentialTypeEntry,
104
113
  ServiceResolutionsView,
105
114
  ServiceResolutionEntry,
106
115
  ServiceResolution,
@@ -0,0 +1,81 @@
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
+ }
@@ -60,6 +60,7 @@
60
60
  * → warning(moduleResourceNeeds 缺席 → 需求面检查跳过,M3 降级先例)
61
61
  */
62
62
  import type { EvaluationExports } from '../core/contracts-expected.js';
63
+ import { resolveDomainBinding } from './domain-binding.js';
63
64
 
64
65
  /** contracts 注入面(resolver 装配产物运行时视图 = EvaluationExports 求值子集) */
65
66
  export type PredicateContracts = Pick<
@@ -513,13 +514,12 @@ export function evaluateIntegrationServices(
513
514
  };
514
515
  checkLayer(integrations.config, 'root.config');
515
516
  for (const [k, v] of Object.entries(integrations.configByInstance ?? {})) checkLayer(v, `root.configByInstance[${k}]`);
516
- // 域级 config:键 ⊆ 该域生效厂商(domains[d].provider ?? root.provider)schema——
517
- // 生效厂商缺席(全走槽级覆盖)→ 用全局并集;生效厂商 schema 缺失 → 跳过该域
517
+ // 域级 config:键 ⊆ 该域生效厂商 schema(resolveDomainBinding 单源——v4.4 F1 等价重构,
518
+ // 语义 ≡ 现状 domains[d].provider ?? root.provider)——生效厂商缺席(全走槽级覆盖)→
519
+ // 用全局并集;生效厂商 schema 缺失 → 跳过该域
518
520
  for (const [d, cfg] of Object.entries(integrations.domains ?? {})) {
519
521
  if (!cfg?.config) continue;
520
- const domainVendor = typeof cfg.provider === 'string' && cfg.provider
521
- ? cfg.provider
522
- : (typeof integrations.provider === 'string' ? integrations.provider : undefined);
522
+ const domainVendor = resolveDomainBinding(integrations, d).provider ?? undefined;
523
523
  if (domainVendor === undefined) {
524
524
  checkLayer(cfg.config, `domains.${d}.config`);
525
525
  continue;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { resolveContractsForApp, type ResolvedContracts } from '../core/contracts-resolver.js';
4
+ import { AppToolkitError } from '../core/errors.js';
4
5
  import type { FileCache } from '../core/file-cache.js';
5
6
 
6
7
  /**
@@ -45,9 +46,28 @@ export async function readIntegrationsStrict(
45
46
  const raw = readIntegrationsConfig(appDir, cache);
46
47
  if (raw === null) return { config: null, strict: false, contracts };
47
48
  if (contracts.strictValidation && contracts.module.parseIntegrationsConfig) {
48
- // 严格链:assertNoInlineSecrets → 旧键直切防线 → zod strict(contracts 单源)
49
- const parsed = contracts.module.parseIntegrationsConfig(raw);
50
- return { config: parsed as unknown as Record<string, unknown>, strict: true, contracts };
49
+ // 严格链:assertNoInlineSecrets → 旧键直切防线 → zod strict(contracts 单源);
50
+ // 失败转换(A1——裸 ZodError/Error 穿透 500 的同族缺口):zod → 400 + fields(原始路径——
51
+ // 文件级读无节点上下文);普通 Error → 400 直通。结构性判定同 write.runStrictValidation。
52
+ try {
53
+ const parsed = contracts.module.parseIntegrationsConfig(raw);
54
+ return { config: parsed as unknown as Record<string, unknown>, strict: true, contracts };
55
+ } catch (err) {
56
+ if (err instanceof Error && Array.isArray((err as { issues?: unknown }).issues)) {
57
+ const fields = ((err as unknown as { issues: Array<{ path?: unknown }> }).issues)
58
+ .filter((i) => Array.isArray(i.path))
59
+ .map((i) => (i.path as Array<string | number>).map(String).join('.'))
60
+ .filter((p) => p.length > 0);
61
+ throw new AppToolkitError('VALIDATION_FAILED', 400, 'integrations.json 校验失败', fields.length > 0 ? fields : undefined);
62
+ }
63
+ throw new AppToolkitError('VALIDATION_FAILED', 400, err instanceof Error ? err.message : String(err));
64
+ }
51
65
  }
52
66
  return { config: raw, strict: false, contracts };
53
67
  }
68
+
69
+ /** P9 服务身份判据单源(A2):GET/PUT/DELETE 共用「词汇 ∪ 已配置」——service-detail 判据逐字冻结
70
+ * (api.md P9 的「demanded 退化」口径为文档层 nuance,现行实现与 GET 均不含 demanded——冻结现状即单源) */
71
+ export function isKnownService(vocabulary: readonly string[], configuredKeys: readonly string[], service: string): boolean {
72
+ return vocabulary.includes(service) || configuredKeys.includes(service);
73
+ }
@@ -17,6 +17,8 @@ import {
17
17
  } from './credentials.js';
18
18
  import type { IntegrationServiceIssue } from './predicate.js';
19
19
  import { checkInstanceKeys } from './predicate.js';
20
+ import { isKnownService } from './read.js';
21
+ import { displayOwnerName, moduleDomainWritable } from './domain-binding.js';
20
22
  import { AppToolkitError } from '../core/errors.js';
21
23
 
22
24
  /**
@@ -185,6 +187,11 @@ function validateCredential(
185
187
  inlineCredentialType: string | undefined,
186
188
  cache?: FileCache,
187
189
  ): void {
190
+ // ⓪ type 在场性(A1 无条件前置):缺 type 的凭据输入原会以 { type: undefined, ...values }
191
+ // 落盘(JSON.stringify 静默丢键)→ 无 type 文件 + GET 回显静默缺席——收紧为显式 400
192
+ if (typeof credentials.type !== 'string' || credentials.type.length === 0) {
193
+ throw new AppToolkitError('VALIDATION_FAILED', 400, '凭据 type 缺失或为空字符串', ['credentials.type']);
194
+ }
188
195
  // ① type 匹配(内联声明优先;catalog 槽供给次之;均缺席 → 跳过——宽松与 P11 对齐)
189
196
  const expectedType = inlineCredentialType ?? declaredCredentialType(appDir, provider, service, cache);
190
197
  if (expectedType !== undefined && expectedType !== '-' && credentials.type !== expectedType) {
@@ -222,14 +229,12 @@ export interface WriteCore {
222
229
  warmup(): Promise<void>;
223
230
  /** PUT /app/integration(root 节点全集) */
224
231
  applyApp(node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
225
- /** PUT /modules/:module/integration(domains 换算;N1 {} 删域键) */
232
+ /** PUT /modules/:module/integration(domains 换算;N1 {} 删域键;v4.4 D34 写守卫单源) */
226
233
  applyModule(moduleId: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
227
- /** PUT /services/:service/integration(整节点含 instances map;N3 {} 不归一化) */
234
+ /** PUT /services/:service/integration(整节点含 instances map;N3 {} 不归一化;v4.4 A2 身份判据) */
228
235
  applyService(service: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
229
- /** DELETE /services/:service/integration(恒整节点;幂等;无预演) */
236
+ /** DELETE /services/:service/integration(恒整节点;幂等仅判据内成员;无预演) */
230
237
  deleteService(service: string): SaveResult;
231
- /** 模块服务域键(module → domains 键 = 服务集派生域键集 upsert 位) */
232
- moduleDomainKey(moduleId: string): string | undefined;
233
238
  }
234
239
 
235
240
  export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): WriteCore {
@@ -270,7 +275,23 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
270
275
  strictContracts = await resolveContractsForApp(appDir);
271
276
  }
272
277
 
273
- function runStrictValidation(next: Record<string, unknown>, issues: IntegrationServiceIssue[]): void {
278
+ /** zod issues → 节点相对 fields(A1):字面量前缀剥离——service 名含点号,禁按段数切分;
279
+ * 无前缀命中的文档级路径(如 'services')原样保留;空 path(文档根级形态)→ 滤除
280
+ * (fields 直指字段名——空串指向不存在)。 */
281
+ function toNodeFields(issues: Array<{ path?: unknown }>, prefix: string): string[] {
282
+ return issues
283
+ .filter((i) => Array.isArray(i.path))
284
+ .map((i) => {
285
+ const p = (i.path as Array<string | number>).map(String).join('.');
286
+ return prefix !== '' && p.startsWith(prefix) && p.length > prefix.length ? p.slice(prefix.length) : p;
287
+ })
288
+ .filter((p) => p.length > 0);
289
+ }
290
+
291
+ /** 严格整文档校验(A1 单点转换):zod → 400 + fields(节点相对);普通 Error(旧键直切/
292
+ * 内联密钥防线——contracts parseIntegrationsConfig 内部 assert 族)→ 同族 400 message 直通。
293
+ * 结构性 ZodError 判定(禁 instanceof——contracts 动态 import,类身份跨模块实例不可靠)。 */
294
+ function runStrictValidation(next: Record<string, unknown>, issues: IntegrationServiceIssue[], prefix: string): void {
274
295
  const strict = strictContracts;
275
296
  if (!strict || !strict.strictValidation || !strict.module.parseIntegrationsConfig) {
276
297
  throw new AppToolkitError(
@@ -280,20 +301,23 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
280
301
  );
281
302
  }
282
303
  void issues;
283
- strict.module.parseIntegrationsConfig(next);
304
+ try {
305
+ strict.module.parseIntegrationsConfig(next);
306
+ } catch (err) {
307
+ if (err instanceof Error && Array.isArray((err as { issues?: unknown }).issues)) {
308
+ throw new AppToolkitError(
309
+ 'VALIDATION_FAILED',
310
+ 400,
311
+ '节点 schema 校验失败',
312
+ toNodeFields((err as unknown as { issues: Array<{ path?: unknown }> }).issues, prefix),
313
+ );
314
+ }
315
+ throw new AppToolkitError('VALIDATION_FAILED', 400, err instanceof Error ? err.message : String(err));
316
+ }
284
317
  }
285
318
 
286
319
  return {
287
320
  warmup,
288
- moduleDomainKey(moduleId: string): string | undefined {
289
- const { modules } = walkManifests(appDir, cache);
290
- const mod = modules.find((m) => m.id === moduleId);
291
- if (!mod) return undefined;
292
- const services = mod.descriptor.contributes.services ?? [];
293
- const domains = new Set(services.map((s) => (s.service.includes('.') ? s.service.slice(0, s.service.indexOf('.')) : s.service)));
294
- // 单域模块 → 该域键;多域/零域 → 模块 id 兜底键(domains 换算层约定)
295
- return domains.size === 1 ? [...domains][0] : moduleId;
296
- },
297
321
 
298
322
  applyApp(node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
299
323
  assertSafeNames(node, 'root');
@@ -332,7 +356,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
332
356
  if (noDiff && !hasCredentialInput) {
333
357
  return { written: false, restartScheduled: false, files: [], credentials: [], issues };
334
358
  }
335
- runStrictValidation(next, issues);
359
+ runStrictValidation(next, issues, '');
336
360
  // 凭据工件落盘(后置于校验通过;dryRun 零写盘 + existsSync 预览)
337
361
  const dryRun = writeOpts.dryRun === true;
338
362
  const applied = writeCredentialArtifacts(appDir, plan, dryRun);
@@ -342,6 +366,33 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
342
366
 
343
367
  applyModule(moduleId: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
344
368
  assertSafeNames(node, 'root');
369
+ // D34 写守卫(v4.4,moduleDomainWritable 单源):404(模块在册)→ 400(多域/零域/共享键
370
+ // 三分支)→ 既有管线。守卫先于 no-op 短路——多域/零域/共享键模块 PUT {} 清域同 400
371
+ // (「写路径拒绝歧义操作」含清除;P13/P17)。
372
+ const { modules, catalog } = walkManifests(appDir, cache);
373
+ const mod = modules.find((m) => m.id === moduleId);
374
+ if (!mod) {
375
+ throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
376
+ }
377
+ const w = moduleDomainWritable({ modules, catalog }, mod);
378
+ if (!w.writable) {
379
+ if (w.reason === 'multi-domain') {
380
+ throw new AppToolkitError(
381
+ 'VALIDATION_FAILED',
382
+ 400,
383
+ `模块 ${moduleId} 跨 ${w.domainKeys.length} 个域(${w.domainKeys.join('、')})——模块级统一配置仅支持单域模块,请按服务级配置(编辑各服务行)`,
384
+ );
385
+ }
386
+ if (w.reason === 'zero-domain') {
387
+ throw new AppToolkitError('VALIDATION_FAILED', 400, `模块 ${moduleId} 无服务需求声明,无域配置位`);
388
+ }
389
+ throw new AppToolkitError(
390
+ 'VALIDATION_FAILED',
391
+ 400,
392
+ `域键 ${w.domainKey} 由 ${w.coOwners.map((o) => displayOwnerName(modules, o)).join('、')} 共用——写通道拒绝歧义操作,请按服务级配置`,
393
+ );
394
+ }
395
+ const domainKey = w.domainKey;
345
396
  const issues: IntegrationServiceIssue[] = [];
346
397
  const current = loadConfig(appDir, cache);
347
398
  if (current === null && Object.keys(node).length === 0) {
@@ -356,10 +407,6 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
356
407
  });
357
408
  }
358
409
  const base: Record<string, unknown> = current ?? { services: {} };
359
- const domainKey = this.moduleDomainKey(moduleId);
360
- if (domainKey === undefined) {
361
- throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
362
- }
363
410
  const domains = { ...((base.domains as Record<string, unknown>) ?? {}) };
364
411
  const next: Record<string, unknown> = { ...base, domains };
365
412
  // N1:模块节点 {} → 删 domains[键](清域);其余 = 域键全量替换
@@ -368,7 +415,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
368
415
  if (deepEqual(current, next)) {
369
416
  return { written: false, restartScheduled: false, files: [], credentials: [], issues };
370
417
  }
371
- runStrictValidation(next, issues);
418
+ runStrictValidation(next, issues, `domains.${domainKey}.`);
372
419
  issues.push(...registryCheck(next));
373
420
  return finalize(next, [], issues, writeOpts.dryRun, false);
374
421
  },
@@ -378,6 +425,10 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
378
425
  assertSafeInstanceKeys(node, writeOpts, 'slot');
379
426
  const issues: IntegrationServiceIssue[] = [];
380
427
  const current = loadConfig(appDir, cache);
428
+ // P9 服务身份判据单源(A2):写入集 = 词汇 ∪ 已配置——未知服务(含 typo)404(api.md §5)
429
+ if (!isKnownService(walkManifests(appDir, cache).vocabulary, Object.keys((current?.services as Record<string, unknown> | undefined) ?? {}), service)) {
430
+ throw new AppToolkitError('SERVICE_NOT_FOUND', 404, `服务 ${service} 不在服务词汇(已装契约包 ∪ 已配置)`);
431
+ }
381
432
  if (current === null) {
382
433
  issues.push({
383
434
  severity: 'warning',
@@ -418,7 +469,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
418
469
  if (noDiff && !hasCredentialInput) {
419
470
  return { written: false, restartScheduled: false, files: [], credentials: [], issues };
420
471
  }
421
- runStrictValidation(next, issues);
472
+ runStrictValidation(next, issues, `services.${service}.`);
422
473
  // 凭据工件落盘(后置于校验通过;dryRun 零写盘 + existsSync 预览)
423
474
  const dryRun = writeOpts.dryRun === true;
424
475
  const applied = writeCredentialArtifacts(appDir, plan, dryRun);
@@ -429,6 +480,10 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
429
480
  deleteService(service: string): SaveResult {
430
481
  const issues: IntegrationServiceIssue[] = [];
431
482
  const current = loadConfig(appDir, cache);
483
+ // P9 服务身份判据单源(A2):判据外未知服务 404;判据内缺席节点维持幂等 written:false
484
+ if (!isKnownService(walkManifests(appDir, cache).vocabulary, Object.keys((current?.services as Record<string, unknown> | undefined) ?? {}), service)) {
485
+ throw new AppToolkitError('SERVICE_NOT_FOUND', 404, `服务 ${service} 不在服务词汇(已装契约包 ∪ 已配置)`);
486
+ }
432
487
  const services = { ...((current?.services as Record<string, unknown>) ?? {}) };
433
488
  if (!(service in services)) {
434
489
  // 幂等:清不存在的节点 → written:false(无 409;无预演)
package/src/views/app.ts CHANGED
@@ -1,11 +1,11 @@
1
- import type { AppView } from '../dto.js';
1
+ import type { AppView, ServiceDemand } from '../dto.js';
2
2
  import { APP_INTEGRATION_NODE_KEYS } from '../dto.js';
3
3
  import { moduleOccupiedServices, type ViewSnapshot } from './context.js';
4
4
 
5
5
  /**
6
- * 应用静态投影(views/app;C4;FX-1c B10——root 节点投影修)。
7
- * /app = 平台服务 + root 节点原值(纯静态,D12):
8
- * services = 服务词汇 − 模块占用(裸 string[]——owner/标题 UI 按首点段派生,F2);
6
+ * 应用静态投影(views/app;C4;FX-1c B10——root 节点投影修;v4.4 D29——services 双端点同型)。
7
+ * /app = 平台服务 + root 节点原值(纯静态):services = 服务词汇 − 模块占用
8
+ * (ServiceDemand[]——平台条目 required 恒 false、title/description 缺席,D29);
9
9
  * 平台服务状态不在此(→ /service-resolutions);模块服务不在此(→ /modules/:m)。
10
10
  * integration = **root 节点投影**(APP_INTEGRATION_NODE_KEYS——D21/L1:GET 预填 ≡ PUT body;
11
11
  * 文件另含 services/domains/modules 域键,不进 root 节点投影——原实现整文件透传,
@@ -14,7 +14,9 @@ import { moduleOccupiedServices, type ViewSnapshot } from './context.js';
14
14
 
15
15
  export function loadAppView(snapshot: ViewSnapshot): AppView {
16
16
  const occupied = moduleOccupiedServices(snapshot.walk);
17
- const services = snapshot.walk.vocabulary.filter((s) => !occupied.has(s));
17
+ const services: ServiceDemand[] = snapshot.walk.vocabulary
18
+ .filter((s) => !occupied.has(s))
19
+ .map((s) => ({ service: s, required: false }));
18
20
  const config = snapshot.integrations;
19
21
  let integration: AppView['integration'] = null;
20
22
  if (config) {
@@ -1,11 +1,12 @@
1
1
  import { readIntegrationsConfig } from '../integrations/read.js';
2
+ import { domainOfService } from '../integrations/domain-binding.js';
2
3
  import { walkManifests, type WalkCatalog, type WalkManifestsResult } from '../assembly/walk-manifests.js';
3
4
  import type { FileCache } from '../core/file-cache.js';
4
5
  import type { ResolvedContracts } from '../core/contracts-resolver.js';
5
6
 
6
7
  /**
7
8
  * 视图装配共享上下文(五投影共用;目录枚举零缓存——walkManifests 每次调用执行)。
8
- * 域前缀换算(service → 首点段)= 本地一行 helper(文件格式词汇,静态面零 contracts 依赖)。
9
+ * 域前缀换算(service → 首点段)自 v4.4 F1 迁 integrations/domain-binding.ts(本文件 re-export 保导入路径)。
9
10
  */
10
11
 
11
12
  export interface ViewContext {
@@ -21,11 +22,7 @@ export interface ViewSnapshot {
21
22
  contracts: ResolvedContracts;
22
23
  }
23
24
 
24
- /** 域前缀换算(本地 helper——静态面零 contracts 依赖前提) */
25
- export function domainOfService(service: string): string {
26
- const i = service.indexOf('.');
27
- return i === -1 ? service : service.slice(0, i);
28
- }
25
+ export { domainOfService };
29
26
 
30
27
  /** 读取视图输入快照(每方法调用独立——Freshness Contract 单位 = 方法调用) */
31
28
  export async function loadViewSnapshot(ctx: ViewContext): Promise<ViewSnapshot> {
@@ -0,0 +1,28 @@
1
+ import type { CredentialTypesView } from '../dto.js';
2
+ import type { ViewSnapshot } from './context.js';
3
+ import { readSchemaRef } from './providers.js';
4
+
5
+ /**
6
+ * 供给轴凭据结构清单(views/credential-types;v4.4 D33 独立端点 GET /credential-types[?provider=]):
7
+ * 结构按 credentialType 键(wanda 全部槽共享同一 type 同一 schema 文件——(provider, service)
8
+ * 组合寻址是错位键);源 = catalog.credentialTypes(既有聚合产物零新读取路径,聚合条目携
9
+ * packageDir 供 schema 文件解析)。静态轴(稳定可缓存、永不 503)。
10
+ * ?provider= 过滤 ≡ ?service= 先例(过滤非寻址;未知 provider → 空 types 不 404)。
11
+ * '-' = 无凭据语义(mock/local——UI 判 '-' 不渲染凭据表单);credentialSchema null = 未声明。
12
+ */
13
+ export function loadCredentialTypesView(snapshot: ViewSnapshot, provider?: string): CredentialTypesView {
14
+ const types =
15
+ provider === undefined
16
+ ? Object.keys(snapshot.catalog.credentialTypes)
17
+ : [...new Set(Object.values(snapshot.catalog.providers[provider] ?? {}).map((e) => e.credentialType))];
18
+ return {
19
+ types: types.sort().map((t) => {
20
+ const entry = snapshot.catalog.credentialTypes[t];
21
+ return {
22
+ credentialType: t,
23
+ owner: entry?.owner ?? '',
24
+ credentialSchema: entry ? readSchemaRef(snapshot.appDir, entry.packageDir, entry.credentialSchema) : null,
25
+ };
26
+ }),
27
+ };
28
+ }