@tbox.cn/app-toolkit 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/chunk-WUULQDOV.js +1 -0
  2. package/dist/contracts-resolver-PS2UH2G6.js +1 -0
  3. package/dist/index.d.ts +382 -129
  4. package/dist/index.js +9 -8
  5. package/package.json +2 -2
  6. package/src/assembly/provider-catalog.ts +1 -1
  7. package/src/assembly/resolve-key.ts +75 -0
  8. package/src/assembly/walk-manifests.ts +216 -96
  9. package/src/core/contracts-expected.ts +2 -2
  10. package/src/core/contracts-resolver.ts +12 -1
  11. package/src/core/env-expansion.ts +14 -12
  12. package/src/core/module-schema.ts +88 -12
  13. package/src/dto.ts +53 -25
  14. package/src/factory.ts +62 -18
  15. package/src/index.ts +18 -7
  16. package/src/integrations/evaluate-app.ts +41 -0
  17. package/src/integrations/mock-bindings.ts +1 -1
  18. package/src/integrations/module-binding.ts +60 -0
  19. package/src/integrations/predicate-io.ts +26 -5
  20. package/src/integrations/predicate.ts +97 -57
  21. package/src/integrations/prune-bindings.ts +205 -0
  22. package/src/integrations/write.ts +43 -27
  23. package/src/views/app.ts +8 -4
  24. package/src/views/context.ts +26 -7
  25. package/src/views/integration-schemas.ts +61 -17
  26. package/src/views/modules.ts +45 -27
  27. package/src/views/providers.ts +13 -1
  28. package/src/views/service-detail.ts +5 -3
  29. package/src/views/service-resolutions.ts +14 -13
  30. package/tests/credentials-view.test.ts +2 -2
  31. package/tests/demo-app.ts +34 -5
  32. package/tests/dev-manifest-catalog.test.ts +86 -6
  33. package/tests/file-cache.test.ts +5 -5
  34. package/tests/module-schema.test.ts +117 -16
  35. package/tests/resolve-key.test.ts +154 -0
  36. package/tests/vendor-schema-keywords.test.ts +86 -0
  37. package/tests/views.test.ts +425 -139
  38. package/tests/write-core.test.ts +83 -0
  39. package/dist/chunk-KPD3LUH4.js +0 -1
  40. package/dist/contracts-resolver-E4SECBOG.js +0 -1
  41. package/src/integrations/domain-binding.ts +0 -81
