@tbox.cn/app-toolkit 0.2.0 → 0.4.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.
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import Ajv from 'ajv';
4
4
  import { resolveContractsForApp } from '../core/contracts-resolver.js';
5
+ import { stemToFilePath } from '../core/credential-format.js';
5
6
  import type { FileCache } from '../core/file-cache.js';
6
7
  import { writeAtomic } from '../core/fskit.js';
7
8
  import { SAFE_NAME_PATTERN } from '../core/module-schema.js';
@@ -36,8 +37,9 @@ import { AppToolkitError } from '../core/errors.js';
36
37
  * 7 严格整文档校验(contracts parseIntegrationsConfig——触发 = !noDiff ∥ 有凭据输入)
37
38
  * 8 凭据工件落盘(**后置于校验通过**——防「凭据已写 + zod 拒绝」不一致态;
38
39
  * dryRun → writeCredentialArtifacts 零写盘 + existsSync 预览)
39
- * 9 finalize:written = integrations 实写 凭据实写(**D-B2**);files 分列两类路径;
40
- * restartScheduled = written && onApplied(凭据影响 boot 装配——重启正当);写后缓存失效
40
+ * 9 finalize(D35):planned = 将触碰(files 预览单源——dryRun 响应 ≡ 同 body 真实写,files 逐字对称);
41
+ * written = 实写(!dryRun 派生,D-B2);restartScheduled = written && onApplied(凭据影响 boot 装配——
42
+ * 重启正当);写后缓存失效
41
43
  * 归一化:N1 模块节点 {} → 删 domains[键];N3 服务节点 {} 不归一化(显式通配激活 = 合法 TIER0 态);
42
44
  * N2 已随 ?instance= 退役。dryRun(PUT only):复读完整管线不落盘不排程。
43
45
  * DELETE:节点移除 + 孤儿凭据写后对账(覆盖 DELETE 与 PUT 替换两类来源,只报告不清理——D9)。
@@ -245,11 +247,13 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
245
247
  appliedCredentials: AppliedCredential[],
246
248
  issues: IntegrationServiceIssue[],
247
249
  dryRun: boolean | undefined,
248
- credentialsWritten: boolean,
249
250
  ): SaveResult {
250
- // D-B2:written 任一文件实写;files 分列(integrations.json + 凭据路径);
251
- // restartScheduled 跟随 written(凭据文件影响 boot 装配——重启正当)
252
- const integrationsWritten = !dryRun && next !== null;
251
+ // D35 双轴派生:planned = 将触碰(files 预览单源——dryRun 响应 ≡ 同 body 真实写,files 逐字对称);
252
+ // written = 实际落盘(!dryRun 派生——D-B2);restartScheduled 跟随 written(凭据文件影响 boot 装配——重启正当)
253
+ const integrationsPlanned = next !== null;
254
+ const credentialsPlanned = appliedCredentials.length > 0;
255
+ const integrationsWritten = !dryRun && integrationsPlanned;
256
+ const credentialsWritten = !dryRun && credentialsPlanned;
253
257
  if (integrationsWritten) {
254
258
  writeAtomic(join(appDir, INTEGRATIONS_FILE), `${JSON.stringify(next, null, 2)}\n`);
255
259
  cache?.invalidate([join(appDir, INTEGRATIONS_FILE)]);
@@ -258,8 +262,8 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
258
262
  const restartScheduled = written && onApplied !== undefined;
259
263
  if (restartScheduled) onApplied?.();
260
264
  const files: string[] = [];
261
- if (integrationsWritten) files.push(INTEGRATIONS_FILE);
262
- if (credentialsWritten) files.push(...appliedCredentials.map((a) => `config/credentials/${a.stem}.json`));
265
+ if (integrationsPlanned) files.push(INTEGRATIONS_FILE);
266
+ if (credentialsPlanned) files.push(...appliedCredentials.map((a) => stemToFilePath(a.stem)));
263
267
  return {
264
268
  written,
265
269
  restartScheduled,
@@ -361,7 +365,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
361
365
  const dryRun = writeOpts.dryRun === true;
362
366
  const applied = writeCredentialArtifacts(appDir, plan, dryRun);
363
367
  issues.push(...registryCheck(next));
364
- return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun, !dryRun && plan.length > 0);
368
+ return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun);
365
369
  },
366
370
 
367
371
  applyModule(moduleId: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
@@ -417,7 +421,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
417
421
  }
418
422
  runStrictValidation(next, issues, `domains.${domainKey}.`);
419
423
  issues.push(...registryCheck(next));
420
- return finalize(next, [], issues, writeOpts.dryRun, false);
424
+ return finalize(next, [], issues, writeOpts.dryRun);
421
425
  },
422
426
 
423
427
  applyService(service: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
@@ -474,7 +478,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
474
478
  const dryRun = writeOpts.dryRun === true;
475
479
  const applied = writeCredentialArtifacts(appDir, plan, dryRun);
476
480
  issues.push(...registryCheck(next));
477
- return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun, !dryRun && plan.length > 0);
481
+ return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun);
478
482
  },
