@sema-agent/server 7.100.0 → 7.101.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 (102) hide show
  1. package/MIGRATION.md +22 -1
  2. package/README.md +12 -1
  3. package/README.zh-CN.md +9 -1
  4. package/USAGE.md +80 -9
  5. package/dist/approval-card.d.ts +24 -12
  6. package/dist/approval-card.js +4 -3
  7. package/dist/approval-policy-settlement.d.ts +31 -0
  8. package/dist/approval-policy-settlement.js +16 -0
  9. package/dist/approval-reconciler.d.ts +8 -1
  10. package/dist/approval-reconciler.js +5 -4
  11. package/dist/boot/config-center.js +86 -20
  12. package/dist/boot/execution-env.js +20 -2
  13. package/dist/boot/parked-revive-gate.js +1 -0
  14. package/dist/boot/reapers.js +1 -0
  15. package/dist/boot/resolve-spec.js +25 -8
  16. package/dist/boot/runner-deps.js +9 -3
  17. package/dist/boot/stage-02-registry-pull.js +9 -0
  18. package/dist/boot/stores.js +4 -0
  19. package/dist/config-catalog.js +5 -3
  20. package/dist/config-center/apply-effective.d.ts +25 -0
  21. package/dist/config-center/apply-effective.js +144 -69
  22. package/dist/config-center/apply-ledger.d.ts +12 -2
  23. package/dist/config-center/apply-ledger.js +4 -1
  24. package/dist/config-center/facade.d.ts +1 -1
  25. package/dist/config-center/facade.js +1 -1
  26. package/dist/config-center/http-client.d.ts +4 -1
  27. package/dist/config-center/http-client.js +15 -10
  28. package/dist/config-center/read-warnings.d.ts +115 -6
  29. package/dist/config-center/read-warnings.js +115 -5
  30. package/dist/config-center/types.d.ts +6 -0
  31. package/dist/config-invariants.d.ts +2 -2
  32. package/dist/config-invariants.js +15 -0
  33. package/dist/config-provider.d.ts +4 -0
  34. package/dist/config-provider.js +64 -9
  35. package/dist/config-types.d.ts +37 -8
  36. package/dist/config.js +87 -22
  37. package/dist/deployment-governance.d.ts +45 -12
  38. package/dist/deployment-governance.js +37 -9
  39. package/dist/env-name-allowlist-knobs.d.ts +37 -0
  40. package/dist/env-name-allowlist-knobs.js +24 -0
  41. package/dist/fleet-client.d.ts +2 -1
  42. package/dist/hooks/hook-llm.js +13 -4
  43. package/dist/host-lsp-manager.d.ts +14 -2
  44. package/dist/host-lsp-manager.js +7 -6
  45. package/dist/http/admission.js +22 -7
  46. package/dist/http/memory-store-refusal-reply.d.ts +24 -0
  47. package/dist/http/memory-store-refusal-reply.js +10 -0
  48. package/dist/http/routes/approvals-assistant.js +1 -0
  49. package/dist/http/routes/capabilities.js +3 -3
  50. package/dist/http/routes/memory-bundle.js +3 -0
  51. package/dist/http/routes/memory-compliance.js +3 -5
  52. package/dist/http/routes/memory-origin.js +3 -5
  53. package/dist/http/routes/sessions.js +7 -6
  54. package/dist/http/routes/side-query.js +10 -4
  55. package/dist/instruction-source-notice.d.ts +97 -0
  56. package/dist/instruction-source-notice.js +78 -0
  57. package/dist/memory-layer-legacy-lock.d.ts +14 -0
  58. package/dist/memory-layer-legacy-lock.js +74 -0
  59. package/dist/memory-operator-faces.js +1 -17
  60. package/dist/memory-store-refusal.d.ts +37 -0
  61. package/dist/memory-store-refusal.js +14 -0
  62. package/dist/model-entry-refusal.d.ts +130 -0
  63. package/dist/model-entry-refusal.js +157 -0
  64. package/dist/model-provider.d.ts +9 -3
  65. package/dist/model-route-endpoint.d.ts +1 -1
  66. package/dist/model-select.d.ts +49 -4
  67. package/dist/model-select.js +48 -17
  68. package/dist/observability/fail-open.d.ts +9 -1
  69. package/dist/observability/fail-open.js +9 -1
  70. package/dist/observability/metrics.js +1 -0
  71. package/dist/observability/secret-env-scrub.d.ts +27 -4
  72. package/dist/observability/secret-env-scrub.js +22 -7
  73. package/dist/plugins/caching-session-store.d.ts +6 -0
  74. package/dist/plugins/caching-session-store.js +6 -0
  75. package/dist/plugins/local-session-store.d.ts +20 -25
  76. package/dist/plugins/local-session-store.js +111 -64
  77. package/dist/plugins/pg-session-storage.d.ts +7 -0
  78. package/dist/plugins/pg-session-storage.js +19 -0
  79. package/dist/plugins/remote-env-host.d.ts +3 -1
  80. package/dist/plugins/remote-env-host.js +3 -6
  81. package/dist/plugins/tidb-pool.js +5 -0
  82. package/dist/plugins/tidb-session-store.d.ts +13 -0
  83. package/dist/plugins/tidb-session-store.js +14 -0
  84. package/dist/plugins/web-search.d.ts +2 -1
  85. package/dist/plugins/web-search.js +43 -24
  86. package/dist/project-memory.d.ts +10 -0
  87. package/dist/project-memory.js +28 -14
  88. package/dist/run-local.d.ts +17 -15
  89. package/dist/run-local.js +115 -15
  90. package/dist/sealed-key.d.ts +5 -2
  91. package/dist/server-secret-env.d.ts +23 -1
  92. package/dist/server-secret-env.js +9 -0
  93. package/dist/tool-approval.d.ts +65 -31
  94. package/dist/tool-approval.js +96 -49
  95. package/dist/trace/core-keyset-guard.d.ts +27 -5
  96. package/dist/trace/engine-notice-wire.d.ts +27 -1
  97. package/dist/trace/engine-notice-wire.js +8 -0
  98. package/dist/trace/wire-projection-faces.d.ts +2 -2
  99. package/dist/trace/wire-projection-faces.js +2 -2
  100. package/package.json +2 -2
  101. package/dist/memory-layer-preflight.d.ts +0 -46
  102. package/dist/memory-layer-preflight.js +0 -121
@@ -1,4 +1,4 @@
1
- import { resolveActiveTiers, SEALED_BOX_ALG } from "@sema-agent/settings-schema";
1
+ import { resolveActiveTiers } from "@sema-agent/settings-schema";
2
2
  import { sealedKeyPoison } from "../sealed-key.js";