@@ -0,0 +1,205 @@
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 { isControlledInlineProvider } from './predicate.js';
10
+ import { reconcileOrphanCredentials } from './credentials.js';
11
+ import { evaluateAppIntegrationIssues } from './evaluate-app.js';
12
+
13
+ /**
14
+ * integrations.json 死绑定清理(cli-remove-prune-and-new D2 核心件)。
15
+ *
16
+ * 不变式:**删除判据 ≡ doctor integration-services 报错判据(构造性同源)**——消费 doctor 同一
17
+ * 共享求值器 evaluateAppIntegrationIssues 的结构化 issue,code 白名单
18
+ * {VENDOR_NOT_INSTALLED, BINDING_UNRESOLVED, MODULE_CONFIG_DEAD} + 守卫(绑定引用链候选
19
+ * provider ∈ installedVendors 即保留,排除活厂商错配)→ 删除域严格 = "供给载体消亡"。
20
+ * v6:模块卸载 → modules[m] **整节点确定性清理**(provider+config+resources——D4:
21
+ * 模块消亡清理 vs 集成位编辑保全不矛盾;MODULE_CONFIG_DEAD 为判据源)。
22
+ *
23
+ * 由此推论(均已实证):需求侧孤儿绑定(demand 消失、供给在场)保留——doctor 判其合法;用户
24
+ * 预配置绑定受保护;一次 remove 自愈全文件历史死债;凭据类 issue(CREDENTIAL_FILE_MISSING 等)
25
+ * 刻意不进白名单,原样保留。
26
+ *
27
+ * 写侧三防线(镜像 write core):归一化(空节点删键)→ parseIntegrationsConfig
28
+ * 严格校验(失败弃写)→ writeAtomic。凭据孤儿只报告不清理(D9——与 DELETE 管线现行为一致)。
29
+ * 全空(无任何存活绑定)→ 删文件——ensureMockBindings(缺席即生成)在重装供给时再生闭环。
30
+ *
31
+ * 最佳努力语义:谓词不可用(contracts 未安装/降级、env 缺失、求值 throw)→ skipped 返回 +
32
+ * 文件原样,绝不阻断 remove 本体。
33
+ */
34
+
35
+ export interface PruneResult {
36
+ /** 被清除的绑定节点描述(remove 日志行,如 'root.provider=mock' / 'services.member.account') */
37
+ removed: string[];
38
+ /** 孤儿凭据(D9 只报告不清理——reconcileOrphanCredentials 产物 message) */
39
+ orphanCredentials: string[];
40
+ /** 跳过原因(正常路径 undefined;not-configured = integrations.json 缺席,兼容态零动作) */
41
+ skipped?: 'predicate-unavailable' | 'validation-failed' | 'not-configured';
42
+ }
43
+
44
+ /** 宽松形状(谓词输入同族子集——doctor 读裸 JSON 先例,严格 zod 归模板 loader) */
45
+ interface PruneDoc {
46
+ provider?: string;
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 walk = walkManifests(appDir);
106
+ const installedVendors = new Set(Object.keys(walk.catalog.providers));
107
+ const walkOwnership = walk.ownership;
108
+ const next = structuredClone(raw) as PruneDoc;
109
+ next.services ??= {};
110
+ const removed: string[] = [];
111
+
112
+ for (const issue of issues) {
113
+ if (issue.severity !== 'error') continue;
114
+ if (issue.code === 'VENDOR_NOT_INSTALLED') {
115
+ if (issue.module) {
116
+ // v6:modules[owner].provider 死厂商 → 整节点删除前提下的 provider 键清理;
117
+ // 仅 provider(无 config/resources)→ 节点删除(N1 归一)
118
+ const modulesNode = (next.modules ?? {}) as Record<string, Record<string, unknown>>;
119
+ const node = modulesNode[issue.module];
120
+ if (node?.provider) {
121
+ removed.push(`modules.${issue.module}.provider=${String(node.provider)}`);
122
+ delete node.provider;
123
+ if (Object.keys(node).length === 0) delete next.modules![issue.module]; // 归一
124
+ }
125
+ } else if (next.provider) {
126
+ removed.push(`root.provider=${next.provider}`);
127
+ delete next.provider;
128
+ }
129
+ } else if (issue.code === 'MODULE_CONFIG_DEAD' && issue.module) {
130
+ // v6 D4:模块卸载 → modules[m] 整节点确定性清理(provider+config+resources)
131
+ const modulesNode = (next.modules ?? {}) as Record<string, Record<string, unknown>>;
132
+ if (modulesNode[issue.module]) {
133
+ const node = modulesNode[issue.module];
134
+ const parts = [
135
+ node.provider !== undefined ? 'provider' : undefined,
136
+ node.config !== undefined ? 'config' : undefined,
137
+ node.resources !== undefined ? 'resources' : undefined,
138
+ ].filter(Boolean);
139
+ removed.push(`modules.${issue.module}(${parts.join('+')} 整节点)`);
140
+ delete next.modules![issue.module];
141
+ }
142
+ } else if (issue.code === 'BINDING_UNRESOLVED' && issue.service) {
143
+ const s = next.services[issue.service];
144
+ if (!s) continue;
145
+ // 守卫:引用链任一候选 ∈ installedVendors → 活厂商错配,保留(doctor 报错语义不变;
146
+ // v6 引用链 = 实例级/槽级/modules[ownerOf]/root 四层)
147
+ const owner = walkOwnership[issue.service];
148
+ const candidates = [
149
+ issue.instance ? providerOf(s.instances?.[issue.instance]?.provider) : undefined,
150
+ providerOf(s.provider),
151
+ owner !== undefined ? providerOf((next.modules as Record<string, { provider?: unknown }> | undefined)?.[owner]?.provider) : undefined,
152
+ providerOf(next.provider),
153
+ ].filter((v): v is string => typeof v === 'string');
154
+ if (candidates.some((v) => installedVendors.has(v))) continue;
155
+ if (issue.instance) {
156
+ const inst = s.instances;
157
+ if (!inst || !(issue.instance in inst)) continue;
158
+ delete inst[issue.instance];
159
+ removed.push(`services.${issue.service}.instances.${issue.instance}`);
160
+ if (Object.keys(inst).length === 0) delete s.instances;
161
+ if (Object.keys(s).length === 0) delete next.services[issue.service];
162
+ } else {
163
+ delete next.services[issue.service];
164
+ removed.push(`services.${issue.service}`);
165
+ }
166
+ }
167
+ }
168
+ if (Object.keys(next.services).length === 0) delete next.services;
169
+ if (removed.length === 0) return { removed, orphanCredentials: [] };
170
+
171
+ // 全空判定先行(**先于严格校验**——空文档 `{}` 缺必填 services 键必然被 zod 拒;删文件免校验)
172
+ const empty =
173
+ !next.provider &&
174
+ Object.keys(next.services ?? {}).length === 0 &&
175
+ Object.keys(next.config ?? {}).length === 0 &&
176
+ Object.keys(next.configByInstance ?? {}).length === 0 &&
177
+ !next.credentialRef &&
178
+ Object.keys(next.credentialRefByInstance ?? {}).length === 0 &&
179
+ (next.instances ?? []).length === 0 &&
180
+ next.defaultInstanceId == null &&
181
+ Object.keys(next.modules ?? {}).length === 0;
182
+
183
+ // ④ 严格校验(仅非空文档——services 为 schema 必填键,全服务剪除的存活文档补空 record 归一)
184
+ if (!empty) {
185
+ next.services ??= {};
186
+ if (typeof contracts.module.parseIntegrationsConfig === 'function') {
187
+ try {
188
+ contracts.module.parseIntegrationsConfig(next as never);
189
+ } catch {
190
+ return { removed: [], orphanCredentials: [], skipped: 'validation-failed' };
191
+ }
192
+ }
193
+ }
194
+
195
+ // ⑤⑥ 写 + 孤儿报告(D9);空 → 删文件(ensureMockBindings 重装再生闭环)
196
+ let orphanCredentials: string[] = [];
197
+ if (empty) {
198
+ if (existsSync(file)) rmSync(file, { force: true });
199
+ } else {
200
+ writeAtomic(file, `${JSON.stringify(next, null, 2)}\n`);
201
+ }
202
+ orphanCredentials = reconcileOrphanCredentials(appDir).map((o) => o.message);
203
+
204
+ return { removed, orphanCredentials };
205
+ }
@@ -19,8 +19,9 @@ import {
19
19
  import type { IntegrationServiceIssue } from './predicate.js';
20
20
  import { checkInstanceKeys } from './predicate.js';
21
21
  import { isKnownService } from './read.js';
22
- import { displayOwnerName, moduleDomainWritable } from './domain-binding.js';
22
+ import { moduleOwnershipWritable } from './module-binding.js';
23
23
  import { AppToolkitError } from '../core/errors.js';
24
+ import { MODULE_INTEGRATION_NODE_KEYS } from '../dto.js';
24
25
 
25
26
  /**
26
27
  * 写核心(C3-4 write;FX-1c 管线重排——F3 九步定序 + D-B2 语义 + F4 输入扩位):
@@ -231,7 +232,8 @@ export interface WriteCore {
231
232
  warmup(): Promise<void>;
232
233
  /** PUT /app/integration(root 节点全集) */
233
234
  applyApp(node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
234
- /** PUT /modules/:module/integration(domains 换算;N1 {} 删域键;v4.4 D34 写守卫单源) */
235
+ /** PUT /modules/:module/integration(v6 modules[owner] 集成位;键集纪律见 MODULE_INTEGRATION_NODE_KEYS;
236
+ * N1 {} 清 provider/config 保 resources;moduleOwnershipWritable 写守卫单源) */
235
237
  applyModule(moduleId: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
236
238
  /** PUT /services/:service/integration(整节点含 instances map;N3 {} 不归一化;v4.4 A2 身份判据) */
237
239
  applyService(service: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
@@ -336,7 +338,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
336
338
  message: 'config/integrations.json 缺席——已物化骨架(写时自动初始化;R13 语义翻转提示)',
337
339
  });
338
340
  }
339
- // root 全量替换(D14 无例外语义):缺席根键删除(services/domains/modules 域不在根键集——随 base 保留),
341
+ // root 全量替换(D14 无例外语义):缺席根键删除(services/modules 域不在根键集——随 base 保留;v6 domains 键已退役),
340
342
  // node 全键置入(未知键交严格 zod 拒——fail-visible;B10:原实现 spread+merge 残留 stale 根键)
341
343
  const next: Record<string, unknown> = { ...base };
342
344
  for (const k of APP_INTEGRATION_NODE_KEYS) if (!(k in node)) delete next[k];
@@ -370,37 +372,27 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
370
372
 
371
373
  applyModule(moduleId: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
372
374
  assertSafeNames(node, 'root');
373
- // D34 写守卫(v4.4,moduleDomainWritable 单源):404(模块在册)→ 400(多域/零域/共享键
374
- // 三分支)→ 既有管线。守卫先于 no-op 短路——多域/零域/共享键模块 PUT {} 清域同 400
375
- // (「写路径拒绝歧义操作」含清除;P13/P17)。
376
- const { modules, catalog } = walkManifests(appDir, cache);
375
+ // v6 写守卫(moduleOwnershipWritable 单源):404(模块在册)→ 400(zero-ownership——
376
+ // 模块级集成位作用于 declare.owns 服务,零归属模块无配置位)→ 既有管线。
377
+ // 守卫先于 no-op 短路——零归属模块 PUT {} 清位同 400(「写路径拒绝歧义操作」含清除)。
378
+ const { modules, catalog, ownership } = walkManifests(appDir, cache);
377
379
  const mod = modules.find((m) => m.id === moduleId);
378
380
  if (!mod) {
379
381
  throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
380
382
  }
381
- const w = moduleDomainWritable({ modules, catalog }, mod);
383
+ const w = moduleOwnershipWritable({ modules, catalog, ownership }, mod);
382
384
  if (!w.writable) {
383
- if (w.reason === 'multi-domain') {
384
- throw new AppToolkitError(
385
- 'VALIDATION_FAILED',
386
- 400,
387
- `模块 ${moduleId} 跨 ${w.domainKeys.length} 个域(${w.domainKeys.join('、')})——模块级统一配置仅支持单域模块,请按服务级配置(编辑各服务行)`,
388
- );
389
- }
390
- if (w.reason === 'zero-domain') {
391
- throw new AppToolkitError('VALIDATION_FAILED', 400, `模块 ${moduleId} 无服务需求声明,无域配置位`);
392
- }
393
385
  throw new AppToolkitError(
394
386
  'VALIDATION_FAILED',
395
387
  400,
396
- `域键 ${w.domainKey} ${w.coOwners.map((o) => displayOwnerName(modules, o)).join('、')} 共用——写通道拒绝歧义操作,请按服务级配置`,
388
+ `模块 ${moduleId} 未声明任何 declare.owns——无模块级配置位(模块级 provider/config 作用于归属服务)`,
397
389
  );
398
390
  }
399
- const domainKey = w.domainKey;
400
391
  const issues: IntegrationServiceIssue[] = [];
401
392
  const current = loadConfig(appDir, cache);
393
+ // N2 语义(v6):空节点 = 清 provider/config 两键,resources 随既有节点保全;
394
+ // 文件缺席 + 空节点 = no-op(幂等)
402
395
  if (current === null && Object.keys(node).length === 0) {
403
- // 空文件清空域 = no-op(N1 幂等)
404
396
  return { written: false, restartScheduled: false, files: [], credentials: [], issues };
405
397
  }
406
398
  if (current === null) {
@@ -411,15 +403,39 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
411
403
  });
412
404
  }