479
483
 
480
484
  deleteService(service: string): SaveResult {
package/src/views/app.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AppView, ServiceDemand } from '../dto.js';
2
2
  import { APP_INTEGRATION_NODE_KEYS } from '../dto.js';
3
- import { moduleOccupiedServices, type ViewSnapshot } from './context.js';
3
+ import { moduleOccupiedServices, resolveServiceDisplay, type ViewSnapshot } from './context.js';
4
4
 
5
5
  /**
6
6
  * 应用静态投影(views/app;C4;FX-1c B10——root 节点投影修;v4.4 D29——services 双端点同型)。
@@ -14,9 +14,13 @@ 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
+ // 平台条目展示名走链(B1):词汇 meta > demand meta > id——title 恒发(id 兜底内聚)
17
18
  const services: ServiceDemand[] = snapshot.walk.vocabulary
18
19
  .filter((s) => !occupied.has(s))
19
- .map((s) => ({ service: s, required: false }));
20
+ .map((s) => {
21
+ const display = resolveServiceDisplay(snapshot.walk, s);
22
+ return { service: s, title: display.title, ...(display.description !== undefined ? { description: display.description } : {}), required: false };
23
+ });
20
24
  const config = snapshot.integrations;
21
25
  let integration: AppView['integration'] = null;
22
26
  if (config) {
@@ -65,3 +65,23 @@ export function suppliedByVendors(catalog: WalkCatalog, service: string): string
65
65
  }
66
66
  return vendors.sort();
67
67
  }
68
+
69
+ /** 服务展示名解析结果(title 恒有值——id 兜底内聚) */
70
+ export interface ServiceDisplay {
71
+ title: string;
72
+ description?: string;
73
+ }
74
+
75
+ /**
76
+ * 服务展示名单源(B1):词汇 meta(slotMeta——contracts-mall 词汇锁保证 mall 域覆盖)
77
+ * > demand meta(catalog.services——域外槽兜底;首声明聚合)> service id。逐字段回落——
78
+ * 词汇条目仅含 description 时 demand title 仍可达(外部契约包部分 meta 前向容错)。
79
+ * 四视图唯一消费点(/modules/:m services、/app 平台条目、/services/:s、/providers/:p services)。
80
+ */
81
+ export function resolveServiceDisplay(walk: WalkManifestsResult, service: string): ServiceDisplay {
82
+ const vocab = walk.slotMeta[service];
83
+ const demand = walk.catalog.services[service];
84
+ const title = vocab?.title ?? demand?.title ?? service;
85
+ const description = vocab?.description ?? demand?.description;
86
+ return { title, ...(description !== undefined ? { description } : {}) };
87
+ }
@@ -1,21 +1,32 @@
1
- import type { LayerIntegrationSchemas } from '../dto.js';
1
+ import type { IntegrationFieldRegistryView, LayerIntegrationSchemas, ServiceIntegrationSchemas } from '../dto.js';
2
2
  import { isSupplied, type ViewSnapshot } from './context.js';
3
- import { readSchemaRef } from './providers.js';
4
- import { moduleDomainWritable, providerFootprint, resolveDomainBinding } from '../integrations/domain-binding.js';
3
+ import { resolveServiceConfigSchema } from './providers.js';
4
+ import { domainOfService, moduleDomainWritable, providerFootprint, resolveDomainBinding } from '../integrations/domain-binding.js';
5
+ import { isControlledInlineProvider } from '../integrations/predicate.js';
5
6
 
6
7
  /**
7
- * 层级 config schema 计算视图(v4.4 D32——静态轴富化,零 contracts 求值)。
8
- * config 表单结构三形态同源(P10):槽级组合单体(providers.ts)/ 层级内嵌(本文件——
9
- * /app、/modules/:m include)/ 供给轴批量(providers.ts include——切换态)。
8
+ * 层级/服务级 config schema 计算视图(v4.4 D32——静态轴富化,零 contracts 求值;v4.7 D38 服务级)。
9
+ * config 表单结构四形态同源(P10):槽级组合单体(providers.ts)/ 层级内嵌(本文件——
10
+ * /app、/modules/:m include)/ 供给轴批量(providers.ts include——切换态)/ 服务级生效绑定
11
+ * (本文件 loadServiceIntegrationSchemas——/services/:s include)。
10
12
  *
11
13
  * 计算规则(D32 裁决):禁交集——config 五层浅合并下「部分服务认识的字段」合法(谓词 #7
12
14
  * 并集键集同依据);禁合成单一 schema——required/title 冲突的有损代数;产物 = 扁平映射,
13
15
  * 分组归客户端纯呈现。层级 schema = 表单引导**非写入门禁**(P16——写路径校验权威恒在谓词
14
16
  * #7 键集 + ajv 凭据链;root 级口径差:#7 = 引用厂商并集,表单 = 层生效厂商单厂商作用域)。
17
+ *
18
+ * v4.6(D36/D37 additive 精化):**原始投影**(loadApp/loadModuleIntegrationSchemas)原样保留
19
+ * 为诊断面 / registry 输入;**消费投影** = buildIntegrationFieldRegistry(字段级合并代数
20
+ * R1/R2/R3 服务端单源,conflict 折叠 schema:null);**轴向假想绑定** = formProviderOf
21
+ * (override ?? 实际解析——双投影同源同口径,formViews 装配层一次计算两投影)。
22
+ * 单源三锚:层级 schema 计算 = resolveLayerIntegrationSchemas / 生效厂商解析 = formProviderOf /
23
+ * 合并代数 = buildIntegrationFieldRegistry。
15
24
  */