3
3
  import { applyAutoCompactWindow, buildModelCostKey, findUnmatchableToolNames, formatUnmatchableToolNames, parseAutonomy } from "../config.js";
4
4
  import { buildModelCompat, readModelCompat } from "../model-compat.js";
@@ -8,6 +8,7 @@ import { stageLimits } from "./stage-limits.js";
8
8
  import { APPROVAL_GROUP_KEYS, BATCH1_LIMIT_KEYS } from "./hot-keys-registry.js";
9
9
  import { buildDisplaySafeUrl } from "../trace/redact.js";
10
10
  import { crossInvariant } from "../config-invariants.js";
11
+ import { judgeCatalogEntries, mintedRefusedNames, refusalEventOf, refusalWarnFields, REFUSED_NAME_MINTED_CODE, unreadableDroppedRefusal } from "../model-entry-refusal.js";
11
12
  import { gateIntentUnserviceableReason } from "../approval.js";
12
13
  import { refusedGroups } from "./apply-ledger.js";
13
14
  const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
@@ -104,9 +105,10 @@ export function applyEffective(config, eff, logger, opts) {
104
105
  logger?.warn("sema_registry_stale_generation_refused", { staged: staged.version, stagedOrdinal: gen, live, note: "an older staged generation must not overwrite a newer committed one — refused whole; next poll replays" });
105
106
  return false;
106
107
  }
