@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.
@@ -1,21 +1,23 @@
1
1
  /**
2
2
  * 配置 JSON 环境变量占位展开(纯数据形态——自 app-sdk env-expansion.ts 迁入改造)。
3
3
  *
4
- * 语法(与 SDK 版语义逐字一致,共享向量 test-fixtures/env-expansion-cases.json 防漂移):
5
- * ${NAME} 占位引用;NAME 限定 [A-Z][A-Z0-9_]*(UPPER_SNAKE,最小化与正文 `${...}` 误碰)
6
- * $${NAME} 转义为字面量 ${NAME}(原样保留一次 $)
4
+ * 语法(语义真源 = 本文件 + 共享向量 test-fixtures/env-expansion-cases.json 防漂移):
5
+ * ${NAME} 占位引用;NAME 限定 [A-Z][A-Z0-9_]*(UPPER_SNAKE,最小化与正文 `${...}` 误碰)
6
+ * ${NAME:def} POSIX 风格缺省值:NAME 未设置时用 def(字面量,不二次展开);设置时空串也算「已设置」
7
+ * $${NAME} 转义为字面量 ${NAME}(原样保留一次 $)
7
8
  *
8
9
  * 语义(评审 N1 钉死,壳层各自包装 throw/report):
9
10
  * - 单遍非递归:env 值内若含 `${OTHER}` 不被二次展开(注入防线);String.replace 回调形态天然单遍。
11
+ * 默认值内含 `${...}` 同样不展开(POSIX 同限);默认值含 `}` 不支持(同 POSIX 同限——正则 [^}] 截断)。
10
12
  * - 值经 JSON 字符串转义注入;不做数值/布尔强制转换。
11
- * - **本版为纯数据返回形态** `{ text?: string; missing: string[] }`——缺变量不 throw,
13
+ * - **本版为纯数据返回形态** `{ text?: string; missing: string[] }`——缺变量(无缺省值时)不 throw,
12
14
  * 由壳层(CLI doctor 壳 / app-sdk 桥 throw 壳 / 沙箱宿主)决定 fail-fast 还是降级:
13
15
  * SDK 桥壳 missing 非空即 throw(错误文案逐字等价 SDK 旧版);CLI 壳同款。
14
16
  * - 无 ${ 的文本快速路径:text 原文返回,零替换零开销。
15
17
  */
16
18
 
17
- /** 占位匹配:$ 或 $$ 后接 {[A-Z][A-Z0-9_]*}($$ 形态同匹配,展开期判首字符去留) */
18
- const ENV_VAR_RE = /\$\$?\{([A-Z][A-Z0-9_]*)\}/g;
19
+ /** 占位匹配:$ 或 $$ 后接 {NAME} 或 {NAME:default}($$ 形态同匹配,展开期判首字符去留) */
20
+ const ENV_VAR_RE = /\$\$?\{([A-Z][A-Z0-9_]*)(?::-([^}]*))?\}/g;
19
21
 
20
22
  /** JSON 字符串字面量转义(值 → 可直接置于 "..." 内的安全文本)——
21
23
  * 基于 JSON.stringify 切片:引号/反斜杠/控制字符(\b/\n/\r/\t/\f)转义语义与 JSON 精确一致 */