16
25
 
17
- /** 核心纯函数:显式作用域(provider null → 整字段 null;services 逐项 serviceSchemas 复合键 +
18
- * readSchemaRef——既有解析路径零新读;供给在场未声明 = null,mock 合法) */
26
+ /** 核心纯函数:显式作用域(provider null → 整字段 null;services 逐项 resolveServiceConfigSchema
27
+ * 复合解析——v4.7 D38 收口,零新读取路径;供给在场未声明 = null,mock 合法) */
28
+ export function resolveLayerIntegrationSchemas(snapshot: ViewSnapshot, provider: string, services: readonly string[]): LayerIntegrationSchemas;
29
+ export function resolveLayerIntegrationSchemas(snapshot: ViewSnapshot, provider: string | null, services: readonly string[]): LayerIntegrationSchemas | null;
19
30
  export function resolveLayerIntegrationSchemas(
20
31
  snapshot: ViewSnapshot,
21
32
  provider: string | null,
@@ -24,30 +35,192 @@ export function resolveLayerIntegrationSchemas(
24
35
  if (provider === null) return null;
25
36
  const out: LayerIntegrationSchemas['services'] = {};
26
37
  for (const s of services) {
27
- const entry = snapshot.walk.serviceSchemas[`${provider}\u0000${s}`];
28
- out[s] = { configSchema: entry ? readSchemaRef(snapshot.appDir, entry.packageDir, entry.configSchema) : null };
38
+ out[s] = { configSchema: resolveServiceConfigSchema(snapshot, provider, s) };
29
39
  }
30
40
  return { provider, services: out };
31
41
  }
32
42
 
33
- /** app 级 loader:生效厂商 = root.provider(非空字符串,缺席 → 整字段 null——D32「未绑定」);
34
- * 作用域 = 该厂商供给足迹全量(≠ AppView.services——root.config 流向包括模块服务在内的所有服务) */
35
- export function loadAppIntegrationSchemas(snapshot: ViewSnapshot): LayerIntegrationSchemas | null {
36
- const p = (snapshot.integrations as { provider?: unknown } | null)?.provider;
37
- const provider = typeof p === 'string' && p.length > 0 ? p : null;
38
- if (provider === null) return null;
39
- return resolveLayerIntegrationSchemas(snapshot, provider, providerFootprint(snapshot.catalog, provider));
43
+ // ── v4.6 D36/D37:合并代数助手(内联——深相等判定基)──
44
+
45
+ /** 稳定序列化(键序归一——R2 深相等 / R1 签名比较统一基;数组保序、对象键 sort) */
46
+ function stableSerialize(v: unknown): string {
47
+ if (v === null || typeof v !== 'object') return JSON.stringify(v ?? null) as string;
48
+ if (Array.isArray(v)) return `[${v.map(stableSerialize).join(',')}]`;
49
+ const keys = Object.keys(v as Record<string, unknown>).sort();
50
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableSerialize((v as Record<string, unknown>)[k])}`).join(',')}}`;
40
51
  }
41
52
 
42
- /** module loader:仅 writable 单域;生效厂商 = resolveDomainBinding(D32/R4 单源);
43
- * 作用域 = demand ∩ 该厂商供给;多域/零域/共享键/未绑定 → 整字段 null(表单降级 = ExtraFields) */
44
- export function loadModuleIntegrationSchemas(snapshot: ViewSnapshot, moduleId: string): LayerIntegrationSchemas | null {
53
+ /** enum 缺席哨兵(≠ 任何实际集签名——混布必不等) */
54
+ const NO_ENUM = '\u0000no-enum';
55
+
56
+ /** enum 集签名(去重排序——序不敏感) */
57
+ function enumSig(sub: Record<string, unknown>): string {
58
+ const e = sub.enum;
59
+ if (!Array.isArray(e)) return NO_ENUM;
60
+ return `[${[...new Set(e.map(stableSerialize))].sort().join(',')}]`;
61
+ }
62
+
63
+ /** 顶层 type 签名(数组形态序敏感——严格全等);非对象(含布尔子 schema)/ type 缺席 → null(R3) */
64
+ function typeSig(sub: unknown): string | null {
65
+ if (!sub || typeof sub !== 'object' || Array.isArray(sub)) return null;
66
+ const t = (sub as Record<string, unknown>).type;
67
+ return t === undefined ? null : stableSerialize(t);
68
+ }
69
+
70
+ /** D36 合并代数单点(R1/R2/R3——唯一扩展点;保守降级:不可调和 → null,客户端 RawJson 保真) */
71
+ function mergeFieldSources(sources: ReadonlyArray<{ subschema: unknown }>): object | null {
72
+ const sigs = sources.map((s) => typeSig(s.subschema));
73
+ if (sigs.some((s) => s === null) || new Set(sigs).size > 1) return null; // R3:非对象/type 缺席/type 不等
74
+ if (sigs[0] === '"object"') {
75
+ // R2:全源稳定序列化深相等 → 首源;异构 → null
76
+ const first = stableSerialize(sources[0].subschema);
77
+ return sources.every((s) => stableSerialize(s.subschema) === first) ? (sources[0].subschema as object) : null;
78
+ }
79
+ // R1:标量型——enum 集全等(含双无哨兵)→ 首源;不等/混布 → null
80
+ const es = new Set(sources.map((s) => enumSig(s.subschema as Record<string, unknown>)));
81
+ return es.size === 1 ? (sources[0].subschema as object) : null;
82
+ }
83
+
84
+ /** D36 registry 构造(纯函数——消费 resolveLayerIntegrationSchemas 产物,禁旁路直取 serviceSchemas)。
85
+ * fields 收集零过滤(布尔/非对象子 schema 入列,判定全权归 mergeFieldSources——S8 互证等式由
86
+ * 收集定义结构性闭合);schemaless 仅 configSchema === null(布尔/畸形整 schema 防御跳过——
87
+ * 不入 schemaless、零参与聚合;D32 运行时畸形,非契约面)。 */
88
+ export function buildIntegrationFieldRegistry(
89
+ layer: LayerIntegrationSchemas,
90
+ o?: { unsupplied?: string[] },
91
+ ): IntegrationFieldRegistryView {
92
+ const schemaless: string[] = [];
93
+ let additionalPropertiesAllowed = true;
94
+ const buckets = new Map<string, Array<{ service: string; subschema: unknown; required: boolean }>>();
95
+ for (const [service, entry] of Object.entries(layer.services)) {
96
+ // 插入序 = scope 遍历序(app 字母序 / module demand 序)——fields 首声明序由此保证
97
+ const schema = entry.configSchema;
98
+ if (schema === null) {
99
+ schemaless.push(service);
100
+ continue;
101
+ }
102
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) continue;
103
+ const s = schema as Record<string, unknown>;
104
+ if (s.additionalProperties === false) additionalPropertiesAllowed = false;
105
+ const props = s.properties;
106
+ if (!props || typeof props !== 'object' || Array.isArray(props)) continue; // ≡ 谓词 collectPropertyKeys 守卫
107
+ const required = Array.isArray(s.required) ? s.required : [];
108
+ for (const key of Object.keys(props)) {
109
+ const bucket = buckets.get(key) ?? [];
110
+ bucket.push({ service, subschema: (props as Record<string, unknown>)[key], required: required.includes(key) });
111
+ buckets.set(key, bucket);
112
+ }
113
+ }
114
+ const fields: IntegrationFieldRegistryView['fields'] = [];
115
+ for (const [key, sources] of buckets) {
116
+ fields.push({
117
+ key,
118
+ schema: mergeFieldSources(sources),
119
+ sources: sources.map(({ service, required }) => ({ service, required })),
120
+ });
121
+ }
122
+ return {
123
+ provider: layer.provider,
124
+ schemaless,
125
+ unsupplied: [...(o?.unsupplied ?? [])],
126
+ additionalPropertiesAllowed,
127
+ fields,
128
+ };
129
+ }
130
+
131
+ /** D37 生效厂商解析单源(实际/假想同径):override(非空串)?? 实际解析。
132
+ * 空串归一(I3)单点;module 轴 D34 不可写/不在册 → null(override 不豁免——预览服务于保存)。 */
133
+ export function formProviderOf(
134
+ axis: 'app' | 'module',
135
+ snapshot: ViewSnapshot,
136
+ moduleId?: string,
137
+ override?: string,
138
+ ): string | null {
139
+ const ovr = typeof override === 'string' && override.length > 0 ? override : undefined;
140
+ if (axis === 'app') {
141
+ if (ovr !== undefined) return ovr;
142
+ const p = (snapshot.integrations as { provider?: unknown } | null)?.provider;
143
+ return typeof p === 'string' && p.length > 0 ? p : null;
144
+ }
45
145
  const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
46
146
  if (!mod) return null; // 视图已先行 404,此处防御
47
147
  const w = moduleDomainWritable(snapshot.walk, mod);
48
- if (!w.writable) return null;
49
- const provider = resolveDomainBinding(snapshot.integrations, w.domainKey).provider;
50
- if (provider === null) return null;
148
+ if (!w.writable) return null; // D34 不豁免
149
+ if (ovr !== undefined) return ovr;
150
+ return resolveDomainBinding(snapshot.integrations, w.domainKey).provider;
151
+ }
152
+
153
+ /** D36/D37 表单层双投影(I1:同源同口径一次计算——双取单算 by construction) */
154
+ export interface LayerFormViews {
155
+ integrationSchemas: LayerIntegrationSchemas | null;
156
+ integrationFieldRegistry: IntegrationFieldRegistryView | null;
157
+ }
158
+
159
+ /** 防御双 null(module 不在册/不可写/未绑定、app 未绑定——F3) */
160
+ const NULL_FORM_VIEWS: LayerFormViews = { integrationSchemas: null, integrationFieldRegistry: null };
161
+
162
+ /** app 轴装配:scope = 供给足迹全量(≠ AppView.services——root.config 流向包括模块服务在内的所有服务);
163
+ * unsupplied 恒 [](结构性:足迹 ⊆ 供给)。未知厂商/零足迹 → 非 null 空集(镜像 D32 mock 语义)。 */
164
+ export function loadAppFormViews(snapshot: ViewSnapshot, appProvider?: string): LayerFormViews {
165
+ const provider = formProviderOf('app', snapshot, undefined, appProvider);
166
+ if (provider === null) return NULL_FORM_VIEWS;
167
+ const layer = resolveLayerIntegrationSchemas(snapshot, provider, providerFootprint(snapshot.catalog, provider));
168
+ return { integrationSchemas: layer, integrationFieldRegistry: buildIntegrationFieldRegistry(layer) };
169
+ }
170
+
171
+ /** module 轴装配:仅 writable 单域(D34);scope = demand ∩ 该厂商供给;unsupplied 同点派生
172
+ * (demand − 供给,Set 去重保序——manifest demand 不去重防御)。 */
173
+ export function loadModuleFormViews(snapshot: ViewSnapshot, moduleId: string, moduleProvider?: string): LayerFormViews {
174
+ const provider = formProviderOf('module', snapshot, moduleId, moduleProvider);
175
+ const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
176
+ if (provider === null || !mod) return NULL_FORM_VIEWS;
51
177
  const demand = (mod.descriptor.contributes.services ?? []).map((s) => s.service);
52
- return resolveLayerIntegrationSchemas(snapshot, provider, demand.filter((s) => isSupplied(snapshot.catalog, provider, s)));
178
+ const supplied = (s: string): boolean => isSupplied(snapshot.catalog, provider, s);
179
+ const layer = resolveLayerIntegrationSchemas(snapshot, provider, demand.filter(supplied));
180
+ return {
181
+ integrationSchemas: layer,
182
+ integrationFieldRegistry: buildIntegrationFieldRegistry(layer, {
183
+ unsupplied: [...new Set(demand.filter((s) => !supplied(s)))],
184
+ }),
185
+ };
186
+ }
187
+
188
+ /** app 级 loader(v4.6 委托化——签名零改动,公开面保持;假想态归 loadAppFormViews override 参) */
189
+ export function loadAppIntegrationSchemas(snapshot: ViewSnapshot): LayerIntegrationSchemas | null {
190
+ return loadAppFormViews(snapshot).integrationSchemas;
191
+ }
192
+
193
+ /** module 级 loader(v4.6 委托化——同上;多域/零域/共享键/未绑定 → 整字段 null,表单降级 = ExtraFields) */
194
+ export function loadModuleIntegrationSchemas(snapshot: ViewSnapshot, moduleId: string): LayerIntegrationSchemas | null {
195
+ return loadModuleFormViews(snapshot, moduleId).integrationSchemas;
196
+ }
197
+
198
+ /** 服务级 loader(v4.7 D38):四分支穷举(node.provider = 文件 services[s].provider 逐字原值;
199
+ * 实例层无条件排除——node 级口径);非空三分支归约一次复合解析。
200
+ * ① undefined/'' → resolveDomainBinding 域级联(域 + root 双缺 → 整字段 null)
201
+ * ② 非空 string → bare:catalog miss(未知厂商/未供给)→ configSchema:null 不 404(富化非门控)
202
+ * ③ 受控内联(isControlledInlineProvider 单源)→ 名 = p.provider;catalog 复合键 > p.configSchema ref
203
+ * (H9 逃逸 400 照抛——readInlineSchema 共享路径)
204
+ * ④ 其余(null/数组/原始值/坏对象)→ 整字段 null(≡ 谓词 #5 INLINE_PROVIDER_INVALID 同判——
205
+ * 不放行域级联,防错误 schema 污染坏节点) */
206
+ export function loadServiceIntegrationSchemas(
207
+ snapshot: ViewSnapshot,
208
+ service: string,
209
+ ): ServiceIntegrationSchemas | null {
210
+ const p = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
211
+ ?.services?.[service]?.provider;
212
+ if (p === undefined || p === '') {
213
+ const binding = resolveDomainBinding(snapshot.integrations, domainOfService(service));
214
+ return binding.provider === null
215
+ ? null
216
+ : { provider: binding.provider, configSchema: resolveServiceConfigSchema(snapshot, binding.provider, service) };
217
+ }
218
+ if (typeof p === 'string') {
219
+ return { provider: p, configSchema: resolveServiceConfigSchema(snapshot, p, service) };
220
+ }
221
+ if (isControlledInlineProvider(p)) {
222
+ const decl = p as { provider: string; configSchema?: string };
223
+ return { provider: decl.provider, configSchema: resolveServiceConfigSchema(snapshot, decl.provider, service, decl.configSchema) };
224
+ }
225
+ return null;
53
226
  }
@@ -1,7 +1,7 @@
1
1
  import type { ModuleDeclaration, ModuleDetailView, ProviderBinding, ServiceDemand } from '../dto.js';
2
2
  import { AppToolkitError } from '../core/errors.js';
3
3
  import { moduleDomainWritable, resolveDomainBinding } from '../integrations/domain-binding.js';
4
- import type { ViewSnapshot } from './context.js';
4
+ import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
5
5
 
6
6
  /**
7
7
  * 需求轴/模块静态投影(views/modules;C4)。静态端点结构性零求值(铁律 #3——
@@ -9,18 +9,34 @@ import type { ViewSnapshot } from './context.js';
9
9
  * v4.4 D34:domainKeys/providerBindings/configEditable 显式化;integration 仅单域
10
10
  * (writable)时在场——多域/零域/共享键无单键落点;多域回退兜底键机器拆除
11
11
  * (派生统一 = domainOfService 去重集合,moduleDomainWritable 单源)。
12
+ * 2026-09-16 双缺陷批:loadModulesView 排除 provider 家族包(供给/实施载体非可配置
13
+ * 业务模块)——walk 声明集与 loadModuleDetailView 不变(装包仍合法,详情保持 200)。
12
14
  */
13
15
 
14
- /** GET /modules:纯模块清单(demand 不在——卡片展开 = GET /modules/:m) */
16
+ /** 家族名归一(cli deriveModuleId 同源语义——依赖方向禁 import,故内联):
17
+ * `@tbox.cn/app-provider-x` / `@app/provider-y` / `provider-z` → `provider-*` */
18
+ function familyName(s: string): string {
19
+ return s.replace(/^@tbox\.cn\/app-/, '').replace(/^@[^/]+\//, '');
20
+ }
21
+
22
+ /** provider 家族包判定(模块清单排除供给/实施载体;生态三先例同源:stage 排除判定 /
23
+ * guard-vendor-neutrality「provider-* 包」/ cli deriveModuleId 家族推导)。install id 与
24
+ * 包名 basename 双查 = 防 .tbox/app.json 登记漂移纵深;命名而非结构(provider-custom 零贡献
25
+ * 空壳与「零贡献合法」形态不可结构区分);未来收敛 = manifest 显式角色标记(届时整体退场)。 */
26
+ function isProviderFamily(mod: { id: string; pkg: string }): boolean {
27
+ return familyName(mod.id).startsWith('provider-') || familyName(mod.pkg).startsWith('provider-');
28
+ }
29
+
30
+ /** GET /modules:纯模块清单(demand 不在——卡片展开 = GET /modules/:m;provider 家族包不在清单) */
15
31
  export function loadModulesView(snapshot: ViewSnapshot): { modules: ModuleDeclaration[] } {
16
32
  const modules: ModuleDeclaration[] = snapshot.walk.modules
33
+ .filter((mod) => !isProviderFamily(mod))
17
34
  .map((mod) => ({
18
35
  id: mod.id,
19
36
  name: mod.descriptor.name,
20
- // 模块级 title/description:manifest 无模块级 meta 字段(demand meta 属 per-service)——
21
- // 一期回退 install id / 空描述(UI 侧按词汇表或 demand meta 增强)
22
- title: mod.descriptor.name,
23
- description: '',
37
+ // 模块 meta(B1 additive):manifest title/description——兜底 name / ''(视图层灌值)
38
+ title: mod.descriptor.title ?? mod.descriptor.name,
39
+ description: mod.descriptor.description ?? '',
24
40
  }))
25
41
  .sort((a, b) => a.id.localeCompare(b.id));
26
42
  return { modules };
@@ -32,12 +48,16 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
32
48
  if (!mod) {
33
49
  throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
34
50
  }
35
- const services: ServiceDemand[] = (mod.descriptor.contributes.services ?? []).map((need) => ({
36
- service: need.service,
37
- ...(need.title ? { title: need.title } : {}),
38
- ...(need.description ? { description: need.description } : {}),
39
- required: need.optional === false,
40
- }));
51
+ const services: ServiceDemand[] = (mod.descriptor.contributes.services ?? []).map((need) => {
52
+ // 展示名单源走链(B1):词汇 meta > demand meta > id——词汇主源防 manifest demand 漂移
53
+ const display = resolveServiceDisplay(snapshot.walk, need.service);
54
+ return {
55
+ service: need.service,
56
+ title: display.title,
57
+ ...(display.description !== undefined ? { description: display.description } : {}),
58
+ required: need.optional === false,
59
+ };
60
+ });
41
61
  const w = moduleDomainWritable(snapshot.walk, mod);
42
62
  const domains = (snapshot.integrations as { domains?: Record<string, unknown> } | null)?.domains ?? {};
43
63
  // integration = domains[换算键] 节点原值(D21 编辑预填源);未配置 = null;多域/零域/共享键 = null(D34)
@@ -46,5 +66,14 @@ export function loadModuleDetailView(snapshot: ViewSnapshot, moduleId: string):
46
66
  const binding = resolveDomainBinding(snapshot.integrations, d);
47
67
  return { domain: d, effective: binding.provider, layer: binding.layer };
48
68
  });
49
- return { domainKeys: [...w.domainKeys], integration, providerBindings, configEditable: w.writable, services };
69
+ return {
70
+ // 模块 meta(B1 additive)——兜底与 /modules 同语义
71
+ title: mod.descriptor.title ?? mod.descriptor.name,
72
+ description: mod.descriptor.description ?? '',
73
+ domainKeys: [...w.domainKeys],
74
+ integration,
75
+ providerBindings,
76
+ configEditable: w.writable,
77
+ services,
78
+ };
50
79
  }