107
- if (staged.modelPlane !== undefined && opts.swapPlane !== undefined) {
108
+ const mp = staged.modelPlaneGroup.stages.models;
109
+ if (mp !== undefined && opts.swapPlane !== undefined) {
108
110
  try {
109
- opts.swapPlane({ models: staged.modelPlane.models, tiers: staged.modelPlane.activeTiers });
111
+ opts.swapPlane({ models: mp.models, tiers: mp.activeTiers });
110
112
  }
111
113
  catch (err) {
112
114
  logger?.warn("sema_registry_model_plane_swap_refused", {
@@ -131,19 +133,20 @@ export function applyEffective(config, eff, logger, opts) {
131
133
  ...(staged.limits.rejected ? { rejected: staged.limits.rejected } : {}),
132
134
  },
133
135
  approvalGate: groupReportOf(staged.approvalGate),
136
+ modelPlane: modelPlaneGroupReport(staged.modelPlaneGroup),
134
137
  },
135
138
  };
136
139
  opts.report({ ...report, fullyApplied: refusedGroups(report).length === 0 });
137
140
  }
138
141
  if (staged.limits.changed.length > 0)
139
142
  logger?.info("sema_registry_limits", { version: staged.version, keys: staged.limits.changed });
140
- if (staged.modelPlane)
141
- logger?.info("sema_registry_models", staged.modelPlane.infoLine);
143
+ if (mp)
144
+ logger?.info("sema_registry_models", mp.infoLine);
142
145
  if (staged.approvalGate.next !== undefined)
143
146
  logger?.info("sema_registry_runtime", { version: staged.version, approvalRequire: staged.approvalGate.next });
144
147
  if (Object.keys(staged.hot.infoLine).length > 0)
145
148
  logger?.info("sema_registry_runtime_hot", staged.hot.infoLine);
146
- for (const n of staged.collabPlane.notices) {
149
+ for (const n of staged.modelPlaneGroup.stages.collab?.notices ?? []) {
147
150
  if (n.level === "warn")
148
151
  logger?.warn(n.msg, n.fields);
149
152
  else
@@ -154,10 +157,11 @@ export function applyEffective(config, eff, logger, opts) {
154
157
  return true;
155
158
  }
156
159
  function commitStaged(config, staged) {
157
- const mp = staged.modelPlane;
160
+ const mp = staged.modelPlaneGroup.stages.models;
158
161
  if (mp) {
159
162
  config.modelApiKeyEnv = mp.modelApiKeyEnv;
160
163
  config.modelApiKeys = mp.modelApiKeys;
164
+ config.modelRefusals = mp.modelRefusals;
161
165
  mutateInPlace(config.modelQuotaWeights, mp.quotaWeights);
162
166
  mutateInPlace(config.models, mp.models);
163
167
  config.model = config.models.default;
@@ -175,61 +179,90 @@ function commitStaged(config, staged) {
175
179
  config.autonomy = staged.hot.autonomyNext;
176
180
  if (staged.hot.setCommandPolicy)
177
181
  config.commandPolicy = staged.hot.commandPolicyNext;
178
- registerCollabWorkflows(staged.collabPlane.workflows);
182
+ const collab = staged.modelPlaneGroup.stages.collab;
183
+ if (collab !== undefined)
184
+ registerCollabWorkflows(collab.workflows);
179
185
  mutateInPlace(config.projects, staged.projects);
180
186
  }
181
- function stageEffective(config, eff, logger, opts) {
182
- if (opts.leg === "boot" && !eff.version) {
183
- logger?.warn("sema_registry_unpublished", {
184
- version: eff.version,
185
- note: "empty/unpublished effective config — using env + built-in fallback; publish a config or unset CONFIG_PUBLISH_MODE",
186
- });
187
- }
187
+ export const MODEL_PLANE_DOMAINS = ["models", "collab"];
188
+ const MODEL_PLANE_SURFACES = {
189
+ models: ["models", "model", "modelRefusals", "modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "roles", "tiers", "atModelAllowlist"],
190
+ collab: ["collabWorkflows"],
191
+ };
192
+ const MODEL_PLANE_GROUP_KEYS = MODEL_PLANE_DOMAINS.flatMap((d) => MODEL_PLANE_SURFACES[d]);
193
+ export function withModelPlaneOf(target, source) {
194
+ const out = { ...target };
195
+ for (const d of MODEL_PLANE_DOMAINS)
196
+ takeModelPlaneDomain(out, source, d);
197
+ return out;
198
+ }
199
+ function takeModelPlaneDomain(out, source, d) {
200
+ const v = source[d];
201
+ if (v === undefined)
202
+ delete out[d];
203
+ else
204
+ out[d] = v;
205
+ }
206
+ export function buildEnvOnlyModelPlane() {
207
+ return { models: { models: [], roles: {} } };
208
+ }
209
+ function modelPlaneGroupReport(group) {
210
+ return {
211
+ applied: MODEL_PLANE_DOMAINS.flatMap((d) => (group.stages[d] !== undefined ? MODEL_PLANE_SURFACES[d] : [])),
212
+ ...(group.rejected !== undefined ? { rejected: { keys: group.rejected.keys, reason: group.rejected.reason } } : {}),
213
+ };
214
+ }
215
+ const ROLE_BINDING_UNAVAILABLE_HINT = "tasks resolving this role fail: alias/tier expansion cannot mint this name; the engine refuses it loud — bind the role to a usable model, or make the entry usable (see the entry's own warn)";
216
+ function stageModelPlane(config, eff, logger, sealedKeys) {
188
217
  const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
189
- let modelPlane;
190
- if (enabled.length > 0) {
218
+ const droppedEnabled = eff.models?.droppedIdentities;
219
+ if (enabled.length === 0 && !(Array.isArray(droppedEnabled) && droppedEnabled.length > 0))
220
+ return { stages: { collab: stageCollabDomain(eff) } };
221
+ {
222
+ const verdict = judgeCatalogEntries(enabled, { anthropicRouteMounted: config.anthropic !== undefined }, eff.models.droppedIdentities);
223
+ for (const [name, r] of Object.entries(verdict.refused))
224
+ logger?.warn(refusalEventOf(r.reason), refusalWarnFields(name, r));
225
+ const refuse = (invariant, reason, detail = {}) => {
226
+ logger?.warn("sema_registry_model_plane_refused", {
227
+ version: typeof eff.version === "number" ? eff.version : 0,
228
+ invariant,
229
+ reason,
230
+ ...detail,
231
+ kept: "the model plane of this candidate is refused as a group — the current model plane (catalog / roles / tiers / refusals / allowlist, and the collab workflows that reference it) keeps serving and the other groups of this candidate still apply; the candidate is not fully applied (LKG / ETag stay on the last fully applied generation) and is re-judged next pull",
232
+ });
233
+ return { stages: {}, rejected: { keys: MODEL_PLANE_GROUP_KEYS, reason, invariant }, refused: verdict.refused };
234
+ };
235
+ if (verdict.unreadable.length > 0) {
236
+ logger?.warn("sema_registry_model_entry_dropped_unproven", {
237
+ entries: [...verdict.unreadable],
238
+ code: REFUSED_NAME_MINTED_CODE,
239
+ hint: "these catalog entries were dropped by the settings-schema read and the raw document could not be matched to the snapshot that was parsed (e.g. the local file changed between two reads) — their names cannot be proven, so no tombstone can be minted and the candidate's model plane is refused (a name nobody can prove cannot be shown not to be re-minted by the tier/alias expansion); the next read re-judges",
240
+ });
241
+ return refuse("refused-names-not-minted", unreadableDroppedRefusal(verdict.unreadable));
242
+ }
243
+ const usable = verdict.usable;
191
244
  const models = {};
192
245
  const modelApiKeyEnv = {};
193
246
  const modelApiKeys = {};
194
247
  const envModel = envDerivedModel(config);
195
248
  const envDefaults = { maxTokens: envModel.maxTokens, contextWindow: envModel.contextWindow, reasoning: envModel.reasoning, charsPerToken: envModel.charsPerToken, defaultThinking: envModel.defaultThinking, compat: envModel.compat };
196
- for (const m of enabled) {
197
- const rawBaseUrl = m.baseUrl;
198
- let safeUrl = rawBaseUrl;
199
- if (rawBaseUrl) {
200
- let ok = false;
201
- try {
202
- const u = new URL(rawBaseUrl);
203
- ok = u.protocol === "http:" || u.protocol === "https:";
204
- }
205
- catch {
206
- ok = false;
207
- }
208
- if (ok)
209
- logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: buildDisplaySafeUrl(rawBaseUrl), hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
210
- else {
211
- logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: buildDisplaySafeUrl(String(rawBaseUrl)), hint: "only http(s) URLs are honored — falling back to the deployment gateway (MODEL_GATEWAY_BASEURL); with none declared the whole candidate is refused (model-route-endpoint)" });
212
- safeUrl = undefined;
213
- }
214
- }
215
- models[m.name] = toModel(safeUrl === rawBaseUrl ? m : { ...m, baseUrl: safeUrl }, envDefaults, logger);
249
+ for (const m of usable) {
250
+ if (m.baseUrl)
251
+ logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: buildDisplaySafeUrl(m.baseUrl), hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
252
+ models[m.name] = toModel(m, envDefaults, logger);
216
253
  if (m.sealedApiKey) {
217
254
  const s = m.sealedApiKey;
218
- if (s.alg !== SEALED_BOX_ALG) {
219
- logger?.warn("sema_registry_model_sealed_key_bad_alg", { model: m.name, alg: String(s.alg), expected: SEALED_BOX_ALG, hint: "unsupported sealed-box alg — key POISONED: tasks on this model fail loud (no apiKeyEnv fallback, no gateway fallback; contract v1 pins the alg)" });
220
- modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "unsupported_alg");
221
- }
222
- else if (!opts.sealedKeys) {
255
+ if (!sealedKeys) {
223
256
  logger?.warn("sema_registry_model_sealed_key_no_store", { model: m.name, publicKeyId: s.publicKeyId, hint: "no sealed-key store on this host (boot custody init failed or lane unwired) — key POISONED: tasks on this model fail loud (no apiKeyEnv fallback, no gateway fallback)" });
224
257
  modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "no_key_store");
225
258
  }
226
- else if (!opts.sealedKeys.has(s.publicKeyId)) {
227
- logger?.warn("sema_registry_model_sealed_key_no_private_key", { model: m.name, publicKeyId: s.publicKeyId, held: opts.sealedKeys.ids(), hint: "ciphertext targets a private key this host doesn't hold (rotated away / other host) — key POISONED: tasks on this model fail loud until the operator re-pastes the key in the web UI (it seals against the newest registered public key)" });
259
+ else if (!sealedKeys.has(s.publicKeyId)) {
260
+ logger?.warn("sema_registry_model_sealed_key_no_private_key", { model: m.name, publicKeyId: s.publicKeyId, held: sealedKeys.ids(), hint: "ciphertext targets a private key this host doesn't hold (rotated away / other host) — key POISONED: tasks on this model fail loud until the operator re-pastes the key in the web UI (it seals against the newest registered public key)" });
228
261
  modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "stale_ciphertext");
229
262
  }
230
263
  else {
231
264
  try {
232
- modelApiKeys[m.name] = opts.sealedKeys.open(s.ciphertext, s.publicKeyId);
265
+ modelApiKeys[m.name] = sealedKeys.open(s.ciphertext, s.publicKeyId);
233
266
  }
234
267
  catch (err) {
235
268
  logger?.warn("sema_registry_model_sealed_key_open_failed", { model: m.name, publicKeyId: s.publicKeyId, err: String(err), hint: "corrupt/foreign ciphertext — key POISONED: tasks on this model fail loud (no apiKeyEnv fallback, no gateway fallback)" });
@@ -243,12 +276,9 @@ function stageEffective(config, eff, logger, opts) {
243
276
  else
244
277
  logger?.warn("sema_registry_model_key_env_missing", { model: m.name, apiKeyEnv: m.apiKeyEnv, hint: "env var not set in this service — model falls back to the gateway key" });
245
278
  }
246
- if (m.provider === "anthropic" && !config.anthropic) {
247
- logger?.warn("sema_registry_model_no_brain", { model: m.name, provider: m.provider, hint: "set ANTHROPIC_API_KEY in this service's env, else it mis-routes to the gateway brain" });
248
- }
249
279
  }
250
280
  const quotaWeights = {};
251
- for (const m of enabled) {
281
+ for (const m of usable) {
252
282
  const qw = typeof m.quotaWeight === "number" && Number.isFinite(m.quotaWeight) && m.quotaWeight > 0 ? m.quotaWeight : undefined;
253
283
  if (qw !== undefined) {
254
284
  quotaWeights[m.name] = qw;
@@ -266,38 +296,80 @@ function stageEffective(config, eff, logger, opts) {
266
296
  centerRoles[role] = { select: tgt.select };
267
297
  }
268
298
  const roles = { ...envRolesBaseline.get(config), ...centerRoles };
269
- const activeTiers = resolveActiveTiers(eff.models) ?? {};
270
- const picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, enabled[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", { source, name, hint: "explicit default names a model that is not in the enabled catalog — falling to the next source" }));
271
- models.default = models[picked.name];
272
- const endpointRefusal = crossInvariant("model-route-endpoint").check({ live: config, next: { ...config, models } });
273
- if (endpointRefusal !== undefined) {
274
- throw new Error(`candidate model plane refused whole (cross-invariant model-route-endpoint): ${endpointRefusal} ` +
275
- "The current generation keeps serving; the candidate is re-judged on the next pull.");
299
+ let picked;
300
+ if (usable.length > 0) {
301
+ picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, usable[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", {
302
+ source,
303
+ name,
304
+ ...(Object.hasOwn(verdict.refused, name) ? { unavailable: verdict.refused[name].code } : {}),
305
+ hint: "explicit default names a model that is not in the usable catalog (not enabled, or unavailable on this host — see `unavailable`) — falling to the next source",
306
+ }));
307
+ models.default = models[picked.name];
308
+ }
309
+ const activeTiers = {};
310
+ for (const [tier, target] of Object.entries(resolveActiveTiers(eff.models) ?? {})) {
311
+ if (models[target] !== undefined) {
312
+ activeTiers[tier] = target;
313
+ continue;
314
+ }
315
+ const refusal = Object.hasOwn(verdict.refused, target) ? verdict.refused[target] : undefined;
316
+ logger?.warn("sema_registry_tier_binding_unavailable", { tier, model: target, ...(refusal !== undefined ? { code: refusal.code } : {}), hint: "this tier binding names a model that is not in this generation's usable catalog — the binding is dropped (the tier word falls to the next lower bound tier, core's ladder); fix the entry or rebind the tier" });
317
+ }
318
+ const nextView = { ...config, models, tiers: activeTiers, modelRefusals: verdict.refused };
319
+ const noDefault = crossInvariant("default-model-resolvable").check({ live: config, next: nextView });
320
+ if (noDefault !== undefined)
321
+ return refuse("default-model-resolvable", noDefault);
322
+ const endpointRefusal = crossInvariant("model-route-endpoint").check({ live: config, next: nextView });
323
+ if (endpointRefusal !== undefined)
324
+ return refuse("model-route-endpoint", endpointRefusal);
325
+ const minted = crossInvariant("refused-names-not-minted").check({ live: config, next: nextView });
326
+ if (minted !== undefined)
327
+ return refuse("refused-names-not-minted", minted, { minted: mintedRefusedNames(nextView) });
328
+ for (const [role, spec] of Object.entries(roles)) {
329
+ const target = typeof spec === "string" ? spec : spec !== null && typeof spec === "object" && "model" in spec && typeof spec.model === "string" ? spec.model : undefined;
330
+ const refusal = target !== undefined && Object.hasOwn(verdict.refused, target) ? verdict.refused[target] : undefined;
331
+ if (refusal !== undefined) {
332
+ logger?.warn("sema_registry_role_binding_unavailable", { role, model: target, code: refusal.code, hint: ROLE_BINDING_UNAVAILABLE_HINT });
333
+ }
276
334
  }
277
335
  const atModelAllowlist = stageAtModelAllowlist(eff.models?.atModelAllowlist, models, logger);
278
- modelPlane = {
336
+ const defaultModel = models.default;
337
+ const plane = {
279
338
  models,
280
339
  modelApiKeyEnv,
281
340
  modelApiKeys,
341
+ modelRefusals: { ...verdict.refused },
282
342
  quotaWeights,
283
343
  roles,
284
344
  activeTiers,
285
345
  atModelAllowlist,
286
- infoLine: { count: enabled.length, default: models.default.id, defaultSource: picked.source, roles: Object.keys(roles), tiers: Object.keys(activeTiers), atModelAllowlist: atModelAllowlist.length, sealedKeys: Object.values(modelApiKeys).filter((v) => typeof v === "string").length, sealedPoisoned: Object.values(modelApiKeys).filter((v) => typeof v !== "string").length },
346
+ infoLine: { count: usable.length, ...(Object.keys(verdict.refused).length > 0 ? { unavailable: Object.keys(verdict.refused) } : {}), default: defaultModel.id, defaultSource: picked.source, roles: Object.keys(roles), tiers: Object.keys(activeTiers), atModelAllowlist: atModelAllowlist.length, sealedKeys: Object.values(modelApiKeys).filter((v) => typeof v === "string").length, sealedPoisoned: Object.values(modelApiKeys).filter((v) => typeof v !== "string").length },
287
347
  };
348
+ return { stages: { models: plane, collab: stageCollabDomain(eff) }, defaultName: picked.name, refused: verdict.refused };
288
349
  }
289
- const approvalGate = stageApprovalGate(config, eff.governance, opts, logger);
290
- const limits = stageLimits(config, { version: typeof eff.version === "number" ? eff.version : 0, ...("limits" in eff ? { limits: eff.limits } : {}), legacyGates: legacyGateValues(eff.runtime) }, logger);
291
- const hot = stageRuntimeHot(config, eff.governance, logger);
350
+ }
351
+ function stageCollabDomain(eff) {
292
352
  const projected = projectCollabToWorkflows(eff.collab?.templates);
293
- const collabNotices = [];
353
+ const notices = [];
294
354
  if (Object.keys(projected.workflows).length > 0)
295
- collabNotices.push({ level: "info", msg: "sema_registry_collab_workflows", fields: { workflows: Object.keys(projected.workflows) } });
355
+ notices.push({ level: "info", msg: "sema_registry_collab_workflows", fields: { workflows: Object.keys(projected.workflows) } });
296
356
  if (projected.skipped.length > 0)
297
- collabNotices.push({ level: "warn", msg: "sema_registry_collab_skipped", fields: { skipped: projected.skipped } });
357
+ notices.push({ level: "warn", msg: "sema_registry_collab_skipped", fields: { skipped: projected.skipped } });
298
358
  if (projected.notes.length > 0)
299
- collabNotices.push({ level: "info", msg: "sema_registry_collab_notes", fields: { notes: projected.notes } });
300
- const collabPlane = { workflows: projected.workflows, notices: collabNotices };
359
+ notices.push({ level: "info", msg: "sema_registry_collab_notes", fields: { notes: projected.notes } });
360
+ return { workflows: projected.workflows, notices };
361
+ }
362
+ function stageEffective(config, eff, logger, opts) {
363
+ if (opts.leg === "boot" && !eff.version) {
364
+ logger?.warn("sema_registry_unpublished", {
365
+ version: eff.version,
366
+ note: "empty/unpublished effective config — using env + built-in fallback; publish a config or unset CONFIG_PUBLISH_MODE",
367
+ });
368
+ }
369
+ const modelPlaneGroup = stageModelPlane(config, eff, logger, opts.sealedKeys);
370
+ const approvalGate = stageApprovalGate(config, eff.governance, opts, logger);
371
+ const limits = stageLimits(config, { version: typeof eff.version === "number" ? eff.version : 0, ...("limits" in eff ? { limits: eff.limits } : {}), legacyGates: legacyGateValues(eff.runtime) }, logger);
372
+ const hot = stageRuntimeHot(config, eff.governance, logger);
301
373
  const projDomain = eff.projects;
302
374
  const rawProjects = (projDomain && typeof projDomain === "object" && !Array.isArray(projDomain) &&
303
375
  "projects" in projDomain && typeof projDomain.projects === "object"
@@ -316,11 +388,10 @@ function stageEffective(config, eff, logger, opts) {
316
388
  }
317
389
  return {
318
390
  version: typeof eff.version === "number" && Number.isFinite(eff.version) ? eff.version : 0,
319
- ...(modelPlane !== undefined ? { modelPlane } : {}),
391
+ modelPlaneGroup,
320
392
  approvalGate,
321
393
  limits,
322
394
  hot,
323
- collabPlane,
324
395
  projects,
325
396
  };
326
397
  }
@@ -522,6 +593,8 @@ export function resolveDefaultModelName(eff, has, fallback, onDangling) {
522
593
  }
523
594
  export function logEffectiveDiff(config, eff, logger) {
524
595
  const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
596
+ const plane = stageModelPlane(config, eff, undefined, undefined);
597
+ const refused = plane.refused ?? {};
525
598
  const centerModels = enabled.map((m) => ({ name: m.name, id: m.id, provider: m.provider, apiKeyEnv: m.apiKeyEnv ?? null, tier: m.tier ?? null }));
526
599
  const envModels = Object.entries(config.models).map(([name, m]) => ({ name, id: m.id, provider: m.provider }));
527
600
  const centerRoles = Object.fromEntries(Object.entries(eff.models?.roles ?? {}).map(([r, t]) => [r, "model" in t ? t.model : { select: t.select }]));
@@ -529,7 +602,9 @@ export function logEffectiveDiff(config, eff, logger) {
529
602
  note: "DRY RUN — registry config NOT applied; unset SEMA_REGISTRY_DRY_RUN to go live",
530
603
  version: eff.version,
531
604
  wouldOverrideModels: enabled.length > 0,
532
- wouldDefaultModel: enabled.length > 0 ? resolveDefaultModelName(eff, (n) => enabled.some((m) => m.name === n), enabled[0].name).name : null,
605
+ wouldDefaultModel: plane.defaultName ?? null,
606
+ ...(Object.keys(refused).length > 0 ? { wouldRefuse: Object.fromEntries(Object.entries(refused).map(([n, r]) => [n, r.code])) } : {}),
607
+ ...(plane.rejected !== undefined ? { wouldRefuseModelPlane: { invariant: plane.rejected.invariant, reason: plane.rejected.reason } } : {}),
533
608
  currentDefaultModel: config.model.id,
534
609
  models: { center: centerModels, envDerived: envModels },
535
610
  roles: { center: centerRoles, current: config.roles },
@@ -25,9 +25,13 @@
25
25
  * · `limitsHttp` —— [ref] 批1 限额组(属主 `stage-limits.ts`);
26
26
  * · `approvalGate` —— S-668 波 2 审批组(`governance.approvalRequire`;属主 `apply-effective.ts` 的
27
27
  * `stageApprovalGate`:坏形 / 工具名匹配不到 / ④ 门意图不可服务 ⇒ 整组拒)。
28
- * 两组同姿势、各用各的判据表(审批组**不**复用限额组的跨键不变量表 —— 那张表的候选视图只叠限额键)。
28
+ * · `modelPlane` —— 模型面组(组内有哪些 effective 域的**唯一定义** = `apply-effective.ts` 的 `MODEL_PLANE_DOMAINS`:models 域 =
29
+ * 目录 / roles / tiers / 拒因表 / 名单 / 钥匙表,collab 域 = 引用它的命名 workflow 注册表;属主 `stageModelPlane`:
30
+ * 模型面三条跨键不变量 `default-model-resolvable` / `model-route-endpoint` / `refused-names-not-minted` 任一不成立 ⇒ 整组拒,
31
+ * 组内各域都停在上一代。opus 腿 O3(7.101.0 合并复审):此前这三条是 stage 抛 = 整份候选中止,同候选其余组陪葬)。
32
+ * 各组同姿势、各用各的判据表(审批组**不**复用限额组的跨键不变量表 —— 那张表的候选视图只叠限额键)。
29
33
  */
30
- export declare const CONFIG_APPLY_GROUPS: readonly ["limitsHttp", "approvalGate"];
34
+ export declare const CONFIG_APPLY_GROUPS: readonly ["limitsHttp", "approvalGate", "modelPlane"];
31
35
  export type ConfigApplyGroup = (typeof CONFIG_APPLY_GROUPS)[number];
32
36
  /** 一组的收敛状态(读面形对所有组相同)。 */
33
37
  export interface ConfigApplyGroupState {
@@ -88,6 +92,12 @@ export declare function refusedGroups(report: ConfigApplyReport): Array<{
88
92
  readonly keys: readonly string[];
89
93
  readonly reason: string;
90
94
  }>;
95
+ /**
96
+ * O3:这一次 apply 的**模型面组**(组内各域 = `MODEL_PLANE_DOMAINS`,目录与引用它的 collab 模板同进退)落地了没有(整组被拒 ⇒
97
+ * false;报告缺席 = 没走到 commit ⇒ false,如实)。调用方据此决定「Runner 现在跑的是哪一代模型面」(`appliedPlaneEff`,swap 判据的
98
+ * 基准)、名册门开不开、以及 boot 要不要按组回落 LKG —— 都只许从真落地的模型面推进。
99
+ */
100
+ export declare function modelPlaneCommitted(report: ConfigApplyReport | undefined): boolean;
91
101
  export interface ConfigApplyLedger {
92
102
  /** 拉到一个候选(无论后续是否应用)。ordinal=收货序数,version=身份标。 */
93
103
  seenTarget(ordinal: number, version: number): void;
@@ -1,10 +1,13 @@
1
- export const CONFIG_APPLY_GROUPS = ["limitsHttp", "approvalGate"];
1
+ export const CONFIG_APPLY_GROUPS = ["limitsHttp", "approvalGate", "modelPlane"];
2
2
  export function refusedGroups(report) {
3
3
  return CONFIG_APPLY_GROUPS.flatMap((group) => {
4
4
  const r = report.groups[group].rejected;
5
5
  return r ? [{ group, keys: r.keys, reason: r.reason }] : [];
6
6
  });
7
7
  }
8
+ export function modelPlaneCommitted(report) {
9
+ return report !== undefined && report.groups.modelPlane.rejected === undefined;
10
+ }
8
11
  export function createConfigApplyLedger(now = () => Date.now()) {
9
12
  let targetOrd = 0;
10
13
  let appliedOrd = 0;
@@ -48,7 +48,7 @@
48
48
  * importers to update, which was done in the same commit.
49
49
  */
50
50
  export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
51
- export { mutateInPlace, applyEffective, type ApplyReport, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, } from "./apply-effective.js";
51
+ export { mutateInPlace, applyEffective, type ApplyReport, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, MODEL_PLANE_DOMAINS, type ModelPlaneDomain, type ModelPlaneOf, withModelPlaneOf, buildEnvOnlyModelPlane, } from "./apply-effective.js";
52
52
  export { restartReasons, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./restart-signal.js";
53
53
  export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
54
54
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS, type CenterReadFaceApply, type CenterReadFaceReport, type ReadFaceBaseline } from "./read-face.js";
@@ -1,5 +1,5 @@
1
1
  export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
2
- export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, } from "./apply-effective.js";
2
+ export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, MODEL_PLANE_DOMAINS, withModelPlaneOf, buildEnvOnlyModelPlane, } from "./apply-effective.js";
3
3
  export { restartReasons, modelPlaneChanged, } from "./restart-signal.js";
4
4
  export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
5
5
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS } from "./read-face.js";
@@ -1,11 +1,13 @@
1
1
  import { type EffectiveReadWarning, type EntitlementRuntimeCaps } from "@sema-agent/settings-schema";
2
2
  import type { ScenarioRuling } from "../capabilities/scenarios.js";
3
3
  import type { EffectiveConfig, ExecutionRuling } from "./types.js";
4
+ import { type DroppedConfigEntry } from "./read-warnings.js";
4
5
  import { type CenterEndpoint } from "./center-request.js";
5
6
  /** GET the effective config (Bearer + ETag). null = 304 (unchanged). Throws on transport/HTTP error, and on a
6
7
  * payload that is not an effective config at all(非对象 / version 非有限数 / gate 域坏形——[ref] 裁B)。
7
8
  * `domainErrors`:坏 catalog 域(schema default 已落)逐域单列——与本地腿 `FetchEffectiveResult` 同键同义,
8
- * boot/refresh 的候选门直接消费。
9
+ * boot/refresh 的候选门直接消费。`droppedEntries`(S-704 ②):schema 逐条丢掉的条目(坐标 + 字段诊断)——
10
+ * **不**进候选门,调用方响亮点名、其余照用(分面单点 `splitFoldedReadWarnings`,与本地腿同一只)。
9
11
  *
10
12
  * `readWarnings`:契约 `onWarning` 流的**原值**(未折叠),与本地腿同键同义。折叠是**有损**的——
11
13
  * `hosts-grandfathered` / `legacy-governance-lifted` 两员按律折成 `null`(值已被收编接受,见
@@ -24,6 +26,7 @@ export declare function fetchEffective(center: CenterEndpoint, etag: string | un
24
26
  domain: string;
25
27
  error: string;
26
28
  }>;
29
+ droppedEntries?: DroppedConfigEntry[];
27
30
  readWarnings: readonly EffectiveReadWarning[];
28
31
  credentialIdentity: string;
29
32
  } | null>;
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readEffectiveWire, PrincipalCapsWire } from "@sema-agent/settings-schema";
3
3
  import { centerPromptsFromEffective } from "../prompts-domain-validate.js";
4
- import { foldEffectiveReadWarning } from "./read-warnings.js";
4
+ import { splitFoldedReadWarnings, withDroppedIdentities } from "./read-warnings.js";
5
5
  import { centerFetch } from "./center-request.js";
6
6
  import { answerStillCurrent } from "../center-credential.js";
7
7
  const CENTER_PULL_TIMEOUT_MS = 8000;
@@ -12,20 +12,25 @@ export async function fetchEffective(center, etag, fetchImpl = fetch, worker) {
12
12
  if (!res.ok)
13
13
  throw new Error(`config-center HTTP ${res.status}`);
14
14
  const warnings = [];
15
- const wire = readEffectiveWire(await res.json(), (w) => warnings.push(w));
15
+ const body = await res.json();
16
+ const wire = readEffectiveWire(body, (w) => warnings.push(w));
16
17
  const centerPrompts = centerPromptsFromEffective(wire);
17
18
  if (centerPrompts === undefined)
18
19
  delete wire.prompts;
19
20
  else
20
21
  wire.prompts = centerPrompts;
21
- const domainErrors = [];
22
- for (const w of warnings) {
23
- const f = foldEffectiveReadWarning(w);
24
- if (f !== null)
25
- domainErrors.push(f);
26
- }
27
- const effective = wire;
28
- return { effective, etag: res.headers.get("etag") ?? undefined, readWarnings: warnings, credentialIdentity: identity, ...(domainErrors.length > 0 ? { domainErrors } : {}) };
22
+ const rawModels = typeof body === "object" && body !== null && !Array.isArray(body) ? body.models : undefined;
23
+ const parsedModels = wire.models?.models;
24
+ const { domainErrors, droppedEntries, droppedIdentities } = splitFoldedReadWarnings(warnings, { raw: rawModels, parsed: Array.isArray(parsedModels) ? parsedModels : undefined });
25
+ const effective = withDroppedIdentities(wire, droppedIdentities);
26
+ return {
27
+ effective,
28
+ etag: res.headers.get("etag") ?? undefined,
29
+ readWarnings: warnings,
30
+ credentialIdentity: identity,
31
+ ...(domainErrors.length > 0 ? { domainErrors } : {}),
32
+ ...(droppedEntries.length > 0 ? { droppedEntries } : {}),
33
+ };
29
34
  }
30
35
  export async function fetchPrincipalCaps(center, principal, etag, fetchImpl = fetch, worker) {
31
36
  const raw = await requestPrincipalView(center, principal, etag, fetchImpl, worker);
@@ -10,20 +10,129 @@
10
10
  * 截断:schema 的键列表有界(64),超界带 `truncatedFrom`(真实总数)——文案如实带上,消费方永远不把有界列表当穷举。
11
11
  * 词表是闭集:`switch` 穷举 `kind`,schema 加新 kind 这里编译红,不会静默漏臂。
12
12
  *
13
- * **归族只有一条判据**(下游只有一个消费者——候选门:非空 ⇒ 这棵树整拒、保 LKG):
14
- * 运维**写了而没生效**的 ⇒ 候选门错误;值**已被收编接受**(写了、生效了、只是换了个位置或被夹住)⇒ 不入诊断面。
15
- * settings-schema 2.0.0 的两员新告警各自入族,没有新开第三档:`entry-dropped`(逐条容错丢掉的那条条目——
16
- * 运维写下的模型/服务器没进目录,与 `domain-defaulted` 是同一件事的细/粗两档)归前者;
17
- * `legacy-governance-lifted`(0.5 期 runtime 文档里的治理三闸被抬进 governance 域)归后者——那几个键**生效了**
13
+ * **归族判据两问**(下游两个消费者:候选门 = 非空 ⇒ 这棵树整拒、保 LKG;逐条丢弃面 = 响亮点名、其余照用):
14
+ * ① 值**已被收编接受**(写了、生效了、只是换了个位置或被夹住)⇒ 不入诊断面(`null`);
15
+ * ② 写了没生效的,按**失效的颗粒**分:丢的是**一条条目**(`entry-dropped`)⇒ `scope:"entry"` —— 这条条目单独
16
+ * 不在,同域其余条目照用,候选**不**因它整拒;丢的是一个域 / 一组键 / 一个文件 ⇒ `scope:"domain"` —— 候选门错误。
17
+ *
18
+ * 🔴 S-704 ②(2026-09-24,凭据轴):此前 `entry-dropped` 归「候选门错误」(settings-schema 2.0.0 提货时的归族),于是
19
+ * schema 的**逐条容错**在本仓的 refresh / 迟到 boot 两条腿上被**还原成整份拒** —— 目录里一条 `contextWindow:"abc"`
20
+ * 就让整份目录停在上一代;而上一代若还是 env(boot 拉取失败 / 超窗且无 LKG),env 模型就一直服务、env 钥匙一直被送到
21
+ * 网关缺省端点(红先实测:`config_candidate_rejected` + env 模型继续服务)。粒度对了规则反而更少:颗粒是条目的失效
22
+ * 留在条目上,只有颗粒更粗的失效才动候选。
23
+ * `legacy-governance-lifted`(0.5 期 runtime 文档里的治理三闸被抬进 governance 域)归①——那几个键**生效了**
18
24
  * (`buildEffective` 用抬升结果解析 governance),把它折成错误会让一份合法的 0.5 期存量配置每一次 refresh 都被
19
25
  * 候选门整拒,而它在 1.10.0 上零告警零 domainErrors(亲验读数)。兼容臂的关窗观测归属主(settings-schema 的
20
26
  * `onWarning` 流),不借候选门这条带副作用的通道传信息。
21
27
  */
22
- import { type EffectiveReadWarning } from "@sema-agent/settings-schema";
28
+ import { type ConfigPath, type EffectiveReadWarning } from "@sema-agent/settings-schema";
29
+ /** 折叠产物:`scope` 决定消费方做什么 —— `"domain"` 进候选门(`domainErrors`),`"entry"` 进逐条丢弃面(`droppedEntries`)。 */
23
30
  export type FoldedReadWarning = {
31
+ readonly scope: "domain";
32
+ readonly domain: string;
33
+ readonly error: string;
34
+ } | {
35
+ readonly scope: "entry";
36
+ readonly domain: string;
37
+ readonly entry: string;
38
+ readonly error: string;
39
+ };
40
+ /** 逐条丢弃面的一行(两条读腿的 `droppedEntries` 同形):`entry` = 被丢那条的出门形坐标(`models.1`),`error` = 哪些字段、为什么。 */
41
+ export type DroppedConfigEntry = {
24
42
  readonly domain: string;
43
+ readonly entry: string;
25
44
  readonly error: string;
26
45
  };
46
+ /**
47
+ * zod 形的错误 → 一行可读的字段诊断(`contextWindow: expected number, received string; api: custom`)。
48
+ * **只读**每条 issue 的 `path`(键名)与 `code`,`invalid_type` 臂另读 `expected` / `received`(两者都是**类型词**)——
49
+ * 刻意不读 `message`(自定义 refine 的 message 可以回声值)与 `invalid_enum_value` 等臂的 `received`(那是原值)。
50
+ * 读不出 issues 形 ⇒ `undefined`(调用方退回既有文案)。模型面 stage 的逐条判据与本折叠点共用这一只(一个刻度)。
51
+ */
52
+ export declare function describeSchemaIssues(error: unknown): string | undefined;
53
+ /**
54
+ * zod 形错误 → 坏字段的**键路径**(去重保序;`(entry)` = 整条)。与 {@link describeSchemaIssues} 同一只读法(只读 `path`),
55
+ * 不含值。模型面 stage 的逐条判据(`judgeCatalogEntry`)与本折叠点的被丢条目身份共用这一只 —— 一个刻度。
56
+ */
57
+ export declare function describeSchemaIssueFields(error: unknown): string[];
27
58
  /** null = 不入诊断面(`hosts-grandfathered` / `legacy-governance-lifted`:值已被收编接受,与本地店同口径)。 */
28
59
  export declare function foldEffectiveReadWarning(w: EffectiveReadWarning): FoldedReadWarning | null;
60
+ /**
61
+ * 🔴 S-712 —— 一条被 schema 逐条丢掉的**启用**目录条目的身份(模型面墓碑的来源;载体 = `EffectiveConfig.models.droppedIdentities`,
62
+ * 随 effective 走 —— 盘上 LKG 从同一个对象落盘,重启从 LKG 起服墓碑不丢)。
63
+ *
64
+ * 为什么要交出身份:条目被丢之后它的名字从目录里消失,而档位增广(core `expandTiers`)会把一个**空出来的**档位词 / CC 别名
65
+ * (条目恰好叫 `sonnet` / `pro` …)重新铸到另一条条目上 —— 显式点它的请求与点名它的角色绑定就这样被静默换了目的地
66
+ * (gpt-6 腿 F2 / F3,真复现)。身份进 `modelRefusals` 之后,不变量 `refused-names-not-minted` 才判得到它。
67
+ *
68
+ * 🔐 纯身份,不含值:`name` / `id` / `provider` 只取**字符串**(条目自己写的词,本就进日志与拒因句),`fields` 是坏字段的
69
+ * **键路径**,`issues` 是 {@link describeSchemaIssues} 的一行(键名 + 类型词)—— 被丢条目的原值(可能是一段贴错位置的凭据)
70
+ * 绝不进载体、日志、LKG 与 400 拒因句。
71
+ */
72
+ export interface DroppedModelIdentity {
73
+ /** 出门形坐标(`models.1`),与同一次读的 `droppedEntries[].entry` 同一串。 */
74
+ readonly entry: string;
75
+ /** 条目写的 `name`。**缺席 = 身份证明不了**(只剩一形:原文档与本次解析对不上 / 坐标在原文档里不存在,见 {@link droppedEntryIdentity})
76
+ * ⇒ 模型面整组拒:证明不了的名字无从证明它不被增广铸出。原文档那一格读到了却没有字符串名的行**不进**载体(没有可保留的名字,
77
+ * 与停用同处置 —— 7.101.0 验修轮 V-2)。 */
78
+ readonly name?: string;
79
+ readonly id?: string;
80
+ readonly provider?: string;
81
+ readonly fields: readonly string[];
82
+ readonly issues: string;
83
+ }
84
+ /** 本次读的模型域**原文**与**解析产物**(两腿各自手里都有这两件:远端 = 响应体与 `readEffectiveWire` 输出;本地 = 盘上
85
+ * `config.d/models.json` 与 `FileConfigStore` 输出)。 */
86
+ export interface ModelsDomainSource {
87
+ /** 模型域的原始文档(`{ models: [...], roles, … }`);读不到 ⇒ `undefined`(身份一律证明不了)。 */
88
+ readonly raw: unknown;
89
+ /** 同一次读解析后幸存的条目(schema 逐条丢弃之后的 `models.models`)。 */
90
+ readonly parsed: readonly unknown[] | undefined;
91
+ }
92
+ /**
93
+ * 一条被丢行在原文档里那一格的读数 —— **三态**(7.101.0 验修轮 V-2,规则变小:「读不出」只剩一种含义):
94
+ * · `named`:格读到了、`name` 是字符串 ⇒ 身份已定(启用的铸墓碑,停用的不是);
95
+ * · `nameless`:格读到了、没有字符串 `name`(`null` / 数 / 整格不是对象)⇒ 身份**也**已定:**没有可保留的名字** —— 请求、
96
+ * `@`-mention、角色绑定都点不到一个非字符串名,档位增广也铸不出它 ⇒ 与 `enabled:false` 同处置(不是墓碑、不拒组);
97
+ * · `undefined`(由 {@link droppedEntryIdentity} 答):格读不到 —— 坐标在原文档里不存在;调用方在原文档与本次解析对不上
98
+ * (`sameRead` 假)时根本不来读 ⇒ 两者同是「身份证明不了」,交一条不带 `name` 的载体(模型面据此整组拒)。
99
+ * 此前 `nameless` 被折进「读不出」⇒ 一行 `{ name: null }` 的坏行(哪怕是停用的)就让整个模型面被拒(codex 验修腿 V-2,boot 腿对
100
+ * v7.100.0 的窄回归:那时 boot 对同一文档宽容采纳幸存条目)。
101
+ */
102
+ export type DroppedEntryCell = {
103
+ readonly kind: "named";
104
+ readonly name: string;
105
+ readonly id?: string;
106
+ readonly provider?: string;
107
+ readonly enabled: boolean;
108
+ } | {
109
+ readonly kind: "nameless";
110
+ };
111
+ /**
112
+ * 🔴 S-712 **唯一**的身份读法:按告警坐标(`path`,模型域文档内的段坐标 —— settings-schema 已把它回写成原文档坐标)回读
113
+ * 原文档那一格,答 {@link DroppedEntryCell} 三态之一;坐标在原文档里走不通 ⇒ `undefined`(格读不到)。字符串才算
114
+ * (`name` / `id` / `provider`),`enabled` 只认字面 `false` 为停用。
115
+ * settings-schema 6.1.0 的 `entry-dropped` 自带 `name` / `id` 到货后,本函数改读告警(同一只函数,换数据源),原文档入参随之删除。
116
+ */
117
+ export declare function droppedEntryIdentity(rawDoc: unknown, path: ConfigPath): DroppedEntryCell | undefined;
118
+ /**
119
+ * 两条读腿(远端 `http-client.ts` / 本地 `config-provider.ts`)共用的**分面单点**:折叠产物按 `scope` 分进候选门
120
+ * (`domainErrors`)与逐条丢弃面(`droppedEntries`)。`switch` 穷举 —— 加一种 scope 不补臂 = 编译红,两腿不会各分各的。
121
+ * 文案**未脱敏**:进日志流之前各腿在自己的产出边界上过 `redactConfigError`(与 `domainErrors` 同一道)。
122
+ * S-712:同一次折叠把被丢**模型目录条目**的身份交出(`droppedIdentities`,见 {@link DroppedModelIdentity}),按 {@link DroppedEntryCell}
123
+ * 三态分流:有名且启用 ⇒ 墓碑;停用的、没有字符串名的 ⇒ 不进载体(停用 = 不在目录,与 7.100.0 同义;无名 = 没有可保留的名字);
124
+ * 格读不到 / 原文档对不上 ⇒ 不带 `name` 的载体(证明不了)。坏行本身照样走 `droppedEntries`(坐标 + 字段,不带值)。
125
+ */
126
+ export declare function splitFoldedReadWarnings(warnings: readonly EffectiveReadWarning[], models: ModelsDomainSource): {
127
+ domainErrors: Array<{
128
+ domain: string;
129
+ error: string;
130
+ }>;
131
+ droppedEntries: DroppedConfigEntry[];
132
+ droppedIdentities: DroppedModelIdentity[];
133
+ };
134
+ /** S-712:把本次读交出的被丢身份挂上 effective 的模型域(两腿同一只;空 ⇒ 不挂键)。 */
135
+ export declare function withDroppedIdentities<E extends {
136
+ models?: object;
137
+ }>(effective: E, droppedIdentities: readonly DroppedModelIdentity[]): E;
29
138
  //# sourceMappingURL=read-warnings.d.ts.map