413
405
  const base: Record<string, unknown> = current ?? { services: {} };
414
- const domains = { ...((base.domains as Record<string, unknown>) ?? {}) };
415
- const next: Record<string, unknown> = { ...base, domains };
416
- // N1:模块节点 {} domains[键](清域);其余 = 域键全量替换
417
- if (Object.keys(node).length === 0) delete domains[domainKey];
418
- else domains[domainKey] = { ...node };
406
+ const modulesNode = { ...((base.modules as Record<string, unknown>) ?? {}) };
407
+ const existing = { ...((modulesNode[moduleId] as Record<string, unknown> | undefined) ?? {}) };
408
+ // 键集纪律:node 内仅接受 MODULE_INTEGRATION_NODE_KEYS 成员(未知键交严格 zod 拒——fail-visible)
409
+ for (const k of Object.keys(node)) {
410
+ if (!(MODULE_INTEGRATION_NODE_KEYS as readonly string[]).includes(k)) {
411
+ throw new AppToolkitError(
412
+ 'VALIDATION_FAILED',
413
+ 400,
414
+ `模块集成位仅接受 ${MODULE_INTEGRATION_NODE_KEYS.join('/')} 键(收到 ${k})`,
415
+ [k],
416
+ );
417
+ }
418
+ }
419
+ // 全量替换(键集内):缺席键删除;空对象/空串值 = 删除(≡ 缺席——模块节点不留空键);
420
+ // PUT {} = 清 provider/config,resources 恒随 existing 保全(N2:清集成位不杀资源绑定)
421
+ const nextNode: Record<string, unknown> = { ...existing };
422
+ for (const k of MODULE_INTEGRATION_NODE_KEYS) {
423
+ const v = node[k];
424
+ const isEmptyish = v === undefined || v === null || v === ''
425
+ || (typeof v === 'object' && !Array.isArray(v) && Object.keys(v as object).length === 0);
426
+ if (isEmptyish) delete nextNode[k];
427
+ else nextNode[k] = v;
428
+ }
429
+ if (Object.keys(nextNode).length === 0) {
430
+ delete modulesNode[moduleId];
431
+ } else {
432
+ modulesNode[moduleId] = nextNode;
433
+ }
434
+ const next: Record<string, unknown> = { ...base, modules: modulesNode };
419
435
  if (deepEqual(current, next)) {
420
436
  return { written: false, restartScheduled: false, files: [], credentials: [], issues };
421
437
  }