@@ -2,7 +2,7 @@ import type { ProviderDetail, ProviderEntry, ProviderServiceDetail, ProvidersVie
2
2
  import { AppToolkitError } from '../core/errors.js';
3
3
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
4
4
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
5
- import type { ViewSnapshot } from './context.js';
5
+ import { resolveServiceDisplay, type ViewSnapshot } from './context.js';
6
6
 
7
7
  /**
8
8
  * 供给轴投影(views/providers;C4)。供给关系三切片(P5):
@@ -21,6 +21,9 @@ export function loadProvidersView(snapshot: ViewSnapshot): ProvidersView {
21
21
  provider,
22
22
  local: entries.every((e) => e.local === true),
23
23
  owner: entries[0].owner,
24
+ // 厂商展示元数据(B1 walk 层传播——首条目 = 首声明,与 owner 同语义)
25
+ ...(entries[0].title ? { title: entries[0].title } : {}),
26
+ ...(entries[0].description ? { description: entries[0].description } : {}),
24
27
  });
25
28
  }
26
29
  return { providers: providers.sort((a, b) => a.provider.localeCompare(b.provider)) };
@@ -40,12 +43,15 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
40
43
  bucket.implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
41
44
  }
42
45
  }
46
+ // 服务展示名走链(B1):词汇 meta > demand meta > id(title 恒发)
47
+ for (const [service, bucket] of Object.entries(services)) {
48
+ const display = resolveServiceDisplay(snapshot.walk, service);
49
+ bucket.title = display.title;
50
+ if (display.description !== undefined) bucket.description = display.description;
51
+ }
43
52
  if (include?.has('integrationSchemas')) {
44
53
  for (const [service, bucket] of Object.entries(services)) {
45
- const schemaEntry = snapshot.walk.serviceSchemas[`${provider}\u0000${service}`];
46
- bucket.integrationSchemas = {
47
- configSchema: schemaEntry ? readSchemaRef(snapshot.appDir, schemaEntry.packageDir, schemaEntry.configSchema) : null,
48
- };
54
+ bucket.integrationSchemas = { configSchema: resolveServiceConfigSchema(snapshot, provider, service) };
49
55
  }
50
56
  }
51
57
  const entries = Object.values(impls);
@@ -53,6 +59,9 @@ export function loadProviderDetailView(snapshot: ViewSnapshot, provider: string,
53
59
  provider,
54
60
  local: entries.every((e) => e.local === true),
55
61
  owner: entries[0].owner,
62
+ // 厂商展示元数据(B1 walk 层传播——首条目透传)
63
+ ...(entries[0].title ? { title: entries[0].title } : {}),
64
+ ...(entries[0].description ? { description: entries[0].description } : {}),
56
65
  services,
57
66
  };
58
67
  }
@@ -95,22 +104,15 @@ export function loadProviderServiceDetailView(
95
104
  implementations.push({ implementation: configured[service]?.implementation ?? `${provider}-inline@1`, credentialType: inlineDeclaration.credentialType });
96
105
  }
97
106
 
98
- // integrationSchemas:catalog schema(复合键)优先;内联声明 configSchema 引用应用 config/schemas/
99
- // (路径约束 H9——resolve 落 config/schemas/ + realpath 复核,违例 400)
100
- // v4.4 F5 消重:schema 表取自 snapshot.walk(原 walkManifests(appDir) 重复全量目录扫描);
101
- // v4.4 D33:credentialSchema 删净(结构归 GET /credential-types——组合单体纯 config)
102
- let configSchema: object | null = null;
103
- const schemaEntry = snapshot.walk.serviceSchemas[`${provider}\u0000${service}`];
104
- if (schemaEntry) {
105
- configSchema = readSchemaRef(snapshot.appDir, schemaEntry.packageDir, schemaEntry.configSchema);
106
- } else if (inlineDeclaration) {
107
- // 内联声明 schema 解析:引用文件在 integrations 节点(provider.configSchema)——
108
- // 形状同 config/schemas/ 约束(路径来源 = 节点声明,由服务端读应用侧)
109
- const inlineSchemaRef = (inline as { configSchema?: string } | undefined)?.configSchema;
110
- if (typeof inlineSchemaRef === 'string') {
111
- configSchema = readInlineSchema(snapshot.appDir, inlineSchemaRef);
112
- }
113
- }
107
+ // integrationSchemas:schema 解析收口 resolveServiceConfigSchema 单源(v4.7 D38——catalog 复合键 >
108
+ // 内联 ref,等价重构 O6/O7/O15 零改动即证);内联 ref 来源 = 节点声明(provider.configSchema,
109
+ // 仅受控内联声明匹配本厂商名时);路径约束 H9——resolve config/schemas/ + realpath 复核,违例 400
110
+ const configSchema = resolveServiceConfigSchema(
111
+ snapshot,
112
+ provider,
113
+ service,
114
+ inlineDeclaration ? (inline as { configSchema?: string } | undefined)?.configSchema : undefined,
115
+ );
114
116
 
115
117
  return {
116
118
  provider,
@@ -120,6 +122,22 @@ export function loadProviderServiceDetailView(
120
122
  };
121
123
  }
122
124
 
125
+ /** per-(provider, service) schema 复合解析单源(v4.7 D38):catalog 复合键(多 impl 声明序首个)
126
+ * > 内联 ref(readInlineSchema 保持私有——H9 双钉子不扩暴露);miss / 未声明 / 文件缺失 → null。
127
+ * views 层 \u0000 复合键唯一消费点(组合单体 / 供给轴批量 / 层级核心 / 服务级 include 同源)。
128
+ * (D36 registry 批若需跨包消费再升 index 导出——additive 零成本) */
129
+ export function resolveServiceConfigSchema(
130
+ snapshot: ViewSnapshot,
131
+ provider: string,
132
+ service: string,
133
+ inlineRef?: string,
134
+ ): object | null {
135
+ const entry = snapshot.walk.serviceSchemas[`${provider}\u0000${service}`];
136
+ if (entry) return readSchemaRef(snapshot.appDir, entry.packageDir, entry.configSchema);
137
+ if (typeof inlineRef === 'string') return readInlineSchema(snapshot.appDir, inlineRef);
138
+ return null;
139
+ }
140
+
123
141
  /** catalog schema 引用读取(相对包根;缺席 → null——SCHEMA_FILE_MISSING 归谓词 #6)。
124
142
  * v4.4 导出共享:层级内嵌(integration-schemas.ts)/ 凭据结构清单(credential-types.ts)同源消费。 */
125
143
  export function readSchemaRef(_appDir: string, packageDir: string, ref: string | undefined): object | null {
@@ -1,7 +1,7 @@
1
1
  import type { ServiceDetail } from '../dto.js';
2
2
  import { AppToolkitError } from '../core/errors.js';
3
3
  import { isKnownService } from '../integrations/read.js';
4
- import { suppliedByVendors, type ViewSnapshot } from './context.js';
4
+ import { resolveServiceDisplay, suppliedByVendors, type ViewSnapshot } from './context.js';
5
5
 
6
6
  /**
7
7
  * 服务静态单体投影(views/service-detail;C4)。求值数据不在此(→ /service-resolutions/:service)。
@@ -17,10 +17,12 @@ export function loadServiceDetailView(snapshot: ViewSnapshot, service: string):
17
17
  }
18
18
  // integration = 文件 services[s] 逐字原值(编辑预填源 D21);无声明 = null
19
19
  const integration = (configured[service] as ServiceDetail['integration'] | undefined) ?? null;
20
+ // 展示名单源走链(B1):词汇 meta > demand meta > id——替换 demand 直拼(恒有值)
21
+ const display = resolveServiceDisplay(snapshot.walk, service);
20
22
  return {
21
23
  service,
22
- ...(demand?.title ? { title: demand.title } : { title: service }),
23
- ...(demand?.description ? { description: demand.description } : {}),
24
+ title: display.title,
25
+ ...(display.description !== undefined ? { description: display.description } : {}),
24
26
  required: demand ? !demand.optional : false,
25
27
  // defaults 唯一落位(manifest 预填)
26
28
  defaults: demand ? demand.defaults.map((d) => ({ ...d })) : [],