@tbox.cn/app-toolkit 0.6.0 → 0.7.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.
@@ -36,8 +36,15 @@ export class AppToolkitError extends Error {
36
36
  /** CONTRACTS_NOT_RESOLVED message(N1 构建指引——api.md 错误信封示例)。
37
37
  * P-4:notes 优先——resolver 在全部不可解析路径均已 push 携带操作指引的 notes(导入失败/
38
38
  * 产物缺失/未安装/<0.9 升级);现行 version===null 分支会把 notes 内细分指引吞成通用文案。
39
- * 三分支保留为兜底(防御性——正常路径 notes 恒非空)。 */
40
- export function contractsNotResolvedMessage(contracts: { version: string | null; strictValidation: boolean; notes: readonly string[] }): string {
39
+ * 分支保留为兜底(防御性——正常路径 notes 恒非空)。
40
+ * v7:+= ownershipBindingV6 分支(标记门第四分支单源化——写门控 assertContractsUsable
41
+ * 读求值轴降级 statusMessage 共用同一文案;可选参向后兼容)。 */
42
+ export function contractsNotResolvedMessage(contracts: {
43
+ version: string | null;
44
+ strictValidation: boolean;
45
+ notes: readonly string[];
46
+ ownershipBindingV6?: boolean;
47
+ }): string {
41
48
  if (contracts.notes.length > 0) {
42
49
  return contracts.notes[0];
43
50
  }
@@ -47,6 +54,9 @@ export function contractsNotResolvedMessage(contracts: { version: string | null;
47
54
  if (!contracts.strictValidation) {
48
55
  return `contracts ${contracts.version} 无严格校验面(需 ≥0.9.0)——升级应用 @tbox.cn/app-contracts 依赖后可用`;
49
56
  }
57
+ if (contracts.ownershipBindingV6 === false) {
58
+ return `contracts ${contracts.version ?? '(版本未知)'} 不支持 v6 modules 绑定形状(OWNERSHIP_BINDING_V6 标记缺席)——升级应用 @tbox.cn/app-contracts 依赖后可用`;
59
+ }
50
60
  // 版本达标但解析态异常(notes 携带细分——导入拒绝/产物缺失)
51
61
  return 'contracts workspace 源码态未构建——执行 pnpm --filter @tbox.cn/app-contracts build 后重试';
52
62
  }
package/src/dto.ts CHANGED
@@ -12,13 +12,17 @@
12
12
 
13
13
  // ===== 词汇基线 =====
14
14
 
15
- /** contracts 求值五态原样(EffectiveServiceResolution.status */
15
+ /** contracts 求值五态原样(EffectiveServiceResolution.status)+ 视图轴降级态
16
+ * (evaluation-unavailable——toolkit 视图独有,非 contracts 求值产物:contracts 不可用
17
+ * (未安装/无严格面/v6 标记缺席)时读求值轴不再 503,改投静态骨架 + 本态标记;
18
+ * 消费方按非 ok 态渲染,statusMessage 携带 N1 分支指引) */
16
19
  export type ServiceStatus =
17
20
  | 'ok'
18
21
  | 'service-not-configured'
19
22
  | 'instance-not-found'
20
23
  | 'instance-disabled'
21
- | 'binding-unresolved';
24
+ | 'binding-unresolved'
25
+ | 'evaluation-unavailable';
22
26
 
23
27
  /** 四配置层(instance 为 map 字段层) */
24
28
  export type BindingLayer = 'app' | 'module' | 'service' | 'instance';
package/src/factory.ts CHANGED
@@ -18,7 +18,13 @@ import { loadViewSnapshotSync } from './views/context.js';
18
18
  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
- import { loadServiceResolutionsView, loadServiceResolutionDetail } from './views/service-resolutions.js';
21
+ import {
22
+ loadServiceResolutionsView,
23
+ loadServiceResolutionDetail,
24
+ loadServiceResolutionsDegradeView,
25
+ loadServiceResolutionDegradeDetail,
26
+ contractsEvaluable,
27
+ } from './views/service-resolutions.js';
22
28
  import { loadAppFormViews, loadModuleFormViews, loadServiceIntegrationSchemas, loadServiceFieldRegistry } from './views/integration-schemas.js';
23
29
  import { loadCredentialsView } from './views/credentials.js';
24
30
  import { loadCredentialTypesView } from './views/credential-types.js';
@@ -51,11 +57,13 @@ import type {
51
57
  * Read+Write = HTTP 直通面 additive 冻结(L1 端点↔方法 1:1,L3 命名镜像:Integration=节点级 /
52
58
  * Config=文件级);Internals = CLI/doctor/桥消费(自由演进)。
53
59
  *
54
- * **门控断言单点(policy at edge)**:求值(2)+ 写(4)+ ensureMockBindings / writeIntegrationsConfig /
55
- * evaluateIntegrationServices 入口 assertContractsUsable——strictValidation === false → 503
56
- * CONTRACTS_NOT_RESOLVED;静态方法不门控(永不 503——铁律 #3);**include 装配为条件分支非门控**
57
- * (D31——serviceResolutions evaluation && strictValidation 双条件装配,<0.9/形态异常 → null,
58
- * 静态端点永不 503;integrationSchemas 纯静态轴零 contracts);integrations/ views/ 机制层保持纯函数。
60
+ * **门控断言单点(policy at edge——v7 读求值轴退役后范围 = + 迁移面)**:写(4)+
61
+ * ensureMockBindings / writeIntegrationsConfig / evaluateIntegrationServices 入口
62
+ * assertContractsUsable——strictValidation === false → 503 CONTRACTS_NOT_RESOLVED;静态方法不门控
63
+ * (永不 503——铁律 #3);**读求值轴(loadServiceResolutions/loadServiceResolution include 装配)
64
+ * v7 起不再 503**——contracts 不可用 200 降级骨架(evaluation-unavailable,doctor 分级/D31 先例
65
+ * 对齐;依据链与裁决见 Agent Note evaluation-axis-degrade);**include 装配为条件分支非门控**
66
+ * (D31 null 分支退役——三条件门 + 内嵌轴同步降级,四面统一骨架);integrations/ views/ 机制层保持纯函数。
59
67
  *
60
68
  * 并发:进程内写串行(per-factory promise chain)+ 原子写 + 写后本实例缓存即刻失效(D26)。
61
69
  * onApplied(D7):toolkit 判定「需要重启」(实际落盘且非 dryRun/no-diff)→ 调用一次 +
@@ -193,11 +201,8 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
193
201
  throw new AppToolkitError('CONTRACTS_NOT_RESOLVED', 503, contractsNotResolvedMessage(contracts));
194
202
  }
195
203
  if (!contracts.ownershipBindingV6) {
196
- throw new AppToolkitError(
197
- 'CONTRACTS_NOT_RESOLVED',
198
- 503,
199
- `contracts ${contracts.version ?? '(版本未知)'} 不支持 v6 modules 绑定形状(OWNERSHIP_BINDING_V6 标记缺席)——升级应用 @tbox.cn/app-contracts 依赖后可用`,
200
- );
204
+ // v6 标记门第四分支(文案单源 = contractsNotResolvedMessage——降级轴 statusMessage 同源)
205
+ throw new AppToolkitError('CONTRACTS_NOT_RESOLVED', 503, contractsNotResolvedMessage(contracts));
201
206
  }
202
207
  }
203
208
 
@@ -263,10 +268,12 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
263
268
  const mod = resolveModuleKey(snap.walk, moduleId);
264
269
  const view = loadModuleDetailView(snap, mod.id);
265
270
  if (include.has('serviceResolutions')) {
266
- view.serviceResolutions = contracts.evaluation && contracts.strictValidation
271
+ // v7:三条件门 + 内嵌轴同步降级(P14 (b) 四面统一骨架——D31 null 分支退役:
272
+ // include 请求了即恒返回视图;老消费方 null 分支保留为兼容死码)
273
+ view.serviceResolutions = contractsEvaluable(contracts)
267
274
  ? await predicateIssues().then((issues) =>
268
275
  loadServiceResolutionsView(snap, issues, view.services.map((s) => s.service)))
269
- : null; // module = demand 服务过滤(≡ 批量 ?service= 语义;issues 恒全量)
276
+ : loadServiceResolutionsDegradeView(snap, contracts, view.services.map((s) => s.service)); // module = demand 服务过滤(≡ 批量 ?service= 语义)
270
277
  }
271
278
  if (include.has('integrationSchemas') || include.has('integrationFieldRegistry')) {
272
279
  // v4.6 D37:表单层双投影同源一次计算(双取单算 + I1——假想重定向天然同步)
@@ -283,9 +290,10 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
283
290
  const snap = snapshot(contracts);
284
291
  const view = loadAppView(snap);
285
292
  if (include.has('serviceResolutions')) {
286
- view.serviceResolutions = contracts.evaluation && contracts.strictValidation
293
+ // v7:内嵌轴同步降级(同 loadModule——D31 null 退役,app = 全量键集)
294
+ view.serviceResolutions = contractsEvaluable(contracts)
287
295
  ? await predicateIssues().then((issues) => loadServiceResolutionsView(snap, issues)) // app = 全量键集
288
- : null;
296
+ : loadServiceResolutionsDegradeView(snap, contracts);
289
297
  }
290
298
  if (include.has('integrationSchemas') || include.has('integrationFieldRegistry')) {
291
299
  const form = loadAppFormViews(snap, o?.appProvider); // 双取单算 + I1(同 loadModule)
@@ -316,15 +324,19 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
316
324
  },
317
325
  async loadServiceResolutions(services?: string[]) {
318
326
  const contracts = await resolveContractsForApp(appDir);
319
- assertContractsUsable(contracts);
327
+ const snap = snapshot(contracts);
328
+ // v7 读求值轴降级门(policy-at-edge 读轴退役):contracts 不可用 → 200 静态骨架 +
329
+ // evaluation-unavailable(fail-visible 保留在数据内;写路径 503 门控不变)
330
+ if (!contractsEvaluable(contracts)) return loadServiceResolutionsDegradeView(snap, contracts, services);
320
331
  const issues = await predicateIssues();
321
- return loadServiceResolutionsView(snapshot(contracts), issues, services);
332
+ return loadServiceResolutionsView(snap, issues, services);
322
333
  },
323
334
  async loadServiceResolution(service: string) {
324
335
  const contracts = await resolveContractsForApp(appDir);
325
- assertContractsUsable(contracts);
326
336
  const snap = snapshot(contracts);
327
337
  const serviceId = resolveServiceKey(snap.walk, configuredServiceKeys(snap), service);
338
+ // v7 降级门同批量端点(三条件与;降级单体携带声明实例枚举 + inheritance 静态投影)
339
+ if (!contractsEvaluable(contracts)) return loadServiceResolutionDegradeDetail(serviceId, snap, contracts);
328
340
  const issues = await predicateIssues();
329
341
  return loadServiceResolutionDetail(serviceId, snap, issues);
330
342
  },
package/src/index.ts CHANGED
@@ -64,10 +64,10 @@ export { pruneIntegrationBindings } from './integrations/prune-bindings.js';
64
64
  export type { PruneResult } from './integrations/prune-bindings.js';
65
65
 
66
66
  // ── assembly 层(目录知识——walkManifests 四产物 + catalog 双形态 + 组合单体装配)──
67
- export { walkManifests } from './assembly/walk-manifests.js';
67
+ export { walkManifests, discoverContractPackages } from './assembly/walk-manifests.js';
68
68
  // 双键寻址(v7 词汇统一——factory 七方法入口单点消费的解析 helper;纯参数式零 IO)
69
69
  export { resolveModuleKey, resolveServiceKey } from './assembly/resolve-key.js';
70
- export type { WalkManifestsResult, WalkModuleInfo, WalkCatalog, WalkProviderEntry, DeclarationIssue, SlotMeta } from './assembly/walk-manifests.js';
70
+ export type { WalkManifestsResult, WalkModuleInfo, WalkCatalog, WalkProviderEntry, DeclarationIssue, SlotMeta, DiscoveredSlotSource } from './assembly/walk-manifests.js';
71
71
  export { readAppManifest, writeAppManifest, manifestPath } from './assembly/app-manifest.js';
72
72
  export type { AppManifest, NpmModuleEntry, ModuleMode } from './assembly/app-manifest.js';
73
73
  export { loadProviderCatalog, loadProviderCatalogRaw } from './assembly/provider-catalog.js';
@@ -10,7 +10,7 @@ import type {
10
10
  ServiceResolutionDetail,
11
11
  ServiceStatus,
12
12
  } from '../dto.js';
13
- import { AppToolkitError } from '../core/errors.js';
13
+ import { AppToolkitError, contractsNotResolvedMessage } from '../core/errors.js';
14
14
  import { resolveModuleBinding } from '../integrations/module-binding.js';
15
15
  import type { ResolvedContracts } from '../core/contracts-resolver.js';
16
16
  import type { WalkCatalog } from '../assembly/walk-manifests.js';
@@ -83,6 +83,10 @@ function evaluateInstance(
83
83
  service,
84
84
  instanceId,
85
85
  supplies as never,
86
+ // v6 ownerOf:服务归属表(walk.ownership——buildOwnershipMap 同源产物)显式参。
87
+ // 缺席 + 文件含 modules 级绑定/配置 → contracts v6 fail-visible(binding-unresolved),
88
+ // root/modules 级联全断——视图轴与谓词轴同源透传,勿再遗漏(bug 复盘:fef9a71a 漏改点)。
89
+ snapshot.walk.ownership as Record<string, string>,
86
90
  );
87
91
  if (r.status === 'ok' && r.effective) {
88
92
  const raw: unknown = r.effective.provider;
@@ -124,6 +128,7 @@ function evaluationContracts(snapshot: ViewSnapshot) {
124
128
  service: string,
125
129
  instanceId: string,
126
130
  supplies?: never,
131
+ ownerOf?: Record<string, string>,
127
132
  ) => EffectiveResolutionShape;
128
133
  normalizeInstances: (input: unknown) => { instances: Array<{ id: string }>; defaultInstanceId: string | null };
129
134
  buildSupplyLookup: (catalog: unknown) => unknown;
@@ -205,18 +210,22 @@ function isSuppliedCheck(catalog: WalkCatalog, provider: string, service: string
205
210
  return isSupplied(catalog, provider, service);
206
211
  }
207
212
 
213
+ /** 批量键集(求值轴/降级轴共用单源:服务词汇 ∪ 需求侧 ∪ 已配置) */
214
+ function resolutionKeys(snapshot: ViewSnapshot): string[] {
215
+ const keys = new Set<string>([...snapshot.walk.vocabulary, ...Object.keys(snapshot.catalog.services)]);
216
+ const configured = (snapshot.integrations as { services?: Record<string, unknown> } | null)?.services ?? {};
217
+ for (const k of Object.keys(configured)) keys.add(k);
218
+ return [...keys].sort();
219
+ }
220
+
208
221
  /** 批量求值(键集 = 服务词汇 ∪ 已配置;?service= 过滤语义——未知键缺席;issues 恒全量) */
209
222
  export function loadServiceResolutionsView(
210
223
  snapshot: ViewSnapshot,
211
224
  issues: IntegrationIssue[],
212
225
  filter?: string[],
213
226
  ): ServiceResolutionsView {
214
- const keys = new Set<string>([...snapshot.walk.vocabulary, ...Object.keys(snapshot.catalog.services)]);
215
- const configured = (snapshot.integrations as { services?: Record<string, unknown> } | null)?.services ?? {};
216
- for (const k of Object.keys(configured)) keys.add(k);
217
-
218
227
  const resolutions: Record<string, ServiceResolutionEntry> = {};
219
- for (const service of [...keys].sort()) {
228
+ for (const service of resolutionKeys(snapshot)) {
220
229
  if (filter && !filter.includes(service)) continue;
221
230
  resolutions[service] = resolveServiceSummary(service, snapshot);
222
231
  }
@@ -238,20 +247,12 @@ export function loadServiceResolutionDetail(
238
247
  });
239
248
 
240
249
  // P4 枚举规则:root 注册表 id 逐一 + 显式 '*' 条目(若在);无注册表且节点通配激活 → 单 '*' 合成;
241
- // 无注册表无节点 → []
250
+ // 无注册表无节点 → [](枚举单源 = enumerateInstanceIds——降级轴声明实例投影共用)
242
251
  const declaredIds = normalized.instances.map((i) => i.id);
243
252
  const slot = (snapshot.integrations as { services?: Record<string, { instances?: Record<string, unknown> }> } | null)
244
253
  ?.services?.[service];
245
254
  const slotKeys = slot?.instances ? Object.keys(slot.instances) : undefined;
246
- let instanceIds: string[];
247
- if (declaredIds.length > 0) {
248
- instanceIds = [...declaredIds];
249
- if (slotKeys?.includes('*')) instanceIds.push('*');
250
- } else if (slotKeys !== undefined && slotKeys.length > 0) {
251
- instanceIds = slotKeys.includes('*') ? ['*'] : slotKeys;
252
- } else {
253
- instanceIds = [];
254
- }
255
+ const instanceIds = enumerateInstanceIds(slotKeys, declaredIds);
255
256
 
256
257
  // 默认实例口径(摘要 status = 默认实例求值——不重复头部;原 defaultEval 死代码已删 FX-2c)
257
258
 
@@ -267,7 +268,7 @@ export function loadServiceResolutionDetail(
267
268
  });
268
269
 
269
270
  // inheritance 四层(app → module → service → instance——effective 链声明位投影)
270
- const inheritance = buildInheritance(snapshot, service);
271
+ const inheritance = buildInheritance(snapshot, service, normalized.defaultInstanceId ?? '*');
271
272
 
272
273
  const summary = resolveServiceSummary(service, snapshot);
273
274
  const resolution: ServiceResolution = {
@@ -283,24 +284,31 @@ export function loadServiceResolutionDetail(
283
284
  };
284
285
  }
285
286
 
287
+ /** P4 实例枚举单源(求值轴/降级轴共用):root 注册表 id 逐一 + 显式 '*' 条目(若在);
288
+ * 无注册表且节点通配激活 → 单 '*' 合成;无注册表无节点 → []。 */
289
+ function enumerateInstanceIds(slotKeys: string[] | undefined, declaredIds: string[]): string[] {
290
+ if (declaredIds.length > 0) {
291
+ const ids = [...declaredIds];
292
+ if (slotKeys?.includes('*')) ids.push('*');
293
+ return ids;
294
+ }
295
+ if (slotKeys !== undefined && slotKeys.length > 0) return slotKeys.includes('*') ? ['*'] : slotKeys;
296
+ return [];
297
+ }
298
+
286
299
  /** inheritance 投影:四层各取该层声明 provider/implementation(缺席层 = 继承位,provider 缺席)。
287
- * 实例层 = **默认实例口径**(FX-2c 修——与摘要 status 同轴:normalizeInstances 委托解析
288
- * defaultInstanceId ?? '*';原实现取 Object.keys[0] 任意实例)。 */
300
+ * 实例层 = **默认实例口径**(FX-2c 修——与摘要 status 同轴);defaultInstanceId 由调用方解析
301
+ * (求值轴 = normalizeInstances 委托;降级轴 = 原始直读——本函数零 contracts 依赖,双轴共用单源)。 */
289
302
  function buildInheritance(
290
303
  snapshot: ViewSnapshot,
291
304
  service: string,
305
+ defaultInstanceId: string,
292
306
  ): ServiceResolution['inheritance'] {
293
307
  const integrations = snapshot.integrations as {
294
308
  provider?: string;
295
309
  services?: Record<string, { provider?: unknown; implementation?: string; instances?: Record<string, { provider?: string; implementation?: string }> }>;
296
310
  } | null;
297
311
  const slot = integrations?.services?.[service];
298
- const cfg = snapshot.integrations as { instances?: unknown; defaultInstanceId?: unknown } | null;
299
- const normalized = evaluationContracts(snapshot).normalizeInstances({
300
- instances: cfg?.instances,
301
- defaultInstanceId: cfg?.defaultInstanceId,
302
- });
303
- const defaultInstanceId = normalized.defaultInstanceId ?? '*';
304
312
  const defaultInstance = slot?.instances?.[defaultInstanceId] ?? slot?.instances?.['*'];
305
313
  // module 段消费 resolveModuleBinding 单源(v6——与 binding / 谓词 #7 同源;ownerOf=walk.ownership)
306
314
  const owner = snapshot.walk.ownership[service];
@@ -328,3 +336,83 @@ function buildInheritance(
328
336
 
329
337
  /** 谓词全量 issues 装配(写路径/求值轴共用——工厂经 evaluateIntegrationServices 装配后传入) */
330
338
  export type { IntegrationIssue };
339
+
340
+ // ── 降级轴(v7——policy-at-edge 读求值轴退役)──
341
+ // contracts 不可用(未安装 / 无严格面 / 求值面缺席 / v6 标记缺席)时,读求值轴不再 503,
342
+ // 改投静态骨架(walk + integrations 纯文件投影,零 contracts 依赖)+ evaluation-unavailable 标记。
343
+ // 依据链:doctor 分级先例(未安装 = warning 正常态)+ D31 include→null 先例;fail-visible 语义
344
+ // 保留在数据内(status + statusMessage N1 分支文案),写路径与 internals 门控不变。
345
+
346
+ /** 读求值轴可用判据(三条件与——降级门单源;写路径 assertContractsUsable 另有 503 语义,不复用) */
347
+ export function contractsEvaluable(contracts: ResolvedContracts): boolean {
348
+ return contracts.evaluation && contracts.strictValidation && contracts.ownershipBindingV6;
349
+ }
350
+
351
+ /** 降级单条目(静态可算字段:module = owners 首 / required = OR 聚合;provider/layer 不投——
352
+ * 保持「求值产物」语义纯净,layer 单独在场违 D31 孤儿层口径) */
353
+ function degradeEntry(snapshot: ViewSnapshot, service: string, message: string): ServiceResolutionEntry {
354
+ const demand = snapshot.catalog.services[service];
355
+ return {
356
+ status: 'evaluation-unavailable',
357
+ statusMessage: message,
358
+ ...(demand?.owners[0] !== undefined ? { module: demand.owners[0] } : {}),
359
+ ...(demand !== undefined ? { required: !demand.optional } : {}),
360
+ };
361
+ }
362
+
363
+ /** 批量降级视图(键集与求值轴同源 resolutionKeys;?service= 过滤语义一致;issues 恒 []——
364
+ * 谓词与求值同源缺席;声明面诊断走 declarationIssues 通道不在此混装) */
365
+ export function loadServiceResolutionsDegradeView(
366
+ snapshot: ViewSnapshot,
367
+ contracts: ResolvedContracts,
368
+ filter?: string[],
369
+ ): ServiceResolutionsView {
370
+ const message = contractsNotResolvedMessage(contracts);
371
+ const resolutions: Record<string, ServiceResolutionEntry> = {};
372
+ for (const service of resolutionKeys(snapshot)) {
373
+ if (filter && !filter.includes(service)) continue;
374
+ resolutions[service] = degradeEntry(snapshot, service, message);
375
+ }
376
+ return { contracts: toContractsResolution(contracts), resolutions, issues: [] };
377
+ }
378
+
379
+ /** 单体降级视图(P14 extends 结构保证——摘要基 + instances 声明枚举 + inheritance 四层静态投影) */
380
+ export function loadServiceResolutionDegradeDetail(
381
+ service: string,
382
+ snapshot: ViewSnapshot,
383
+ contracts: ResolvedContracts,
384
+ ): ServiceResolutionDetail {
385
+ const message = contractsNotResolvedMessage(contracts);
386
+ const resolution: ServiceResolution = {
387
+ ...degradeEntry(snapshot, service, message),
388
+ instances: degradeInstances(snapshot, service),
389
+ inheritance: buildInheritance(snapshot, service, degradeDefaultInstanceId(snapshot)),
390
+ };
391
+ return { contracts: toContractsResolution(contracts), resolution, issues: [] };
392
+ }
393
+
394
+ /** 声明实例投影(原始数据——root instances[].id ∪ slot 实例键,P4 枚举单源共用;
395
+ * 非 normalizeInstances 派生语义,单源不破)。逐实例同标记,statusMessage 不重复携带
396
+ * (resolution 顶层已携带)。 */
397
+ function degradeInstances(snapshot: ViewSnapshot, service: string): ResolvedInstance[] {
398
+ const slot = (snapshot.integrations as { services?: Record<string, { instances?: Record<string, unknown> }> } | null)
399
+ ?.services?.[service];
400
+ const cfg = snapshot.integrations as { instances?: unknown } | null;
401
+ const declaredIds = Array.isArray(cfg?.instances)
402
+ ? (cfg.instances as Array<{ id?: unknown }>)
403
+ .map((i) => (i && typeof i === 'object' ? i.id : undefined))
404
+ .filter((id): id is string => typeof id === 'string')
405
+ : [];
406
+ const slotKeys = slot?.instances ? Object.keys(slot.instances) : undefined;
407
+ return enumerateInstanceIds(slotKeys, declaredIds).map((id) => ({
408
+ instanceId: id,
409
+ status: 'evaluation-unavailable' as const,
410
+ }));
411
+ }
412
+
413
+ /** 降级默认实例口径(近似声明):defaultInstanceId 原始直读,缺席 → '*' 通配位。
414
+ * normalizeInstances 的「单候选唯一解」推导不可复制(求值面单源)——近似口径见 api 契约。 */
415
+ function degradeDefaultInstanceId(snapshot: ViewSnapshot): string {
416
+ const cfg = snapshot.integrations as { defaultInstanceId?: unknown } | null;
417
+ return typeof cfg?.defaultInstanceId === 'string' && cfg.defaultInstanceId.length > 0 ? cfg.defaultInstanceId : '*';
418
+ }
@@ -119,7 +119,7 @@ describe('walkManifests · dev-manifest catalog 通道(聚合宽/声明窄)'
119
119
  * (local/codegen 全量副本——mall 生成应用主形态,不依赖 install)。
120
120
  * 修复前探测只扫包根 → 三形态全 miss(vocabulary 恒空)。
121
121
  */
122
- describe('walkManifests · 词汇探测三跳 + slotMeta(B0', () => {
122
+ describe('walkManifests · 词汇探测四跳 + slotMeta(B0;v7 += 跳⓪ config override 层)', () => {
123
123
  it('P1: nm 包根布局(跳①)→ vocabulary 命中;slotMeta 透传', () => {
124
124
  app = createTempApp('tbox-probe-');
125
125
  const store = join(app.appDir, '_store-a');
@@ -185,6 +185,44 @@ describe('walkManifests · 词汇探测三跳 + slotMeta(B0)', () => {
185
185
  expect(walk.slotMeta).toEqual({ 'e.f': { name: '甲' } }); // 空串 description 不入表(与 title 守卫对称)
186
186
  });
187
187
 
188
+ it('P6: 跳⓪ config override 层——meta per-service 覆盖真源 + 无键键穿透真源(首声明 = config 最前)', () => {
189
+ app = createTempApp('tbox-probe-');
190
+ const store = join(app.appDir, '_store-truth');
191
+ writeFakeContractsPackage(store, {
192
+ slots: ['a.b', 'c.d'],
193
+ meta: { 'a.b': { name: '真源甲' }, 'c.d': { name: '真源丙' } },
194
+ });
195
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
196
+ // 用户 override:只改名 a.b;c.d 不写(穿透真源);e.f 仅快照声明(幻影语义由 doctor 报告)
197
+ writeFileSync(
198
+ join(app.appDir, 'config', 'service-slots.json'),
199
+ JSON.stringify({ $comment: { seededAt: 'x' }, slots: ['a.b', 'e.f'], meta: { 'a.b': { name: '用户改名甲' } } }),
200
+ );
201
+
202
+ const walk = walkManifests(app.appDir);
203
+ // 词汇并集:config ∪ 真源(config 不遮蔽真源新增)
204
+ expect(walk.vocabulary).toEqual(['a.b', 'c.d', 'e.f']);
205
+ // meta 覆盖:config 先声明胜出;未覆盖键穿透真源
206
+ expect(walk.slotMeta['a.b']).toEqual({ name: '用户改名甲' });
207
+ expect(walk.slotMeta['c.d']).toEqual({ name: '真源丙' });
208
+ });
209
+
210
+ it('P7: 跳⓪ 快照 meta 恒空(seed 形态)→ 真源 meta 恒穿透;$comment 键零解析影响', () => {
211
+ app = createTempApp('tbox-probe-');
212
+ const store = join(app.appDir, '_store-seed');
213
+ writeFakeContractsPackage(store, { slots: ['a.b'], meta: { 'a.b': { name: '真源甲' } } });
214
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
215
+ // seed-once 产物形态:slots 全量 + meta 空对象
216
+ writeFileSync(
217
+ join(app.appDir, 'config', 'service-slots.json'),
218
+ JSON.stringify({ $comment: { seededAt: '2026-09-21', slots: 1 }, slots: ['a.b'], meta: {} }),
219
+ );
220
+
221
+ const walk = walkManifests(app.appDir);
222
+ expect(walk.vocabulary).toEqual(['a.b']);
223
+ expect(walk.slotMeta['a.b']).toEqual({ name: '真源甲' }); // 空 meta 不夺覆盖权
224
+ });
225
+
188
226
  it('P6: 无契约包(退化态)→ vocabulary 空 + slotMeta 空(P9 退化路径语义锁)', () => {
189
227
  app = createTempApp('tbox-probe-');
190
228
  const walk = walkManifests(app.appDir);