422
- runStrictValidation(next, issues, `domains.${domainKey}.`);
438
+ runStrictValidation(next, issues, `modules.${moduleId}.`);
423
439
  issues.push(...registryCheck(next));
424
440
  return finalize(next, [], issues, writeOpts.dryRun);
425
441
  },
package/src/views/app.ts CHANGED
@@ -1,22 +1,26 @@
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 双端点同型)。
7
7
  * /app = 平台服务 + root 节点原值(纯静态):services = 服务词汇 − 模块占用
8
- * (ServiceDemand[]——平台条目 required 恒 false、title/description 缺席,D29);
8
+ * (ServiceDemand[]——平台条目 required 恒 false、name/description 缺席,D29);
9
9
  * 平台服务状态不在此(→ /service-resolutions);模块服务不在此(→ /modules/:m)。
10
10
  * integration = **root 节点投影**(APP_INTEGRATION_NODE_KEYS——D21/L1:GET 预填 ≡ PUT body;
11
- * 文件另含 services/domains/modules 域键,不进 root 节点投影——原实现整文件透传,
11
+ * 文件另含 services/modules 域键,不进 root 节点投影——原实现整文件透传,
12
12
  * PUT 回会级联写回域键);文件缺席 = null,文件在场 → 投影对象(可为空)。