@@ -37,14 +39,14 @@ export function expandEnvVars(
37
39
  ): EnvExpansionResult {
38
40
  const missing: string[] = [];
39
41
  // 单遍替换:回调返回值不回扫(非递归语义;$${} 转义在回调内消去一个 $)
40
- const out = text.replace(ENV_VAR_RE, (raw, name: string) => {
42
+ const out = text.replace(ENV_VAR_RE, (raw, name: string, fallback?: string) => {
41
43
  if (raw.startsWith('$$')) return raw.slice(1);
42
44
  const v = env[name];
43
- if (v === undefined) {
44
- missing.push(name);
45
- return raw; // 占位保留(纯数据形态:missing 上报,不 throw)
46
- }
47
- return jsonEscape(v);
45
+ if (v !== undefined) return jsonEscape(v);
46
+ // ${NAME:def}:NAME 未设置 → 缺省值(字面量;空串默认 = 显式空值语义)
47
+ if (fallback !== undefined) return jsonEscape(fallback);
48
+ missing.push(name);
49
+ return raw; // 占位保留(纯数据形态:missing 上报,不 throw)
48
50
  });
49
51
  if (missing.length > 0) return { missing };
50
52
  return { text: out, missing: [] };
@@ -78,6 +78,9 @@ export const moduleDescriptorSchema = z
78
78
  .object({
79
79
  schemaVersion: z.number().default(1),
80
80
  name: z.string(),
81
+ // 模块级 meta additive(B1):展示名/描述——回退归视图层(title ?? name / ''),schema 不灌值
82
+ title: z.string().optional(),
83
+ description: z.string().optional(),
81
84
  version: z.string().default('0.0.0'),
82
85
  kind: z.enum(['platform', 'business', 'third-party']).default('business'),
83
86
  risk: z
package/src/dto.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DTO 全量(api.md §3 字段级单源落源——28 类型;演进 additive-only,铁律 #8)。
2
+ * DTO 全量(api.md §3 字段级单源落源——36 类型;演进 additive-only,铁律 #8)。
3
3
  * agtcodingbox / tbox-ai-coding 经 `import type` 消费(L2 DTO 单源——API 层零类型镜像)。
4
4
  * 词汇七概念:app / module / service / instance / provider / integration / resolution。
5
5
  * 弃用词汇清单见 api.md §3 尾(component/slot/scope/configMode/…——防复活)。
@@ -20,13 +20,15 @@ export type BindingLayer = 'app' | 'module' | 'service' | 'instance';
20
20
 
21
21
  // ===== 需求轴(GET /modules · 静态)=====
22
22
 
23
- /** 纯模块清单(无 services——卡片展开走 /modules/:m */
23
+ /** 纯模块清单(无 services——卡片展开走 /modules/:m);provider 家族包不在清单
24
+ * (供给/实施载体非可配置业务模块——供给轴归 /providers;判定 = install id / 包名
25
+ * basename 去 scope 后 `provider-` 前缀;已装 provider 详情 GET /modules/:m 仍 200) */
24
26
  export interface ModulesView {
25
27
  modules: ModuleDeclaration[];
26
28
  }
27
29
 
28
30
  export interface ModuleDeclaration {
29
- /** install id(packages/ 目录名 / .tbox/app.json entry.id */
31
+ /** install id(packages/ 目录名 / .tbox/app.json entry.id)——恒业务模块(provider 家族不在本清单) */
30
32
  id: string;
31
33
  /** 包名 */
32
34
  name: string;
@@ -48,6 +50,10 @@ export interface ServiceDemand {
48
50
 
49
51
  /** GET /modules/:module(模块静态一站式 · PUT 读对偶) */
50
52
  export interface ModuleDetailView {
53
+ /** B1 additive:模块 meta 展示名(manifest title;视图层兜底 name——恒有值) */
54
+ title: string;
55
+ /** B1 additive:模块 meta 描述(manifest description;兜底 '') */
56
+ description: string;
51
57
  /** v4.4 D34:demand 服务首段去重(排序;1..N;零域 = []) */
52
58
  domainKeys: string[];
53
59
  /** domains[换算键] 节点原值;未配置 = null;v4.4 D34:仅单域(writable)时在场——多域/零域/共享键无单键落点 */
@@ -62,6 +68,9 @@ export interface ModuleDetailView {
62
68
  serviceResolutions?: ServiceResolutionsView | null;
63
69
  /** v4.4 D32:include=integrationSchemas 层级富化(多域/零域/共享键/未绑定 → null) */
64
70
  integrationSchemas?: LayerIntegrationSchemas | null;
71
+ /** v4.6 D36:include=integrationFieldRegistry 集成字段注册表(null 语义镜像 integrationSchemas;
72
+ * 未知厂商/零足迹 → 非 null 空集 + unsupplied 回填——D37 轴参假想计算同此) */
73
+ integrationFieldRegistry?: IntegrationFieldRegistryView | null;
65
74
  }
66
75
 
67
76
  /** 模块域厂商绑定解析(v4.4 D34——静态文件事实;规则 ≡ 谓词 #7 域级,resolveDomainBinding 单源) */
@@ -82,6 +91,27 @@ export interface LayerIntegrationSchemas {
82
91
  services: Record<string, { configSchema: object | null }>;
83
92
  }
84
93
 
94
+ /** v4.6 D36:集成字段注册表(层级表单服务端预计算——合并代数 R1/R2/R3 服务端单源;
95
+ * 消费 resolveLayerIntegrationSchemas 产物构造,禁旁路直取 serviceSchemas) */
96
+ export interface IntegrationFieldRegistryView {
97
+ /** echo:本注册表计算口径(假想时 = 轴参值——D37) */
98
+ provider: string;
99
+ /** 作用域内 configSchema === null 的服务(UI「未提供参数模板」提示行) */
100
+ schemaless: string[];
101
+ /** module 轴 = demand − 该厂商供给(Set 去重保序);app 轴恒 [](结构性:足迹 ⊆ 供给) */
102
+ unsupplied: string[];
103
+ /** 作用域内全部非 null schema 的 additionalProperties 均 ≠ false(聚合;空真) */
104
+ additionalPropertiesAllowed: boolean;
105
+ /** 逐 key 合并产物(首声明插入序 = scope 遍历序:app 字母序 / module demand 序) */
106
+ fields: Array<{
107
+ key: string;
108
+ /** null = 类型不可调和(R3/R2 异构/R1 enum 不等——冲突折叠,客户端 RawJson 保真) */
109
+ schema: object | null;
110
+ /** required 聚合 = sources.some(s => s.required);conflict 字段照常聚合 */
111
+ sources: Array<{ service: string; required: boolean }>;
112
+ }>;
113
+ }
114
+
85
115
  /** GET /app(应用静态视图:平台服务 + root 原值) */
86
116
  export interface AppView {
87
117
  /** 平台服务(v4.4 D29:服务词汇 − 模块占用——与模块 demand 同型 ServiceDemand[];
@@ -93,6 +123,9 @@ export interface AppView {
93
123
  serviceResolutions?: ServiceResolutionsView | null;
94
124
  /** v4.4 D32:include=integrationSchemas 层级富化(root.provider 缺席 → null) */
95
125
  integrationSchemas?: LayerIntegrationSchemas | null;
126
+ /** v4.6 D36:include=integrationFieldRegistry 集成字段注册表(root.provider 缺席且无轴参 → null;
127
+ * 未知厂商/零足迹 → 非 null 空集——D37 轴参假想计算同此) */
128
+ integrationFieldRegistry?: IntegrationFieldRegistryView | null;
96
129
  }
97
130
 
98
131
  /** GET /services/:service(服务静态单体) */
@@ -103,6 +136,18 @@ export interface ServiceDetail extends ServiceDemand {
103
136
  integration: ServiceIntegrationNode | null;
104
137
  /** 供给该服务的厂商名单(catalog 静态切片——picker 数据源) */
105
138
  suppliedBy: string[];
139
+ /** v4.7 D38:include=integrationSchemas 生效绑定富化(三态:null 全链未绑定 /
140
+ * 绑定无可计算 schema / 含 schema;malformed 节点 → null——不放行域级联) */
141
+ integrationSchemas?: ServiceIntegrationSchemas | null;
142
+ }
143
+
144
+ /** 服务级生效绑定 schema(v4.7 D38 静态轴富化——零 contracts,永不 503;实例层无条件排除:
145
+ * node 级口径 ≠ resolution 默认实例口径,api.md §4.4 注) */
146
+ export interface ServiceIntegrationSchemas {
147
+ /** 生效厂商:显式(含内联声明名)?? domains[首段].provider ?? root.provider */
148
+ provider: string;
149
+ /** catalog 复合键(多 impl 声明序首个)> 内联 ref;miss(未知厂商/未供给/内联缺 ref)→ null(富化非门控) */
150
+ configSchema: object | null;
106
151
  }
107
152
 
108
153
  // ===== 求值轴 =====
@@ -246,6 +291,9 @@ export interface ProviderEntry {
246
291
  local: boolean;
247
292
  /** 供给声明来源包名 */
248
293
  owner: string;
294
+ /** B1 additive:厂商展示名(manifest 模块 meta——多包首声明;缺席 = undefined) */
295
+ title?: string;
296
+ description?: string;
249
297
  }
250
298
 
251
299
  /** GET /providers/:provider:供给足迹 */
@@ -253,12 +301,18 @@ export interface ProviderDetail {
253
301
  provider: string;
254
302
  local: boolean;
255
303
  owner: string;
304
+ /** B1 additive:厂商展示名(同 ProviderEntry——首声明透传) */
305
+ title?: string;
306
+ description?: string;
256
307
  /** per (provider, service) 保真,同厂商异构合法;★ implementations 恒数组(H1——
257
308
  * 多元素 = 升级窗口,UI 出 impl 选择器,PUT 显式钉版) */
258
309
  services: Record<
259
310
  string,
260
311
  {
261
312
  implementations: Array<{ implementation: string; credentialType: string }>;
313
+ /** B1 additive:服务展示名(resolveServiceDisplay——词汇 > demand > id;恒发) */
314
+ title?: string;
315
+ description?: string;
262
316
  /** v4.4 F5:include=integrationSchemas 切换态批量(纯 config——凭据结构归 /credential-types) */
263
317
  integrationSchemas?: { configSchema: object | null };
264
318
  }
@@ -359,7 +413,8 @@ export interface SaveResult {
359
413
  written: boolean;
360
414
  /** 恒 false 当 dryRun / no-diff / 应用未运行(onApplied 协作点回填) */
361
415
  restartScheduled: boolean;
362
- /** 触碰文件清单(dryRun []) */
416
+ /** 触碰清单;dryRun = 将触碰预览(D35——与同 body 真实写逐字对称,含凭据工件路径;
417
+ * no-diff 早退 = 预览零触碰) */
363
418
  files: string[];
364
419
  /** 凭据工件(0..n;dryRun = 将创建的工件——确定性 stem 派生预览) */
365
420
  credentials: Array<{ stem: string; ref: string; overwrote: boolean }>;
package/src/factory.ts CHANGED
@@ -19,7 +19,7 @@ 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';
22
+ import { loadAppFormViews, loadModuleFormViews, loadServiceIntegrationSchemas } from './views/integration-schemas.js';
23
23
  import { loadCredentialsView } from './views/credentials.js';
24
24
  import { loadCredentialTypesView } from './views/credential-types.js';
25
25
  import type { WalkManifestsResult } from './assembly/walk-manifests.js';
@@ -42,7 +42,9 @@ import type {
42
42
 
43
43
  /**
44
44
  * createAppToolkit(C4 工厂;v4.4——24 方法 = 11 读(含 credentials/credential-types 双端点)
45
- * + 4 写 + 9 迁移面;loadApp/loadModule/loadProvider 增 include 选项)。
45
+ * + 4 写 + 9 迁移面;loadApp/loadModule/loadProvider 增 include 选项;v4.6——loadApp/loadModule
46
+ * += 轴向假想绑定选项(appProvider/moduleProvider,D37);表单双投影经 formViews 单点装配;
47
+ * v4.7——loadService 增 include 选项(服务级生效绑定 schema 单体))。
46
48
  * AppToolkit = AppToolkitRead & AppToolkitWrite & AppToolkitInternals——
47
49
  * Read+Write = HTTP 直通面 additive 冻结(L1 端点↔方法 1:1,L3 命名镜像:Integration=节点级 /
48
50
  * Config=文件级);Internals = CLI/doctor/桥消费(自由演进)。
@@ -63,11 +65,14 @@ export interface AppToolkitOptions {
63
65
  onApplied?: () => void;
64
66
  }
65
67
 
66
- /** include 协议白名单(v4.4 D30/P15——键名 = 产出字段名 1:1;per 端点;注册表 additive-only) */
68
+ /** include 协议白名单(v4.4 D30/P15——键名 = 产出字段名 1:1;per 端点;注册表 additive-only
69
+ * v4.6 D36 += integrationFieldRegistry:app/module 三键,provider 轴不加——YAGNI;
70
+ * v4.7 D38 += service 键——服务级生效绑定 schema) */
67
71
  const INCLUDE_WHITELIST = {
68
- app: ['serviceResolutions', 'integrationSchemas'],
69
- module: ['serviceResolutions', 'integrationSchemas'],
72
+ app: ['serviceResolutions', 'integrationSchemas', 'integrationFieldRegistry'],
73
+ module: ['serviceResolutions', 'integrationSchemas', 'integrationFieldRegistry'],
70
74
  provider: ['integrationSchemas'],
75
+ service: ['integrationSchemas'],
71
76
  } as const;
72
77
 
73
78
  /** include 校验(P15 单点在 toolkit 方法内——壳层仅解析可重复参数,零校验纪律不变):
@@ -87,12 +92,35 @@ function validateInclude(kind: keyof typeof INCLUDE_WHITELIST, include?: string[
87
92
  return new Set(include);
88
93
  }
89
94
 
95
+ /** 表单层 include 键(D37 轴参严检依据——serviceResolutions 不算) */
96
+ const FORM_INCLUDE_KEYS = ['integrationSchemas', 'integrationFieldRegistry'] as const;
97
+
98
+ /** v4.6 D37 轴参严检(400 先于 404——镜像输入校验先行):非空轴参需与表单 include 键同现;
99
+ * 缺席/空串 ≡ 按实际配置解析(I3——不触发严检);错轴参数由壳层按名读取,天然静默忽略。 */
100
+ function assertAxisProviderUsable(
101
+ paramName: 'appProvider' | 'moduleProvider',
102
+ value: string | undefined,
103
+ include: ReadonlySet<string>,
104
+ ): void {
105
+ if (typeof value !== 'string' || value.length === 0) return;
106
+ if (!FORM_INCLUDE_KEYS.some((k) => include.has(k))) {
107
+ throw new AppToolkitError(
108
+ 'VALIDATION_FAILED',
109
+ 400,
110
+ `参数 ${paramName} 需与表单 include 键(${FORM_INCLUDE_KEYS.join('、')})同现——轴参仅重定向表单层双投影`,
111
+ [paramName],
112
+ );
113
+ }
114
+ }
115
+
90
116
  /** ── HTTP 直通面(11 读;L1 镜像律:方法名 = load + URL 资源段;query → 选项字段 L4)── */
91
117
  export interface AppToolkitRead {
92
118
  loadModules(): Promise<ModulesView>;
93
- loadModule(moduleId: string, o?: { include?: string[] }): Promise<ModuleDetailView>;
94
- loadApp(o?: { include?: string[] }): Promise<AppView>;
95
- loadService(service: string): Promise<ServiceDetail>;
119
+ /** v4.6 D37:moduleProvider = 轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include 键同现) */
120
+ loadModule(moduleId: string, o?: { include?: string[]; moduleProvider?: string }): Promise<ModuleDetailView>;
121
+ /** v4.6 D37:appProvider = 轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include 键同现) */
122
+ loadApp(o?: { include?: string[]; appProvider?: string }): Promise<AppView>;
123
+ loadService(service: string, o?: { include?: string[] }): Promise<ServiceDetail>;
96
124
  loadServiceResolutions(services?: string[]): Promise<ServiceResolutionsView>;
97
125
  loadServiceResolution(service: string): Promise<ServiceResolutionDetail>;
98
126
  loadProviders(): Promise<ProvidersView>;
@@ -201,8 +229,9 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
201
229
  // v4.4 D31:门控与内嵌分离——静态方法零门控分支;include=serviceResolutions 按
202
230
  // evaluation && strictValidation 双条件装配(<0.9/形态异常 → null,永不 503——技术方案 §2.4:
203
231
  // 仅判 strictValidation 会被「严格面在场+求值面缺席」畸形包击穿)
204
- async loadModule(moduleId: string, o?: { include?: string[] }): Promise<ModuleDetailView> {
232
+ async loadModule(moduleId: string, o?: { include?: string[]; moduleProvider?: string }): Promise<ModuleDetailView> {
205
233
  const include = validateInclude('module', o?.include); // ① 输入校验先行(400 先于 404)
234
+ assertAxisProviderUsable('moduleProvider', o?.moduleProvider, include); // ①' D37 轴参严检(先于 404)
206
235
  const contracts = await resolveContractsForApp(appDir);
207
236
  const snap = snapshot(contracts);
208
237
  const view = loadModuleDetailView(snap, moduleId); // ② MODULE_NOT_FOUND
@@ -212,13 +241,17 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
212
241
  loadServiceResolutionsView(snap, issues, view.services.map((s) => s.service)))
213
242
  : null; // module = demand 服务过滤(≡ 批量 ?service= 语义;issues 恒全量)
214
243
  }
215
- if (include.has('integrationSchemas')) {
216
- view.integrationSchemas = loadModuleIntegrationSchemas(snap, moduleId);
244
+ if (include.has('integrationSchemas') || include.has('integrationFieldRegistry')) {
245
+ // v4.6 D37:表单层双投影同源一次计算(双取单算 + I1——假想重定向天然同步)
246
+ const form = loadModuleFormViews(snap, moduleId, o?.moduleProvider);
247
+ if (include.has('integrationSchemas')) view.integrationSchemas = form.integrationSchemas;
248
+ if (include.has('integrationFieldRegistry')) view.integrationFieldRegistry = form.integrationFieldRegistry;
217
249
  }
218
250
  return view;
219
251
  },
220
- async loadApp(o?: { include?: string[] }): Promise<AppView> {
252
+ async loadApp(o?: { include?: string[]; appProvider?: string }): Promise<AppView> {
221
253
  const include = validateInclude('app', o?.include);
254
+ assertAxisProviderUsable('appProvider', o?.appProvider, include); // D37 轴参严检(先于任何计算)
222
255
  const contracts = await resolveContractsForApp(appDir);
223
256
  const snap = snapshot(contracts);
224
257
  const view = loadAppView(snap);
@@ -227,14 +260,23 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
227
260
  ? await predicateIssues().then((issues) => loadServiceResolutionsView(snap, issues)) // app = 全量键集
228
261
  : null;
229
262
  }
230
- if (include.has('integrationSchemas')) {
231
- view.integrationSchemas = loadAppIntegrationSchemas(snap);
263
+ if (include.has('integrationSchemas') || include.has('integrationFieldRegistry')) {
264
+ const form = loadAppFormViews(snap, o?.appProvider); // 双取单算 + I1(同 loadModule)
265
+ if (include.has('integrationSchemas')) view.integrationSchemas = form.integrationSchemas;
266
+ if (include.has('integrationFieldRegistry')) view.integrationFieldRegistry = form.integrationFieldRegistry;
232
267
  }
233
268
  return view;
234
269
  },
235
- async loadService(service: string) {
270
+ // v4.7 D38:include 镜像 loadModule 三段式——① 输入校验 400 先于 404 ② 视图 ③ 静态富化(永不 503)
271
+ async loadService(service: string, o?: { include?: string[] }): Promise<ServiceDetail> {
272
+ const include = validateInclude('service', o?.include);
236
273
  const contracts = await resolveContractsForApp(appDir);
237
- return loadServiceDetailView(snapshot(contracts), service);
274
+ const snap = snapshot(contracts); // 单快照——detail 与 include 装配共享
275
+ const view = loadServiceDetailView(snap, service);
276
+ if (include.has('integrationSchemas')) {
277
+ view.integrationSchemas = loadServiceIntegrationSchemas(snap, service);
278
+ }
279
+ return view;
238
280
  },
239
281
  async loadServiceResolutions(services?: string[]) {
240
282
  const contracts = await resolveContractsForApp(appDir);
package/src/index.ts CHANGED
@@ -56,6 +56,10 @@ export type {
56
56
  // ── 谓词装配单源(FX-2b/F1:CLI doctor 规则与 toolkit 工厂同装配面——双通道 walkManifests)──
57
57
  export { buildPredicateAssembly, refToEnvKey } from './integrations/predicate-io.js';
58
58
  export type { PredicateAssembly, PredicateIoInputs } from './integrations/predicate-io.js';
59
+ // ── 应用级求值单源(cli-remove-prune-and-new:doctor integration-services 与 prune 同一求值)──
60
+ export { evaluateAppIntegrationIssues } from './integrations/evaluate-app.js';
61
+ export { pruneIntegrationBindings } from './integrations/prune-bindings.js';
62
+ export type { PruneResult } from './integrations/prune-bindings.js';
59
63
 
60
64
  // ── assembly 层(目录知识——walkManifests 四产物 + catalog 双形态 + 组合单体装配)──
61
65
  export { walkManifests } from './assembly/walk-manifests.js';
@@ -85,7 +89,7 @@ export { loadAppView } from './views/app.js';
85
89
  export { loadServiceDetailView } from './views/service-detail.js';
86
90
  export { loadProvidersView, loadProviderDetailView, loadProviderServiceDetailView } from './views/providers.js';
87
91
  export { loadServiceResolutionsView, loadServiceResolutionDetail, resolveServiceSummary } from './views/service-resolutions.js';
88
- export { loadAppIntegrationSchemas, loadModuleIntegrationSchemas, resolveLayerIntegrationSchemas } from './views/integration-schemas.js';
92
+ export { loadAppIntegrationSchemas, loadModuleIntegrationSchemas, loadServiceIntegrationSchemas, resolveLayerIntegrationSchemas } from './views/integration-schemas.js';
89
93
  export { echoForRef, loadCredentialsView } from './views/credentials.js';
90
94
  export { loadCredentialTypesView } from './views/credential-types.js';
91
95
  export type { ViewContext, ViewSnapshot } from './views/context.js';
@@ -94,7 +98,7 @@ export type { ViewContext, ViewSnapshot } from './views/context.js';
94
98
  export { createAppToolkit } from './factory.js';
95
99
  export type { AppToolkit, AppToolkitRead, AppToolkitWrite, AppToolkitInternals, AppToolkitOptions } from './factory.js';
96
100
 
97
- // ── DTO(api.md §3 字段级单源——34 类型;agtcodingbox/tbox-ai-coding import type 消费)──
101
+ // ── DTO(api.md §3 字段级单源——36 类型;agtcodingbox/tbox-ai-coding import type 消费)──
98
102
  export type {
99
103
  ServiceStatus,
100
104
  BindingLayer,
@@ -104,8 +108,10 @@ export type {
104
108
  ModuleDetailView,
105
109
  AppView,
106
110
  ServiceDetail,
111
+ ServiceIntegrationSchemas,
107
112
  ProviderBinding,
108
113
  LayerIntegrationSchemas,
114
+ IntegrationFieldRegistryView,
109
115
  CredentialsView,
110
116
  CredentialBindingEcho,
111
117
  CredentialTypesView,
@@ -141,7 +141,7 @@ export function reconcileOrphanCredentials(appDir: string): OrphanIssue[] {
141
141
  issues.push({
142
142
  severity: 'warning',
143
143
  code: 'ORPHANED_CREDENTIAL',
144
- message: `config/credentials/${stem}.json 不再被引用(凭据文件保留,可手动清理)`,
144
+ message: `${stemToFilePath(stem)} 不再被引用(凭据文件保留,可手动清理)`,
145
145
  });
146
146
  }
147
147
  return issues;
@@ -0,0 +1,41 @@
1
+ import type { ResolvedContracts } from '../core/contracts-resolver.js';
2
+ import { walkManifests } from '../assembly/walk-manifests.js';
3
+ import { loadProviderCatalog } from '../assembly/provider-catalog.js';
4
+ import { buildPredicateAssembly } from './predicate-io.js';
5
+ import {
6
+ evaluateIntegrationServices,
7
+ type IntegrationServiceIssue,
8
+ type PredicateContracts,
9
+ type SupplyLookup,
10
+ } from './predicate.js';
11
+
12
+ /**
13
+ * 应用级 integrations 求值(cli-remove-prune-and-new D2.4):doctor integration-services 规则内
14
+ * "装配+求值"序列的单源抽取(FX-2b/F1 轨迹延续——此前 buildPredicateAssembly 已单源,本函数
15
+ * 将 walk→supplies→assembly→evaluate 全序列收口)。
16
+ *
17
+ * 消费方:
18
+ * - doctor integration-services 规则(外壳保留:stale 防线/文件读取/env 展开/形状守卫/分级投射)
19
+ * - pruneIntegrationBindings(prune 判据 ≡ doctor 判据的构造性同源载体)
20
+ *
21
+ * 契约:调用方须先 `resolveContractsForApp` 并确保 `strictValidation`(本函数不再分级门控——
22
+ * doctor 的 warning/error 升级指引与 prune 的 skip 语义分属两消费方,不收口)。
23
+ */
24
+ export async function evaluateAppIntegrationIssues(
25
+ appDir: string,
26
+ integrations: Record<string, unknown>,
27
+ contracts: ResolvedContracts,
28
+ env: Readonly<Record<string, string | undefined>> = process.env,
29
+ ): Promise<IntegrationServiceIssue[]> {
30
+ const module = contracts.module as PredicateContracts;
31
+ const walk = walkManifests(appDir);
32
+ const supplies: SupplyLookup = module.buildSupplyLookup(loadProviderCatalog(appDir) as never);
33
+ const assembly = buildPredicateAssembly(appDir, walk, integrations, env);
34
+
35
+ return evaluateIntegrationServices({
36
+ contracts: module,
37
+ integrations: integrations as never,
38
+ supplies,
39
+ ...assembly,
40
+ });
41
+ }
@@ -481,7 +481,8 @@ export function evaluateIntegrationServices(
481
481
  // 不应被 B 厂商判死。引用面(v3)= root.provider ∪ domains.*.provider ∪ 槽级(含内联
482
482
  // .provider)——terse 化后槽级双字段退场,引用面不上移即静默漏检(N2)。
483
483
  // 保守跳过条件(无法确证即不判死——误报会阻断部署):任一引用厂商 configSchema 文件缺失,
484
- // 或被绑定但零 configSchema 声明(如 mock local 供给——无 schema 知识)。
484
+ // 或被绑定但零 configSchema 声明(如 local 供给未声明 configSchema——无 schema 知识。
485
+ // 注:provider-mock 0.3.0 起全槽带 schema,mock 绑定下本检查已生效)。
485
486
  if (vendorConfigKeys) {
486
487
  const referenced = new Set<string>();
487
488
  let schemaIncomplete = false;
@@ -0,0 +1,186 @@
1
+ import { existsSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { ResolvedContracts } from '../core/contracts-resolver.js';
4
+ import { resolveContractsForApp } from '../core/contracts-resolver.js';
5
+ import { expandEnvVars } from '../core/env-expansion.js';
6
+ import { writeAtomic } from '../core/fskit.js';
7
+ import { walkManifests } from '../assembly/walk-manifests.js';
8
+ import { readIntegrationsConfig } from './read.js';
9
+ import { domainOfService } from './domain-binding.js';
10
+ import { isControlledInlineProvider } from './predicate.js';
11
+ import { reconcileOrphanCredentials } from './credentials.js';
12
+ import { evaluateAppIntegrationIssues } from './evaluate-app.js';
13
+
14
+ /**
15
+ * integrations.json 死绑定清理(cli-remove-prune-and-new D2 核心件)。
16
+ *
17
+ * 不变式:**删除判据 ≡ doctor integration-services 报错判据(构造性同源)**——消费 doctor 同一
18
+ * 共享求值器 evaluateAppIntegrationIssues 的结构化 issue,code 白名单
19
+ * {VENDOR_NOT_INSTALLED, BINDING_UNRESOLVED} + 守卫(绑定引用链候选 provider ∈ installedVendors
20
+ * 即保留,排除活厂商错配)→ 删除域严格 = "供给载体消亡"。
21
+ *
22
+ * 由此推论(均已实证):需求侧孤儿绑定(demand 消失、供给在场)保留——doctor 判其合法;用户
23
+ * 预配置绑定受保护;一次 remove 自愈全文件历史死债;凭据类 issue(CREDENTIAL_FILE_MISSING 等)
24
+ * 刻意不进白名单,原样保留。
25
+ *
26
+ * 写侧三防线(镜像 write core):N1 归一化(空模块节点删 domains 键)→ parseIntegrationsConfig
27
+ * 严格校验(失败弃写)→ writeAtomic。凭据孤儿只报告不清理(D9——与 DELETE 管线现行为一致)。
28
+ * 全空(无任何存活绑定)→ 删文件——ensureMockBindings(缺席即生成)在重装供给时再生闭环。
29
+ *
30
+ * 最佳努力语义:谓词不可用(contracts 未安装/降级、env 缺失、求值 throw)→ skipped 返回 +
31
+ * 文件原样,绝不阻断 remove 本体。
32
+ */
33
+
34
+ export interface PruneResult {
35
+ /** 被清除的绑定节点描述(remove 日志行,如 'root.provider=mock' / 'services.member.account') */
36
+ removed: string[];
37
+ /** 孤儿凭据(D9 只报告不清理——reconcileOrphanCredentials 产物 message) */
38
+ orphanCredentials: string[];
39
+ /** 跳过原因(正常路径 undefined;not-configured = integrations.json 缺席,兼容态零动作) */
40
+ skipped?: 'predicate-unavailable' | 'validation-failed' | 'not-configured';
41
+ }
42
+
43
+ /** 宽松形状(谓词输入同族子集——doctor 读裸 JSON 先例,严格 zod 归模板 loader) */
44
+ interface PruneDoc {
45
+ provider?: string;
46
+ domains?: Record<string, { provider?: string; config?: Record<string, unknown> }>;
47
+ config?: Record<string, unknown>;
48
+ configByInstance?: Record<string, Record<string, unknown>>;
49
+ credentialRef?: string;
50
+ credentialRefByInstance?: Record<string, string>;
51
+ instances?: Array<{ id: string }>;
52
+ defaultInstanceId?: string | null;
53
+ modules?: Record<string, unknown>;
54
+ services?: Record<
55
+ string,
56
+ {
57
+ provider?: string | Record<string, unknown>;
58
+ implementation?: unknown;
59
+ config?: Record<string, unknown>;
60
+ credentialRef?: string;
61
+ instances?: Record<string, { enabled?: boolean; provider?: unknown; implementation?: unknown; credentialRef?: string }>;
62
+ }
63
+ >;
64
+ }
65
+
66
+ /** 候选 provider 字符串提取(string 直取;受控内联对象取 .provider;其余 undefined) */
67
+ function providerOf(v: unknown): string | undefined {
68
+ if (typeof v === 'string') return v.length > 0 ? v : undefined;
69
+ if (v && typeof v === 'object' && isControlledInlineProvider(v)) {
70
+ const p = (v as { provider: string }).provider;
71
+ return p.length > 0 ? p : undefined;
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ export async function pruneIntegrationBindings(appDir: string): Promise<PruneResult> {
77
+ const file = join(appDir, 'config', 'integrations.json');
78
+ const raw = readIntegrationsConfig(appDir);
79
+ if (raw === null) return { removed: [], orphanCredentials: [], skipped: 'not-configured' };
80
+
81
+ const contracts: ResolvedContracts = await resolveContractsForApp(appDir);
82
+ if (!contracts.strictValidation) {
83
+ return { removed: [], orphanCredentials: [], skipped: 'predicate-unavailable' };
84
+ }
85
+
86
+ // env 展开(tolerant——doctor 走 fail-fast 外壳,prune 缺变量即降级跳过)
87
+ let integrations: Record<string, unknown>;
88
+ try {
89
+ const expanded = expandEnvVars(JSON.stringify(raw), process.env);
90
+ if (expanded.missing.length > 0 || expanded.text === undefined) {
91
+ return { removed: [], orphanCredentials: [], skipped: 'predicate-unavailable' };
92
+ }
93
+ integrations = JSON.parse(expanded.text) as Record<string, unknown>;
94
+ } catch {
95
+ return { removed: [], orphanCredentials: [], skipped: 'predicate-unavailable' };
96
+ }
97
+
98
+ let issues;
99
+ try {
100
+ issues = await evaluateAppIntegrationIssues(appDir, integrations, contracts, process.env);
101
+ } catch {
102
+ return { removed: [], orphanCredentials: [], skipped: 'predicate-unavailable' };
103
+ }
104
+
105
+ const installedVendors = new Set(Object.keys(walkManifests(appDir).catalog.providers));
106
+ const next = structuredClone(raw) as PruneDoc;
107
+ next.services ??= {};
108
+ const removed: string[] = [];
109
+
110
+ for (const issue of issues) {
111
+ if (issue.severity !== 'error') continue;
112
+ if (issue.code === 'VENDOR_NOT_INSTALLED') {
113
+ if (issue.module) {
114
+ const d = next.domains?.[issue.module];
115
+ if (d?.provider) {
116
+ removed.push(`domains.${issue.module}.provider=${d.provider}`);
117
+ delete d.provider;
118
+ if (Object.keys(d).length === 0) delete next.domains![issue.module]; // N1
119
+ }
120
+ } else if (next.provider) {
121
+ removed.push(`root.provider=${next.provider}`);
122
+ delete next.provider;
123
+ }
124
+ } else if (issue.code === 'BINDING_UNRESOLVED' && issue.service) {
125
+ const s = next.services[issue.service];
126
+ if (!s) continue;
127
+ // 守卫:引用链任一候选 ∈ installedVendors → 活厂商错配,保留(doctor 报错语义不变)
128
+ const candidates = [
129
+ issue.instance ? providerOf(s.instances?.[issue.instance]?.provider) : undefined,
130
+ providerOf(s.provider),
131
+ providerOf(next.domains?.[domainOfService(issue.service)]?.provider),
132
+ providerOf(next.provider),
133
+ ].filter((v): v is string => typeof v === 'string');
134
+ if (candidates.some((v) => installedVendors.has(v))) continue;
135
+ if (issue.instance) {
136
+ const inst = s.instances;
137
+ if (!inst || !(issue.instance in inst)) continue;
138
+ delete inst[issue.instance];
139
+ removed.push(`services.${issue.service}.instances.${issue.instance}`);
140
+ if (Object.keys(inst).length === 0) delete s.instances;
141
+ if (Object.keys(s).length === 0) delete next.services[issue.service];
142
+ } else {
143
+ delete next.services[issue.service];
144
+ removed.push(`services.${issue.service}`);
145
+ }
146
+ }
147
+ }
148
+ if (Object.keys(next.services).length === 0) delete next.services;
149
+ if (removed.length === 0) return { removed, orphanCredentials: [] };
150
+
151
+ // 全空判定先行(**先于严格校验**——空文档 `{}` 缺必填 services 键必然被 zod 拒;删文件免校验)
152
+ const empty =
153
+ !next.provider &&
154
+ Object.keys(next.domains ?? {}).length === 0 &&
155
+ Object.keys(next.services ?? {}).length === 0 &&
156
+ Object.keys(next.config ?? {}).length === 0 &&
157
+ Object.keys(next.configByInstance ?? {}).length === 0 &&
158
+ !next.credentialRef &&
159
+ Object.keys(next.credentialRefByInstance ?? {}).length === 0 &&
160
+ (next.instances ?? []).length === 0 &&
161
+ next.defaultInstanceId == null &&
162
+ Object.keys(next.modules ?? {}).length === 0;
163
+
164
+ // ④ 严格校验(仅非空文档——services 为 schema 必填键,全服务剪除的存活文档补空 record 归一)
165
+ if (!empty) {
166
+ next.services ??= {};
167
+ if (typeof contracts.module.parseIntegrationsConfig === 'function') {
168
+ try {
169
+ contracts.module.parseIntegrationsConfig(next as never);
170
+ } catch {
171
+ return { removed: [], orphanCredentials: [], skipped: 'validation-failed' };
172
+ }
173
+ }
174
+ }
175
+
176
+ // ⑤⑥ 写 + 孤儿报告(D9);空 → 删文件(ensureMockBindings 重装再生闭环)
177
+ let orphanCredentials: string[] = [];
178
+ if (empty) {
179
+ if (existsSync(file)) rmSync(file, { force: true });
180
+ } else {
181
+ writeAtomic(file, `${JSON.stringify(next, null, 2)}\n`);
182
+ }
183
+ orphanCredentials = reconcileOrphanCredentials(appDir).map((o) => o.message);
184
+
185
+ return { removed, orphanCredentials };
186
+ }