13
13
  */
14
14
 
15
15
  export function loadAppView(snapshot: ViewSnapshot): AppView {
16
16
  const occupied = moduleOccupiedServices(snapshot.walk);
17
+ // 平台条目展示名走链(B1):词汇 meta > demand meta > id——name 恒发(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, name: display.name, ...(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) {
@@ -1,12 +1,11 @@
1
1
  import { readIntegrationsConfig } from '../integrations/read.js';
2
- import { domainOfService } from '../integrations/domain-binding.js';
3
2
  import { walkManifests, type WalkCatalog, type WalkManifestsResult } from '../assembly/walk-manifests.js';
4
3
  import type { FileCache } from '../core/file-cache.js';
5
4
  import type { ResolvedContracts } from '../core/contracts-resolver.js';
6
5
 
7
6
  /**
8
7
  * 视图装配共享上下文(五投影共用;目录枚举零缓存——walkManifests 每次调用执行)。
9
- * 域前缀换算(service 首点段)自 v4.4 F1 迁 integrations/domain-binding.ts(本文件 re-export 保导入路径)。
8
+ * v6:域前缀换算退役(domainOfService 随归属模型退场)——模块占用判定改 declare 三键直读。
10
9
  */
11
10
 
12
11
  export interface ViewContext {
@@ -22,8 +21,6 @@ export interface ViewSnapshot {
22
21
  contracts: ResolvedContracts;
23
22
  }
24
23
 
25
- export { domainOfService };
26
-
27
24
  /** 读取视图输入快照(每方法调用独立——Freshness Contract 单位 = 方法调用) */
28
25
  export async function loadViewSnapshot(ctx: ViewContext): Promise<ViewSnapshot> {
29
26
  const walk = walkManifests(ctx.appDir, ctx.cache);
@@ -40,12 +37,14 @@ export function loadViewSnapshotSync(ctx: ViewContext, contracts: ResolvedContra
40
37
  return { appDir: ctx.appDir, walk, catalog: walk.catalog, integrations, contracts };
41
38
  }
42
39
 
43
- /** 模块占用服务集(demand 侧——平台服务 = 词汇 本集) */
40
+ /** 模块占用服务集(v6——demand supply owns:归属模型下「平台服务」= 无主服务;
41
+ * mall-core owns auth.alipay-login → 移出 AppView.services 平台条目,入其模块页 ownedServices) */
44
42
  export function moduleOccupiedServices(walk: WalkManifestsResult): Set<string> {
45
43
  const occupied = new Set<string>();
46
44
  for (const mod of walk.modules) {
47
- for (const need of mod.descriptor.contributes.services ?? []) occupied.add(need.service);
48
- for (const slot of mod.descriptor.contributes.providers?.slots ?? []) occupied.add(slot.service);
45
+ for (const own of mod.descriptor.declare.owns ?? []) occupied.add(own.service);
46
+ for (const need of mod.descriptor.declare.consumes ?? []) occupied.add(need.service);
47
+ for (const slot of mod.descriptor.declare.providers?.slots ?? []) occupied.add(slot.service);
49
48
  }
50
49
  return occupied;
51
50
  }
@@ -65,3 +64,23 @@ export function suppliedByVendors(catalog: WalkCatalog, service: string): string
65
64
  }
66
65
  return vendors.sort();
67
66
  }
67
+
68
+ /** 服务展示名解析结果(name 恒有值——id 兜底内聚;v7 词汇统一 title→name) */
69
+ export interface ServiceDisplay {
70
+ name: string;
71
+ description?: string;
72
+ }
73
+
74
+ /**
75
+ * 服务展示名单源(B1):词汇 meta(slotMeta——contracts-mall 词汇锁保证 mall 域覆盖)
76
+ * > demand meta(catalog.services——域外槽兜底;首声明聚合)> service id。逐字段回落——
77
+ * 词汇条目仅含 description 时 demand name 仍可达(外部契约包部分 meta 前向容错)。
78
+ * 四视图唯一消费点(/modules/:m services、/app 平台条目、/services/:s、/providers/:p services)。
79
+ */
80
+ export function resolveServiceDisplay(walk: WalkManifestsResult, service: string): ServiceDisplay {
81
+ const vocab = walk.slotMeta[service];
82
+ const demand = walk.catalog.services[service];
83
+ const name = vocab?.name ?? demand?.name ?? service;
84
+ const description = vocab?.description ?? demand?.description;
85
+ return { name, ...(description !== undefined ? { description } : {}) };
86
+ }
@@ -1,7 +1,7 @@
1
1
  import type { IntegrationFieldRegistryView, LayerIntegrationSchemas, ServiceIntegrationSchemas } from '../dto.js';
2
2
  import { isSupplied, type ViewSnapshot } from './context.js';
3
3
  import { resolveServiceConfigSchema } from './providers.js';
4
- import { domainOfService, moduleDomainWritable, providerFootprint, resolveDomainBinding } from '../integrations/domain-binding.js';
4
+ import { moduleOwnershipWritable, providerFootprint, resolveModuleBinding } from '../integrations/module-binding.js';
5
5
  import { isControlledInlineProvider } from '../integrations/predicate.js';
6
6
 
7
7
  /**
@@ -117,6 +117,12 @@ export function buildIntegrationFieldRegistry(
117
117
  key,
118
118
  schema: mergeFieldSources(sources),
119
119
  sources: sources.map(({ service, required }) => ({ service, required })),
120
+ // G 批:x-tbox-global 聚合旗标(sources.some——subschema 属性标记直读,零新增 IO;
121
+ // 任一来源声明即 true——并集口径与 required 聚合同族)
122
+ global: sources.some(
123
+ (s) => !!s.subschema && typeof s.subschema === 'object'
124
+ && (s.subschema as Record<string, unknown>)['x-tbox-global'] === true,
125
+ ),
120
126
  });
121
127
  }
122
128
  return {
@@ -129,7 +135,7 @@ export function buildIntegrationFieldRegistry(
129
135
  }
130
136
 
131
137
  /** D37 生效厂商解析单源(实际/假想同径):override(非空串)?? 实际解析。
132
- * 空串归一(I3)单点;module 轴 D34 不可写/不在册 → null(override 不豁免——预览服务于保存)。 */
138
+ * 空串归一(I3)单点;module 轴 v6 零归属 → null(override 不豁免——预览服务于保存)。 */
133
139
  export function formProviderOf(
134
140
  axis: 'app' | 'module',
135
141
  snapshot: ViewSnapshot,
@@ -144,10 +150,10 @@ export function formProviderOf(
144
150
  }
145
151
  const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
146
152
  if (!mod) return null; // 视图已先行 404,此处防御
147
- const w = moduleDomainWritable(snapshot.walk, mod);
148
- if (!w.writable) return null; // D34 不豁免
153
+ const w = moduleOwnershipWritable(snapshot.walk, mod);
154
+ if (!w.writable) return null; // v6 零归属不豁免
149
155
  if (ovr !== undefined) return ovr;
150
- return resolveDomainBinding(snapshot.integrations, w.domainKey).provider;
156
+ return resolveModuleBinding(snapshot.integrations, mod.id).provider;
151
157
  }
152
158
 
153
159
  /** D36/D37 表单层双投影(I1:同源同口径一次计算——双取单算 by construction) */
@@ -159,29 +165,39 @@ export interface LayerFormViews {
159
165
  /** 防御双 null(module 不在册/不可写/未绑定、app 未绑定——F3) */
160
166
  const NULL_FORM_VIEWS: LayerFormViews = { integrationSchemas: null, integrationFieldRegistry: null };
161
167
 
168
+ /** G 批表单收窄(消费投影单点):app/module 轴 registry.fields 只留 global===true——
169
+ * 未声明键落 ExtraFields 语义(P16 复合可见面:layer.properties 仍全量,字段级代数不受扰)。
170
+ * schemaless/unsupplied/additionalPropertiesAllowed 描述服务/Schema 面,不随字段过滤。 */
171
+ function narrowToGlobal(registry: IntegrationFieldRegistryView): IntegrationFieldRegistryView {
172
+ return { ...registry, fields: registry.fields.filter((f) => f.global) };
173
+ }
174
+
162
175
  /** app 轴装配:scope = 供给足迹全量(≠ AppView.services——root.config 流向包括模块服务在内的所有服务);
163
176
  * unsupplied 恒 [](结构性:足迹 ⊆ 供给)。未知厂商/零足迹 → 非 null 空集(镜像 D32 mock 语义)。 */
164
177
  export function loadAppFormViews(snapshot: ViewSnapshot, appProvider?: string): LayerFormViews {
165
178
  const provider = formProviderOf('app', snapshot, undefined, appProvider);
166
179
  if (provider === null) return NULL_FORM_VIEWS;
167
180
  const layer = resolveLayerIntegrationSchemas(snapshot, provider, providerFootprint(snapshot.catalog, provider));
168
- return { integrationSchemas: layer, integrationFieldRegistry: buildIntegrationFieldRegistry(layer) };
181
+ return { integrationSchemas: layer, integrationFieldRegistry: narrowToGlobal(buildIntegrationFieldRegistry(layer)) };
169
182
  }
170
183
 
171
- /** module 轴装配:仅 writable 单域(D34);scope = demand该厂商供给;unsupplied 同点派生
172
- * (demand − 供给,Set 去重保序——manifest demand 不去重防御)。 */
184
+ /** module 轴装配:仅 writable(v6 owns>0);scope = ownedServices该厂商供给(v6——模块
185
+ * config 流向归属服务);unsupplied 同点派生(ownedServices − 供给,Set 去重保序——
186
+ * declare.owns 不去重防御)。 */
173
187
  export function loadModuleFormViews(snapshot: ViewSnapshot, moduleId: string, moduleProvider?: string): LayerFormViews {
174
188
  const provider = formProviderOf('module', snapshot, moduleId, moduleProvider);
175
189
  const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
176
190
  if (provider === null || !mod) return NULL_FORM_VIEWS;
177
- const demand = (mod.descriptor.contributes.services ?? []).map((s) => s.service);
191
+ const owned = (mod.descriptor.declare.owns ?? []).map((o) => o.service);
178
192
  const supplied = (s: string): boolean => isSupplied(snapshot.catalog, provider, s);
179
- const layer = resolveLayerIntegrationSchemas(snapshot, provider, demand.filter(supplied));
193
+ const layer = resolveLayerIntegrationSchemas(snapshot, provider, owned.filter(supplied));
180
194
  return {
181
195
  integrationSchemas: layer,
182
- integrationFieldRegistry: buildIntegrationFieldRegistry(layer, {
183
- unsupplied: [...new Set(demand.filter((s) => !supplied(s)))],
184
- }),
196
+ integrationFieldRegistry: narrowToGlobal(
197
+ buildIntegrationFieldRegistry(layer, {
198
+ unsupplied: [...new Set(owned.filter((s) => !supplied(s)))],
199
+ }),
200
+ ),
185
201
  };
186
202
  }
187
203
 
@@ -195,9 +211,20 @@ export function loadModuleIntegrationSchemas(snapshot: ViewSnapshot, moduleId: s
195
211
  return loadModuleFormViews(snapshot, moduleId).integrationSchemas;
196
212
  }
197
213
 
214
+ /** 服务级生效绑定(v6 级联):显式 ?? modules[ownerOf[s]].provider ?? root.provider。
215
+ * 服务无主 → 仅 root(平台槽语义)。 */
216
+ function serviceLevelBindingProvider(snapshot: ViewSnapshot, service: string): string | null {
217
+ const owner = snapshot.walk.ownership[service];
218
+ if (owner !== undefined) {
219
+ return resolveModuleBinding(snapshot.integrations, owner).provider;
220
+ }
221
+ const p = (snapshot.integrations as { provider?: unknown } | null)?.provider;
222
+ return typeof p === 'string' && p.length > 0 ? p : null;
223
+ }
224
+
198
225
  /** 服务级 loader(v4.7 D38):四分支穷举(node.provider = 文件 services[s].provider 逐字原值;
199
226
  * 实例层无条件排除——node 级口径);非空三分支归约一次复合解析。
200
- * ① undefined/'' → resolveDomainBinding 域级联(域 + root 双缺 → 整字段 null)
227
+ * ① undefined/'' → 模块级联(v6:modules[ownerOf[s]] root 双缺 → 整字段 null)
201
228
  * ② 非空 string → bare:catalog miss(未知厂商/未供给)→ configSchema:null 不 404(富化非门控)
202
229
  * ③ 受控内联(isControlledInlineProvider 单源)→ 名 = p.provider;catalog 复合键 > p.configSchema ref
203
230
  * (H9 逃逸 400 照抛——readInlineSchema 共享路径)
@@ -210,10 +237,10 @@ export function loadServiceIntegrationSchemas(
210
237
  const p = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
211
238
  ?.services?.[service]?.provider;
212
239
  if (p === undefined || p === '') {
213
- const binding = resolveDomainBinding(snapshot.integrations, domainOfService(service));
214
- return binding.provider === null
240
+ const provider = serviceLevelBindingProvider(snapshot, service);
241
+ return provider === null
215
242
  ? null
216
- : { provider: binding.provider, configSchema: resolveServiceConfigSchema(snapshot, binding.provider, service) };
243
+ : { provider, configSchema: resolveServiceConfigSchema(snapshot, provider, service) };
217
244
  }
218
245
  if (typeof p === 'string') {
219
246
  return { provider: p, configSchema: resolveServiceConfigSchema(snapshot, p, service) };
@@ -224,3 +251,20 @@ export function loadServiceIntegrationSchemas(
224
251
  }
225
252
  return null;
226
253
  }
254
+
255
+ /** G 批:服务轴字段注册表(D38 四分支链复用——消费 loadServiceIntegrationSchemas 单源;
256
+ * 伪层 {provider, services: {[s]: {configSchema}}} → buildIntegrationFieldRegistry——
257
+ * fields = 该 schema 全部字段带 global 旗标;configSchema null → schemaless=[service]
258
+ * 非 null 空集;schemas 整体 null(全链未绑定/坏节点)→ registry null——镜像 D38 语义) */
259
+ export function loadServiceFieldRegistry(
260
+ snapshot: ViewSnapshot,
261
+ service: string,
262
+ ): IntegrationFieldRegistryView | null {
263
+ const schemas = loadServiceIntegrationSchemas(snapshot, service);
264
+ if (schemas === null) return null;
265
+ const layer: LayerIntegrationSchemas = {
266
+ provider: schemas.provider,
267
+ services: { [service]: { configSchema: schemas.configSchema } },
268
+ };
269
+ return buildIntegrationFieldRegistry(layer);
270
+ }