@sema-agent/settings-schema 10.1.1 → 11.1.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 (240) hide show
  1. package/CHANGELOG.md +469 -1
  2. package/README.md +144 -66
  3. package/bin/settings-schema.mjs +10 -12
  4. package/conformance/VERSION +1 -1
  5. package/conformance/baseline.json +3 -1
  6. package/conformance/cases/c01-entry-salvage-then-typo/expected.json +3 -1
  7. package/conformance/cases/c02-hosts-clamp-stacked/expected.json +12 -5
  8. package/conformance/cases/c02-hosts-clamp-stacked/meta.json +1 -1
  9. package/conformance/cases/c03-literal-dotted-top-key/expected.json +22 -2
  10. package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +3 -1
  11. package/conformance/cases/c06-legacy-governance-lift/expected.json +11 -101
  12. package/conformance/cases/c06-legacy-governance-lift/meta.json +1 -1
  13. package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +3 -1
  14. package/conformance/cases/c08-limits-unknown-carried-64/expected.json +203 -2
  15. package/conformance/cases/c09-limits-unknown-carried-65/expected.json +204 -2
  16. package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +3 -1
  17. package/conformance/cases/c11-unicode-unknown-keys/expected.json +20 -2
  18. package/conformance/cases/c12-prototype-name-top-keys/expected.json +3 -1
  19. package/conformance/cases/c13-models-field-null/expected.json +3 -1
  20. package/conformance/cases/c14-models-field-empty-array/expected.json +6 -1
  21. package/conformance/cases/c15-models-field-number/expected.json +3 -1
  22. package/conformance/cases/c16-deep-nesting-205/expected.json +14 -2
  23. package/conformance/cases/c17-empty-domain-file/expected.json +3 -1
  24. package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +3 -1
  25. package/conformance/cases/c19-wire-envelope-no-version/expected.json +3 -1
  26. package/conformance/cases/c20-governance-absent-lifts/expected.json +11 -86
  27. package/conformance/cases/c20-governance-absent-lifts/meta.json +1 -1
  28. package/conformance/cases/c21-governance-empty-object-wins/expected.json +6 -5
  29. package/conformance/cases/c21-governance-empty-object-wins/input.json +1 -4
  30. package/conformance/cases/c21-governance-empty-object-wins/meta.json +1 -1
  31. package/conformance/cases/c23-mixed-validity-models/expected.json +3 -1
  32. package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +22 -2
  33. package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +6 -1
  34. package/conformance/cases/c26-hosts-array-1024/expected.json +3 -1
  35. package/conformance/cases/c27-hosts-array-1025/expected.json +6 -7175
  36. package/conformance/cases/c27-hosts-array-1025/meta.json +1 -1
  37. package/conformance/cases/c28-large-document-over-1mb/expected.json +14 -2
  38. package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +3 -1
  39. package/conformance/cases/c30-version-triple-a-first/expected.json +6 -1
  40. package/conformance/cases/c31-version-triple-b-changed/expected.json +6 -1
  41. package/conformance/cases/c32-version-triple-a-restored/expected.json +6 -1
  42. package/conformance/cases/c33-limits-nested-carried-key/expected.json +18 -2
  43. package/conformance/cases/c34-skills-content-vs-manifest/expected.json +19 -3
  44. package/conformance/cases/c35-portable-domain-file-is-null/expected.json +3 -1
  45. package/conformance/cases/c36-skills-entry-without-content/expected.json +6 -2
  46. package/conformance/cases/c37-skills-field-null/expected.json +6 -2
  47. package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +3 -1
  48. package/conformance/cases/c39-hidden-file-in-config-d/expected.json +3 -1
  49. package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +11 -4
  50. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +9 -3
  51. package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +39 -2
  52. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +4 -1
  53. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +3 -1
  54. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +19 -10
  55. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +1 -1
  56. package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/expected.json +12 -5
  57. package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/meta.json +9 -0
  58. package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/expected.json +23 -3
  59. package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/input.json +6 -0
  60. package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/meta.json +10 -0
  61. package/conformance/cases/c48-s935-model-headers-transported/expected.json +6 -1
  62. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +3 -1
  63. package/conformance/cases/c50-s784-lane-host-legal/expected.json +3 -1
  64. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +3 -1
  65. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +3 -1
  66. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +3 -1
  67. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +3 -1
  68. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +3 -1
  69. package/conformance/cases/c56-s784-lane-device-legal/expected.json +3 -1
  70. package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +3 -1
  71. package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +3 -1
  72. package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +3 -1
  73. package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +3 -1
  74. package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +3 -1
  75. package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +3 -1
  76. package/conformance/cases/c63-s784-host-two-replicas/expected.json +3 -1
  77. package/conformance/cases/c64-s784-multitenant-local-store/expected.json +3 -1
  78. package/conformance/cases/c65-s784-pg-without-db/expected.json +3 -1
  79. package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +3 -1
  80. package/conformance/cases/c67-s784-retired-field-loud/expected.json +3 -1
  81. package/conformance/cases/c67-s784-retired-field-loud/meta.json +1 -1
  82. package/conformance/cases/c68-s784-device-local-store/expected.json +3 -1
  83. package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +3 -1
  84. package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +3 -1
  85. package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +119 -0
  86. package/conformance/cases/c71-one-scanner-write-reads-display-forms/input.json +24 -0
  87. package/conformance/cases/c71-one-scanner-write-reads-display-forms/meta.json +10 -0
  88. package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +111 -0
  89. package/conformance/cases/c72-dangling-reference-write-seat-refuses/input.json +26 -0
  90. package/conformance/cases/c72-dangling-reference-write-seat-refuses/meta.json +6 -0
  91. package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +3 -1
  92. package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +3 -1
  93. package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +6 -1
  94. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +11 -1
  95. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +11 -1
  96. package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +16 -2
  97. package/conformance/cases/cli04-env-value-with-newline/expected.json +3 -1
  98. package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -1
  99. package/conformance/cases/t01-absent-domain-key-not-null/expected.json +3 -1
  100. package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +29 -2
  101. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +3 -1
  102. package/conformance/cases/z01-every-domain-empty-object/expected.json +8 -4
  103. package/conformance/cases/z01-every-domain-empty-object/input.json +0 -1
  104. package/conformance/cases/z01-every-domain-empty-object/meta.json +1 -1
  105. package/conformance/cases/z02-every-domain-unknown-key/expected.json +163 -23
  106. package/conformance/cases/z02-every-domain-unknown-key/input.json +0 -3
  107. package/conformance/cases/z02-every-domain-unknown-key/meta.json +1 -1
  108. package/dist/api/auth-bridge.d.ts +51 -181
  109. package/dist/api/auth-bridge.js +58 -33
  110. package/dist/api/auth.d.ts +37 -146
  111. package/dist/api/auth.js +16 -26
  112. package/dist/api/feedback.d.ts +87 -0
  113. package/dist/api/feedback.js +66 -0
  114. package/dist/api/pull-token.d.ts +32 -0
  115. package/dist/api/pull-token.js +88 -0
  116. package/dist/api/scopes.d.ts +82 -174
  117. package/dist/api/scopes.js +23 -18
  118. package/dist/atomic-write.d.ts +14 -0
  119. package/dist/atomic-write.js +60 -0
  120. package/dist/bundle.d.ts +15 -7
  121. package/dist/bundle.js +28 -27
  122. package/dist/cli.d.ts +3 -1
  123. package/dist/cli.js +149 -80
  124. package/dist/config-d-file.d.ts +14 -0
  125. package/dist/config-d-file.js +67 -0
  126. package/dist/config-fns.d.ts +87 -186
  127. package/dist/config-fns.js +207 -262
  128. package/dist/conformance/contract-adapter.d.ts +9 -2
  129. package/dist/conformance/contract-adapter.js +15 -7
  130. package/dist/conformance/corpus.d.ts +3 -0
  131. package/dist/conformance/corpus.js +5 -0
  132. package/dist/conformance/derive.d.ts +3 -0
  133. package/dist/conformance/index.d.ts +2 -2
  134. package/dist/conformance/index.js +2 -2
  135. package/dist/conformance/run.js +67 -22
  136. package/dist/conformance/types.d.ts +31 -15
  137. package/dist/conformance/types.js +5 -0
  138. package/dist/conformance/warnings.d.ts +1 -1
  139. package/dist/conformance/warnings.js +1 -4
  140. package/dist/cross-domain.d.ts +66 -22
  141. package/dist/cross-domain.js +164 -93
  142. package/dist/cross-session.d.ts +29 -11
  143. package/dist/default-model.d.ts +43 -0
  144. package/dist/default-model.js +34 -0
  145. package/dist/execution-lane-index.d.ts +4 -8
  146. package/dist/execution-lane-index.js +4 -8
  147. package/dist/execution-lanes.d.ts +31 -8
  148. package/dist/execution-lanes.js +23 -5
  149. package/dist/file-edit.d.ts +3 -3
  150. package/dist/file-edit.js +11 -18
  151. package/dist/file-store.d.ts +7 -6
  152. package/dist/file-store.js +37 -50
  153. package/dist/fleet.d.ts +364 -762
  154. package/dist/fleet.js +277 -224
  155. package/dist/hooks.d.ts +1151 -4737
  156. package/dist/hooks.js +59 -15
  157. package/dist/index.d.ts +10 -4
  158. package/dist/index.js +13 -7
  159. package/dist/integer.d.ts +11 -0
  160. package/dist/integer.js +11 -0
  161. package/dist/issue-path.d.ts +10 -0
  162. package/dist/issue-path.js +10 -0
  163. package/dist/local-load.d.ts +1 -1
  164. package/dist/local-load.js +4 -14
  165. package/dist/loopback.d.ts +26 -0
  166. package/dist/loopback.js +54 -0
  167. package/dist/migration-receipt.d.ts +42 -0
  168. package/dist/migration-receipt.js +12 -0
  169. package/dist/one-time-migrations.d.ts +62 -0
  170. package/dist/one-time-migrations.js +828 -0
  171. package/dist/prompts-digest.d.ts +1 -1
  172. package/dist/prompts-digest.js +3 -2
  173. package/dist/reader.d.ts +2 -2
  174. package/dist/remote-exec.d.ts +45 -187
  175. package/dist/remote-exec.js +16 -10
  176. package/dist/resolve-roster.d.ts +18 -3
  177. package/dist/resolve-roster.js +28 -25
  178. package/dist/retired-envs.d.ts +24 -0
  179. package/dist/retired-envs.js +114 -0
  180. package/dist/retired-form.d.ts +31 -0
  181. package/dist/retired-form.js +28 -0
  182. package/dist/safety-merge-spec.d.ts +131 -191
  183. package/dist/safety-merge-spec.js +18 -14
  184. package/dist/scheduler-lifetime.d.ts +5 -0
  185. package/dist/scheduler-lifetime.js +7 -0
  186. package/dist/scheduler-store-migration.d.ts +14 -0
  187. package/dist/scheduler-store-migration.js +14 -0
  188. package/dist/scheduler-store-node.d.ts +28 -12
  189. package/dist/scheduler-store-node.js +32 -23
  190. package/dist/scheduler-store.d.ts +47 -14
  191. package/dist/scheduler-store.js +32 -61
  192. package/dist/secret-refs.d.ts +3 -3
  193. package/dist/secret-refs.js +1 -1
  194. package/dist/secret-scan.d.ts +14 -24
  195. package/dist/secret-scan.js +45 -23
  196. package/dist/sha256-core.d.ts +16 -0
  197. package/dist/sha256-core.js +138 -0
  198. package/dist/sha256.d.ts +6 -1
  199. package/dist/sha256.js +8 -98
  200. package/dist/skills-manifest.js +1 -1
  201. package/dist/types.d.ts +1509 -13536
  202. package/dist/types.js +526 -438
  203. package/dist/url-shape.d.ts +34 -0
  204. package/dist/url-shape.js +61 -0
  205. package/dist/web-admin-credential.d.ts +96 -0
  206. package/dist/{center-credential.js → web-admin-credential.js} +49 -46
  207. package/dist/web-admin-env.d.ts +33 -0
  208. package/dist/web-admin-env.js +33 -0
  209. package/dist/worker-row-verdict.d.ts +16 -0
  210. package/dist/worker-row-verdict.js +31 -0
  211. package/dist/workers-rows.d.ts +34 -0
  212. package/dist/workers-rows.js +42 -0
  213. package/dist/zod-unwrap.d.ts +11 -0
  214. package/dist/zod-unwrap.js +25 -0
  215. package/package.json +17 -9
  216. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +0 -6
  217. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +0 -6
  218. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +0 -49
  219. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +0 -26
  220. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +0 -4
  221. package/conformance/migrate-workers/m02-g5-idempotent/expected.json +0 -38
  222. package/conformance/migrate-workers/m02-g5-idempotent/input.json +0 -33
  223. package/conformance/migrate-workers/m02-g5-idempotent/meta.json +0 -5
  224. package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +0 -71
  225. package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +0 -42
  226. package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +0 -4
  227. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +0 -52
  228. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +0 -30
  229. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +0 -4
  230. package/conformance/migrate-workers/m05-row-verdict-reserved-env/expected.json +0 -82
  231. package/conformance/migrate-workers/m05-row-verdict-reserved-env/input.json +0 -68
  232. package/conformance/migrate-workers/m05-row-verdict-reserved-env/meta.json +0 -4
  233. package/dist/center-credential.d.ts +0 -116
  234. package/dist/issue-text.d.ts +0 -10
  235. package/dist/issue-text.js +0 -8
  236. package/dist/migrate-workers.d.ts +0 -71
  237. package/dist/migrate-workers.js +0 -351
  238. package/dist/migrate.d.ts +0 -255
  239. package/dist/migrate.js +0 -542
  240. /package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/input.json +0 -0
@@ -1,3 +1,62 @@
1
+ import { formatConfigPath } from "./config-fns.js";
2
+ /** What a dangling reference failed to find —— the closed set (S-1075, 11.0.0): one word per referenced domain. */
3
+ export const REF_ISSUE_KINDS = ["unknown_model", "unknown_roster", "unknown_worker", "unknown_skill", "unknown_mcp", "unknown_plugin"];
4
+ /** The domain a {@link RefIssueKind}'s target lives in. */
5
+ export const REF_ISSUE_TARGET_DOMAIN = {
6
+ unknown_model: "models",
7
+ unknown_roster: "rosters",
8
+ unknown_worker: "workers",
9
+ unknown_skill: "skills",
10
+ unknown_mcp: "mcp",
11
+ unknown_plugin: "plugins",
12
+ };
13
+ /** The domains whose documents carry cross-domain references (by name) —— the referrer side of the judge. */
14
+ export const REF_REFERRER_DOMAINS = ["rosters", "workers", "models", "collab", "systems", "entitlement"];
15
+ /** The noun a kind's sentence uses for its target. */
16
+ function refTargetNoun(kind) {
17
+ switch (kind) {
18
+ case "unknown_model":
19
+ return "model";
20
+ case "unknown_roster":
21
+ return "roster";
22
+ case "unknown_worker":
23
+ return "worker";
24
+ case "unknown_skill":
25
+ return "skill";
26
+ case "unknown_mcp":
27
+ return "mcp server";
28
+ case "unknown_plugin":
29
+ return "plugin";
30
+ }
31
+ }
32
+ /** How a referrer is named in the sentence (its name through `show`). */
33
+ function refReferrerLabel(r, show) {
34
+ const named = (noun) => (r.name === undefined ? noun : `${noun} "${show(r.name)}"`);
35
+ switch (r.domain) {
36
+ case "rosters":
37
+ return named("roster");
38
+ case "workers":
39
+ return named("worker");
40
+ case "models":
41
+ return named("models");
42
+ case "collab":
43
+ return named("collab template");
44
+ case "systems":
45
+ return named("system");
46
+ case "entitlement":
47
+ return named("tier");
48
+ }
49
+ }
50
+ /** The ONE rendering of a {@link RefIssue}'s sentence: `<referrer> → unknown <noun> "<target>" (at <path>)`. `show` maps each
51
+ * user-written value (the referrer's name, the target); the default shows them as written — that is how `message` is minted. */
52
+ export function renderRefIssue(issue, show = (v) => v) {
53
+ return `${refReferrerLabel(issue.referrer, show)} → unknown ${refTargetNoun(issue.kind)} "${show(issue.target)}" (at ${formatConfigPath(issue.path)})`;
54
+ }
55
+ /** The one minting point of a {@link RefIssue}. */
56
+ function danglingRef(path, kind, referrer, target) {
57
+ const operands = { path, code: "custom", kind, referrer, target };
58
+ return { ...operands, message: renderRefIssue(operands) };
59
+ }
1
60
  /** A resolver over the reader, optionally shadowed by pending (same-operation) writes keyed by domain.
2
61
  * Widened from the fat ConfigStore to the narrow {@link ConfigReader} (it only calls `getDomain`) so a TOC
3
62
  * file source can use it without the full store.
@@ -12,131 +71,143 @@ export function createSiblingResolver(store, pending) {
12
71
  return store.getDomain(domain);
13
72
  };
14
73
  }
15
- /** The domains whose writes carry cross-domain model/roster references we validate at save time — the closed
16
- * set as a RUNTIME VALUE (4.0.0). A consumer that has to ask "is this domain ref-checked?" at runtime (a
17
- * config-rollback path deciding whether to run the guard before writing) could only hand-copy the list
18
- * before, and a hand copy drifts silently: two rollback routes in the admin console each carried a
19
- * five-member copy that had lost `entitlement`, so an entitlement rollback skipped the guard entirely with
20
- * nothing red anywhere. Ask this constant (or {@link isRefCheckedDomain}) instead of writing a list.
74
+ /** The domains a write must be judged for —— the closed set as a RUNTIME VALUE (4.0.0): every domain that HOLDS cross-domain
75
+ * references ({@link REF_REFERRER_DOMAINS}) and every domain those references POINT INTO (the targets of
76
+ * {@link REF_ISSUE_TARGET_DOMAIN}) —— 11.0.0 (S-1066 ③) added the three target-only domains `skills` / `mcp` / `plugins`: deleting
77
+ * a skill a tier still names is a dangling reference made by the skills write, so that write is judged too. A consumer that has
78
+ * to ask "is this domain ref-checked?" at runtime (a config-rollback path deciding whether to run the guard before writing)
79
+ * asks this constant (or {@link isRefCheckedDomain}) instead of writing a list — a hand copy drifts silently (two rollback routes
80
+ * in the admin console each once carried a five-member copy that had lost `entitlement`).
21
81
  *
22
- * `satisfies readonly (keyof DomainConfig)[]` keeps every member a real domain key; {@link RefCheckedDomain}
23
- * is DERIVED from this value, so there is exactly one place the set is written down. */
24
- export const REF_CHECKED_DOMAINS = ["rosters", "workers", "models", "collab", "systems", "entitlement"];
82
+ * `satisfies` keeps every member a real domain key; a pin checks it equals referrers ∪ targets. {@link RefCheckedDomain} is
83
+ * DERIVED from this value. */
84
+ export const REF_CHECKED_DOMAINS = ["rosters", "workers", "models", "collab", "systems", "entitlement", "skills", "mcp", "plugins"];
25
85
  /** Runtime narrowing to {@link RefCheckedDomain}, same idiom as `isDomainName`: use it instead of hard-coding
26
86
  * the set when filtering an arbitrary domain key (a batch payload's keys, a rollback target). */
27
87
  export function isRefCheckedDomain(d) {
28
88
  return typeof d === "string" && REF_CHECKED_DOMAINS.includes(d);
29
89
  }
30
- /** Cross-domain ref integrity for one {@link RefCheckedDomain} value. Every issue `path` starts with the
31
- * DOMAIN KEY (e.g. `["rosters", i, "models", j]`, `["collab", "templates", i, …]`) so a caller can prefix a
32
- * batch index and get a coordinate that is unambiguous across arms. Empty = ok. Runtime resolution stays
33
- * tolerant (falls back); this is purely a save-time guard — a precise 400 at save time is friendlier than a
34
- * silently-wrong roster.
35
- *
36
- * 4.0.0 made that first sentence TRUE: the `collab` arm used to start its paths at `"templates"` and the
37
- * `entitlement` arm at `"tiers"`, so a caller could not read `path[0]` as the domain. See the CHANGELOG entry
38
- * for the migration (BREAKING, value shape). */
39
- export async function refIntegrityIssues(domain, value, resolve) {
40
- const issues = [];
90
+ /** Every by-name reference `domain`'s document makes —— the ONE place the reference graph is written down (which field of which
91
+ * domain names what). Exhaustive over {@link RefReferrerDomain}: a new referrer domain is a compile error until it is listed. */
92
+ function referencesFrom(domain, docs, push) {
41
93
  if (domain === "rosters") {
42
- const catalog = new Set(((await resolve("models"))?.models ?? []).map((m) => m.name));
43
- value.rosters.forEach((r, i) => {
44
- r.models.forEach((name, j) => {
45
- if (!catalog.has(name))
46
- issues.push({ path: ["rosters", i, "models", j], message: `roster "${r.name}" references unknown model "${name}" (not in the models catalog)`, code: "custom" });
47
- });
94
+ (docs.rosters?.rosters ?? []).forEach((r, i) => {
95
+ const referrer = { domain, name: r.name };
96
+ r.models.forEach((name, j) => push({ path: ["rosters", i, "models", j], kind: "unknown_model", referrer, target: name }));
48
97
  for (const [role, target] of Object.entries(r.roles ?? {})) {
49
- if (target && "model" in target && !catalog.has(target.model))
50
- issues.push({ path: ["rosters", i, "roles", role], message: `roster "${r.name}" role.${role} → unknown model "${target.model}"`, code: "custom" });
98
+ if (target && "model" in target)
99
+ push({ path: ["rosters", i, "roles", role], kind: "unknown_model", referrer, target: target.model });
51
100
  }
52
- // 0.6.3 markers (primaryModel/cheapModel) reference the catalog like a {model}-form role does — same
53
- // save-time guard (a dangling marker would silently derive nothing at resolution).
54
- if (r.primaryModel && !catalog.has(r.primaryModel))
55
- issues.push({ path: ["rosters", i, "primaryModel"], message: `roster "${r.name}" primaryModel → unknown model "${r.primaryModel}" (not in the models catalog)`, code: "custom" });
56
- if (r.cheapModel && !catalog.has(r.cheapModel))
57
- issues.push({ path: ["rosters", i, "cheapModel"], message: `roster "${r.name}" cheapModel → unknown model "${r.cheapModel}" (not in the models catalog)`, code: "custom" });
58
- (r.atModelAllowlist ?? []).forEach((name, j) => {
59
- if (!catalog.has(name))
60
- issues.push({ path: ["rosters", i, "atModelAllowlist", j], message: `roster "${r.name}" atModelAllowlist "${name}" not in the models catalog`, code: "custom" });
61
- });
101
+ // 0.6.3 markers (primaryModel/cheapModel) reference the catalog like a {model}-form role does.
102
+ if (r.primaryModel)
103
+ push({ path: ["rosters", i, "primaryModel"], kind: "unknown_model", referrer, target: r.primaryModel });
104
+ if (r.cheapModel)
105
+ push({ path: ["rosters", i, "cheapModel"], kind: "unknown_model", referrer, target: r.cheapModel });
106
+ if (r.atMention?.mode === "only")
107
+ r.atMention.models.forEach((name, j) => push({ path: ["rosters", i, "atMention", "models", j], kind: "unknown_model", referrer, target: name }));
62
108
  });
63
109
  }
64
110
  else if (domain === "workers") {
65
- const rosterNames = new Set(((await resolve("rosters"))?.rosters ?? []).map((r) => r.name));
66
- value.workers.forEach((w, i) => {
67
- if (w.roster && !rosterNames.has(w.roster))
68
- issues.push({ path: ["workers", i, "roster"], message: `worker "${w.name}" references unknown roster "${w.roster}"`, code: "custom" });
111
+ (docs.workers?.workers ?? []).forEach((w, i) => {
112
+ if (w.roster)
113
+ push({ path: ["workers", i, "roster"], kind: "unknown_roster", referrer: { domain, name: w.name }, target: w.roster });
69
114
  });
70
115
  }
71
116
  else if (domain === "models") {
72
- // The roles map + @-allowlist reference models in THIS payload's OWN catalog (they ship together in the
73
- // models domain), so validate against `value`, not the stored/sibling models.
74
- const m = value;
75
- const catalog = new Set(m.models.map((x) => x.name));
76
- for (const [role, target] of Object.entries(m.roles ?? {})) {
77
- if (target && "model" in target && !catalog.has(target.model))
78
- issues.push({ path: ["models", "roles", role], message: `models.roles.${role} → unknown model "${target.model}" (not in the catalog)`, code: "custom" });
117
+ // The roles map + `atMention.models` reference the catalog of the SAME document (they ship together in the models domain).
118
+ const m = docs.models;
119
+ for (const [role, target] of Object.entries(m?.roles ?? {})) {
120
+ if (target && "model" in target)
121
+ push({ path: ["models", "roles", role], kind: "unknown_model", referrer: { domain }, target: target.model });
79
122
  }
80
- (m.atModelAllowlist ?? []).forEach((name, j) => {
81
- if (!catalog.has(name))
82
- issues.push({ path: ["models", "atModelAllowlist", j], message: `atModelAllowlist "${name}" not in the models catalog`, code: "custom" });
83
- });
123
+ if (m?.atMention?.mode === "only")
124
+ m.atMention.models.forEach((name, j) => push({ path: ["models", "atMention", "models", j], kind: "unknown_model", referrer: { domain }, target: name }));
84
125
  }
85
126
  else if (domain === "collab") {
86
- // collab (0.7.0, replaces teams): a slot member's `modelRef` is a models-CATALOG name (T1 — never a
87
- // roster, never a gateway id) and must resolve against the catalog (sibling — batch-aware). ABSENT
88
- // modelRef is the legal "跟随用户当前模型" state, not a dangle. This is the SAVE-time guard; a model
89
- // deleted/renamed LATER still dangles → the web renders the 悬空 destructive badge (§11-6), same as
90
- // groupBindings.
91
- const catalog = new Set(((await resolve("models"))?.models ?? []).map((x) => x.name));
92
- value.templates.forEach((t, i) => {
127
+ // collab (0.7.0): a slot member's `modelRef` is a models-CATALOG name. ABSENT modelRef is the legal "跟随用户当前模型" state.
128
+ (docs.collab?.templates ?? []).forEach((t, i) => {
93
129
  t.slots.forEach((s, j) => {
94
130
  s.members.forEach((mem, k) => {
95
- if (mem.modelRef && !catalog.has(mem.modelRef))
96
- issues.push({ path: ["collab", "templates", i, "slots", j, "members", k, "modelRef"], message: `collab template "${t.id}" slot "${s.id}" → unknown model "${mem.modelRef}" (not in the catalog)`, code: "custom" });
131
+ if (mem.modelRef)
132
+ push({ path: ["collab", "templates", i, "slots", j, "members", k, "modelRef"], kind: "unknown_model", referrer: { domain, name: t.id }, target: mem.modelRef });
97
133
  });
98
134
  });
99
135
  });
100
136
  }
101
137
  else if (domain === "systems") {
102
- // systems.callsWorkers → workers domain (a dangling name would mint a credential routed at nobody;
103
- // resolution stays tolerant — this is the save-time guard, same as the other ref-checked domains).
104
- const workers = new Set((await resolve("workers"))?.workers.map((w) => w.name) ?? []);
105
- value.systems.forEach((sys, i) => {
106
- sys.callsWorkers.forEach((name, j) => {
107
- if (!workers.has(name))
108
- issues.push({ path: ["systems", i, "callsWorkers", j], message: `system "${sys.name}" references unknown worker "${name}"`, code: "custom" });
109
- });
138
+ // systems.callsWorkers → workers (a dangling name would mint a credential routed at nobody).
139
+ (docs.systems?.systems ?? []).forEach((sys, i) => {
140
+ sys.callsWorkers.forEach((name, j) => push({ path: ["systems", i, "callsWorkers", j], kind: "unknown_worker", referrer: { domain, name: sys.name }, target: name }));
110
141
  });
111
142
  }
112
143
  else if (domain === "entitlement") {
113
- // A tier references FIVE sibling domains by name (roster→rosters, models[]→models catalog, visibleSkills→
114
- // skills, visibleMcp→mcp, visiblePlugins→plugins). A dangling ref otherwise resolves silently (e.g. a typo'd roster → resolve-
115
- // entitlement yields an empty set → the principal is silently denied) — a precise 400 at save time is
116
- // friendlier, matching the rosters/workers guards.
117
- const rosterNames = new Set(((await resolve("rosters"))?.rosters ?? []).map((r) => r.name));
118
- const modelCatalog = new Set(((await resolve("models"))?.models ?? []).map((m) => m.name));
119
- const skillNames = new Set((await resolve("skills"))?.skills.map((s) => s.name) ?? []);
120
- const mcpNames = new Set((await resolve("mcp"))?.servers.map((s) => s.name) ?? []);
121
- const pluginNames = new Set((await resolve("plugins"))?.plugins.map((p) => p.name) ?? []);
122
- value.tiers.forEach((t, i) => {
123
- if (t.roster && !rosterNames.has(t.roster))
124
- issues.push({ path: ["entitlement", "tiers", i, "roster"], message: `tier "${t.id}" references unknown roster "${t.roster}"`, code: "custom" });
125
- (t.models ?? []).forEach((name, j) => { if (!modelCatalog.has(name))
126
- issues.push({ path: ["entitlement", "tiers", i, "models", j], message: `tier "${t.id}" model "${name}" not in the models catalog`, code: "custom" }); });
127
- (t.visibleSkills ?? []).forEach((name, j) => { if (!skillNames.has(name))
128
- issues.push({ path: ["entitlement", "tiers", i, "visibleSkills", j], message: `tier "${t.id}" visibleSkill "${name}" not in skills`, code: "custom" }); });
129
- (t.visibleMcp ?? []).forEach((name, j) => { if (!mcpNames.has(name))
130
- issues.push({ path: ["entitlement", "tiers", i, "visibleMcp", j], message: `tier "${t.id}" visibleMcp "${name}" not in mcp`, code: "custom" }); });
131
- (t.visiblePlugins ?? []).forEach((name, j) => { if (!pluginNames.has(name))
132
- issues.push({ path: ["entitlement", "tiers", i, "visiblePlugins", j], message: `tier "${t.id}" visiblePlugin "${name}" not in plugins`, code: "custom" }); });
144
+ // A tier references FIVE domains by name; a dangling one otherwise resolves silently (a typo'd roster → an empty set → the
145
+ // principal is silently denied).
146
+ (docs.entitlement?.tiers ?? []).forEach((t, i) => {
147
+ const referrer = { domain, name: t.id };
148
+ if (t.roster)
149
+ push({ path: ["entitlement", "tiers", i, "roster"], kind: "unknown_roster", referrer, target: t.roster });
150
+ (t.models ?? []).forEach((name, j) => push({ path: ["entitlement", "tiers", i, "models", j], kind: "unknown_model", referrer, target: name }));
151
+ (t.visibleSkills ?? []).forEach((name, j) => push({ path: ["entitlement", "tiers", i, "visibleSkills", j], kind: "unknown_skill", referrer, target: name }));
152
+ (t.visibleMcp ?? []).forEach((name, j) => push({ path: ["entitlement", "tiers", i, "visibleMcp", j], kind: "unknown_mcp", referrer, target: name }));
153
+ (t.visiblePlugins ?? []).forEach((name, j) => push({ path: ["entitlement", "tiers", i, "visiblePlugins", j], kind: "unknown_plugin", referrer, target: name }));
133
154
  });
134
155
  }
135
156
  else {
136
- // Exhaustiveness guard: a new RefCheckedDomain MUST add its own branch above, not silently inherit another
137
- // domain's validation. `never` makes that a compile error; the throw is the runtime backstop.
157
+ // Exhaustiveness guard: a new referrer domain MUST add its own branch above. `never` makes that a compile error.
138
158
  const unhandled = domain;
139
- throw new Error(`refIntegrityIssues: unhandled domain "${String(unhandled)}"`);
159
+ throw new Error(`referencesFrom: unhandled domain "${String(unhandled)}"`);
160
+ }
161
+ }
162
+ /** The names a target domain defines. */
163
+ function namesIn(kind, docs) {
164
+ switch (kind) {
165
+ case "unknown_model":
166
+ return new Set((docs.models?.models ?? []).map((m) => m.name));
167
+ case "unknown_roster":
168
+ return new Set((docs.rosters?.rosters ?? []).map((r) => r.name));
169
+ case "unknown_worker":
170
+ return new Set((docs.workers?.workers ?? []).map((w) => w.name));
171
+ case "unknown_skill":
172
+ return new Set((docs.skills?.skills ?? []).map((sk) => sk.name));
173
+ case "unknown_mcp":
174
+ return new Set((docs.mcp?.servers ?? []).map((sv) => sv.name));
175
+ case "unknown_plugin":
176
+ return new Set((docs.plugins?.plugins ?? []).map((pl) => pl.name));
177
+ }
178
+ }
179
+ /**
180
+ * **THE reference judge** (11.0.0, S-1066 ③; pure, synchronous): every by-name cross-domain reference in a document set that does
181
+ * not resolve, as a {@link RefIssue} (`path` = where the reference sits, `path[0]` = the referrer's domain). Both seats read this
182
+ * one conclusion: the write seat ({@link refIntegrityIssues}, refusing) and the read side (`resolveEffectiveForWorker`'s
183
+ * `refIssues`, reporting what it drops). Before 11.0.0 the write seat judged forward only and the read side judged on its own,
184
+ * silently.
185
+ */
186
+ export function danglingRefs(docs) {
187
+ const issues = [];
188
+ const names = new Map();
189
+ const known = (kind) => names.get(kind) ?? names.set(kind, namesIn(kind, docs)).get(kind);
190
+ for (const domain of REF_REFERRER_DOMAINS) {
191
+ referencesFrom(domain, docs, (ref) => {
192
+ if (!known(ref.kind).has(ref.target))
193
+ issues.push(danglingRef(ref.path, ref.kind, ref.referrer, ref.target));
194
+ });
140
195
  }
141
196
  return issues;
142
197
  }
198
+ /** Cross-domain ref integrity for a write of one {@link RefCheckedDomain} value (the write seat): the judge's conclusion on the
199
+ * document set AFTER the write ({@link danglingRefs} over `value` + every other ref-checked domain through `resolve`), narrowed
200
+ * to the references this write is answerable for —— the ones its own document holds (forward: a roster naming a model that is not
201
+ * in the catalog) and the ones pointing into it (reverse, 11.0.0: deleting a model a roster still names; the issue names the
202
+ * roster, `path` points at the reference). Empty = ok.
203
+ *
204
+ * BATCH-AWARE through `resolve` (a sibling being written in the same batch shadows the stored one, see
205
+ * {@link createSiblingResolver}). A reference that already dangled before this write and still involves this domain is reported
206
+ * too — the same as the forward half always did (a roster write re-judges every roster). */
207
+ export async function refIntegrityIssues(domain, value, resolve) {
208
+ const docs = { [domain]: value };
209
+ for (const other of REF_CHECKED_DOMAINS)
210
+ if (other !== domain)
211
+ docs[other] = await resolve(other);
212
+ return danglingRefs(docs).filter((i) => i.referrer.domain === domain || REF_ISSUE_TARGET_DOMAIN[i.kind] === domain);
213
+ }
@@ -49,7 +49,13 @@ export type CrossSessionInboundValue = (typeof CROSS_SESSION_INBOUND_VALUES)[num
49
49
  /** 设置 UI 的「不设」拼写(见头注)——两键共用同一个字面量。 */
50
50
  export declare const CROSS_SESSION_INBOUND_UNSET: "default";
51
51
  /** 文件层受理面:四个显式值 + `"default"`(未设)。 */
52
- export declare const CrossSessionInboundSetting: z.ZodEnum<["wake", "next-turn", "hold", "refuse", "default"]>;
52
+ export declare const CrossSessionInboundSetting: z.ZodEnum<{
53
+ default: "default";
54
+ wake: "wake";
55
+ "next-turn": "next-turn";
56
+ hold: "hold";
57
+ refuse: "refuse";
58
+ }>;
53
59
  export type CrossSessionInboundSettingValue = z.infer<typeof CrossSessionInboundSetting>;
54
60
  /** 四值闭集(CC 逐字)。`"never"` = 不设期限。 */
55
61
  export declare const DIALOG_EXPIRY_VALUES: readonly ["60s", "5m", "10m", "never"];
@@ -57,7 +63,13 @@ export type DialogExpiryValue = (typeof DIALOG_EXPIRY_VALUES)[number];
57
63
  /** 未设时生效的值(CC 缺省)。 */
58
64
  export declare const DIALOG_EXPIRY_DEFAULT: DialogExpiryValue;
59
65
  /** 文件层受理面:四个值 + `"default"`(未设 ⇒ 落 {@link DIALOG_EXPIRY_DEFAULT})。 */
60
- export declare const DialogExpirySetting: z.ZodEnum<["60s", "5m", "10m", "never", "default"]>;
66
+ export declare const DialogExpirySetting: z.ZodEnum<{
67
+ never: "never";
68
+ default: "default";
69
+ "60s": "60s";
70
+ "5m": "5m";
71
+ "10m": "10m";
72
+ }>;
61
73
  export type DialogExpirySettingValue = z.infer<typeof DialogExpirySetting>;
62
74
  /**
63
75
  * 整份 `settings.json` 里**本片段负责的两键**。刻意 `passthrough`:本片段不是整份 settings 的闭集门
@@ -65,15 +77,21 @@ export type DialogExpirySettingValue = z.infer<typeof DialogExpirySetting>;
65
77
  * 值必合法**。
66
78
  */
67
79
  export declare const CrossSessionSettings: z.ZodObject<{
68
- crossSessionInbound: z.ZodOptional<z.ZodEnum<["wake", "next-turn", "hold", "refuse", "default"]>>;
69
- dialogExpiry: z.ZodOptional<z.ZodEnum<["60s", "5m", "10m", "never", "default"]>>;
70
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
71
- crossSessionInbound: z.ZodOptional<z.ZodEnum<["wake", "next-turn", "hold", "refuse", "default"]>>;
72
- dialogExpiry: z.ZodOptional<z.ZodEnum<["60s", "5m", "10m", "never", "default"]>>;
73
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
74
- crossSessionInbound: z.ZodOptional<z.ZodEnum<["wake", "next-turn", "hold", "refuse", "default"]>>;
75
- dialogExpiry: z.ZodOptional<z.ZodEnum<["60s", "5m", "10m", "never", "default"]>>;
76
- }, z.ZodTypeAny, "passthrough">>;
80
+ crossSessionInbound: z.ZodOptional<z.ZodEnum<{
81
+ default: "default";
82
+ wake: "wake";
83
+ "next-turn": "next-turn";
84
+ hold: "hold";
85
+ refuse: "refuse";
86
+ }>>;
87
+ dialogExpiry: z.ZodOptional<z.ZodEnum<{
88
+ never: "never";
89
+ default: "default";
90
+ "60s": "60s";
91
+ "5m": "5m";
92
+ "10m": "10m";
93
+ }>>;
94
+ }, z.core.$loose>;
77
95
  export type CrossSessionSettings = z.infer<typeof CrossSessionSettings>;
78
96
  /** 一层读数:`ok:false` = 词表外(调用方响亮拒);`ok:true` 无 `value` = 未设;有 `value` = 显式值。 */
79
97
  export type SettingLayerRead<T> = {
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 「目录的缺省模型是哪一条」—— **一只**纯函数(11.0.0,S-1070)。server 的 apply 与 dry-run、壳的本地池读口都调它,不再各写一份链;
3
+ * `roles.default` 是 `{select}` 形时它不答名(交回 `select`,由引擎解),所以本包与引擎对同一份配置不会各答一个。
4
+ *
5
+ * 缺省模型只有一个位:`models.roles.default`(core 角色链的根;N-7 起 `models.default` 结构键退役 —— 那时两位各答一个、主任务跑前者、
6
+ * `default` 角色解到后者)。本模块纯、零 I/O、浏览器安全。
7
+ */
8
+ import type { ModelsConfig, RoleTarget } from "./types.js";
9
+ /** core 对 `default` 角色的档语义:从它的缺省档 `pro` 起**只向低档**扫 —— pro → flash → lite,绝不向上抓 max / ultra
10
+ * (core `ROLE_TIER_DEFAULTS.default` + `DEFAULT_TIER_ORDER`;钉对 devDependency 里精确钉住的那一版 core)。 */
11
+ export declare const DEFAULT_ROLE_TIER_LADDER: readonly ["pro", "flash", "lite"];
12
+ /** 命名链每一级的来源词(闭集)。 */
13
+ export type CatalogDefaultSource = "roles.default" | `tierGroup.${(typeof DEFAULT_ROLE_TIER_LADDER)[number]}` | "fallback";
14
+ /**
15
+ * 解析的结果 —— 纯数据,判别联合(`kind` 闭集,两词):
16
+ * · `named` —— 命名链选中的目录名 `name`、哪一级选的 `source`、哪几级点了名却不可用 `dangling`(调用方据此告警,解析口自己不出声);
17
+ * · `select` —— `roles.default` 是 `{select}` 形:它不点名模型,按目录条目的**属性**(档 / 能力 / 偏好)挑。挑哪一条是引擎的选择语义
18
+ * (core `resolveTaskModel` 对 `default` 角色的解法),本包不复刻、不猜、也不跳到命名链的下一级 —— `select` 原样交回,调用方交给引擎解。
19
+ * 引擎要在与 `has` **同一可用集**(启用 ∧ 本机可服务)上解:交给它一份更宽的目录,它就可能挑中一条 `has` 说不可用的条目。
20
+ * 它是第一级,前面没有可悬空的级,所以不带 `dangling`。
21
+ */
22
+ export type CatalogDefaultModel = {
23
+ readonly kind: "named";
24
+ readonly name: string;
25
+ readonly source: CatalogDefaultSource;
26
+ readonly dangling: readonly {
27
+ readonly source: Exclude<CatalogDefaultSource, "fallback">;
28
+ readonly name: string;
29
+ }[];
30
+ } | {
31
+ readonly kind: "select";
32
+ readonly select: Extract<RoleTarget, {
33
+ select: unknown;
34
+ }>["select"];
35
+ };
36
+ /**
37
+ * 缺省模型由 `roles.default` 的形分两路(一条规则:点名的走命名链,不点名的交回引擎):
38
+ * · `{select}` ⇒ `{ kind: "select", select }`(见 {@link CatalogDefaultModel});
39
+ * · `{model}` 或缺席 ⇒ 命名链 —— 第一个**可用**的候选:`roles.default` 的 `{model}` → 活动档位组的 pro / flash / lite 绑定
40
+ * ({@link DEFAULT_ROLE_TIER_LADDER})→ `fallback`(调用方的兜底,通常是第一条可用条目)。`has(name)` = 调用方的「这条目录名此刻可用」
41
+ * (启用 ∧ 本机可服务);点了名但不可用的一级记进 `dangling`、落下一级,绝不静默指错模型。
42
+ */
43
+ export declare function resolveCatalogDefaultModel(models: Pick<ModelsConfig, "roles" | "tierGroups" | "activeTierGroup">, has: (name: string) => boolean, fallback: string): CatalogDefaultModel;
@@ -0,0 +1,34 @@
1
+ import { resolveActiveTiers } from "./config-fns.js";
2
+ /** core 对 `default` 角色的档语义:从它的缺省档 `pro` 起**只向低档**扫 —— pro → flash → lite,绝不向上抓 max / ultra
3
+ * (core `ROLE_TIER_DEFAULTS.default` + `DEFAULT_TIER_ORDER`;钉对 devDependency 里精确钉住的那一版 core)。 */
4
+ export const DEFAULT_ROLE_TIER_LADDER = ["pro", "flash", "lite"];
5
+ /**
6
+ * 缺省模型由 `roles.default` 的形分两路(一条规则:点名的走命名链,不点名的交回引擎):
7
+ * · `{select}` ⇒ `{ kind: "select", select }`(见 {@link CatalogDefaultModel});
8
+ * · `{model}` 或缺席 ⇒ 命名链 —— 第一个**可用**的候选:`roles.default` 的 `{model}` → 活动档位组的 pro / flash / lite 绑定
9
+ * ({@link DEFAULT_ROLE_TIER_LADDER})→ `fallback`(调用方的兜底,通常是第一条可用条目)。`has(name)` = 调用方的「这条目录名此刻可用」
10
+ * (启用 ∧ 本机可服务);点了名但不可用的一级记进 `dangling`、落下一级,绝不静默指错模型。
11
+ */
12
+ export function resolveCatalogDefaultModel(models, has, fallback) {
13
+ const target = models.roles?.default;
14
+ const named = target !== undefined && "model" in target ? target.model : undefined;
15
+ const select = target !== undefined && "select" in target ? target.select : undefined;
16
+ // 判序与引擎同一只真值判(core `partsModel`:`model` 为真 ⇒ 按名;否则 `select` 为真 ⇒ 按属性挑):schema 读过的值只有一形,这一判序只对
17
+ // 绕过 schema 递进来的两形并在(含 `model: ""`)起作用 —— 那时也与引擎同答。
18
+ if (!named && select)
19
+ return { kind: "select", select };
20
+ const tiers = resolveActiveTiers(models) ?? {};
21
+ const candidates = [
22
+ { source: "roles.default", name: named },
23
+ ...DEFAULT_ROLE_TIER_LADDER.map((t) => ({ source: `tierGroup.${t}`, name: tiers[t] })),
24
+ ];
25
+ const dangling = [];
26
+ for (const c of candidates) {
27
+ if (c.name === undefined || c.name === "")
28
+ continue;
29
+ if (has(c.name))
30
+ return { kind: "named", name: c.name, source: c.source, dangling };
31
+ dangling.push({ source: c.source, name: c.name });
32
+ }
33
+ return { kind: "named", name: fallback, source: "fallback", dangling };
34
+ }
@@ -1,14 +1,14 @@
1
1
  /**
2
- * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引** + 「迁移能搬哪些键」的**一份**键集与指路句,
3
- * 供 `types.ts`(写闸)与 `migrate-workers.ts`(一次性迁移)同读。
2
+ * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引** + 「迁移能搬哪些键」的**一份**键集,
3
+ * 供 `types.ts`(写闸)与 `one-time-migrations.ts`(一次性迁移 SS-M4)同读。
4
4
  * 不进主桶、不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势)—— 消费方只拿表本身,索引是本包的实现细节。
5
5
  * 索引只从表派生,不另存任何词:表改了,这里跟着变。
6
6
  *
7
- * ## 迁移指路(7.0.1)
7
+ * ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`)
8
8
  * 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**。
9
9
  * 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只谓词**
10
10
  * ({@link isMigratableEnvKey}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
11
- * (`test/migration-signpost-7.0.1.test.ts` 用真跑迁移的读数逐个保留名对拍)。
11
+ * (`test/migration-signpost-11.0.0.test.ts` 用真跑迁移的读数逐个保留名对拍)。
12
12
  */
13
13
  import { type ExecutionLane, type ExecutionLaneRow } from "./execution-lanes.js";
14
14
  /** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
@@ -32,7 +32,3 @@ export declare const STORE_ENV_KEYS: readonly ["DB_BACKEND", "MEMORY_ENGINE", "M
32
32
  export declare function isLaneEnvKey(name: string): boolean;
33
33
  /** 一个名字是不是迁移会处理的 6.x env 键(搬进结构块 / 丢弃 / 点名它拒):车道块的键 ∪ 店键。写闸拒因带不带迁移指路只看它。 */
34
34
  export declare function isMigratableEnvKey(name: string): boolean;
35
- /** 迁移命令的唯一拼写(design/327 §3.4 原句):`npx` 全名形 —— 没装本包的运维粘贴即可跑;裸 bin 名 `settings-schema …` 在没装本包的机器上是 `command not found`。 */
36
- export declare const MIGRATE_WORKERS_COMMAND: "npx @sema-agent/settings-schema migrate-workers";
37
- /** 拒因里的迁移指路 —— **一句话、一个常量**:保留名闸(`env` / `secretEnv` 里的 {@link isMigratableEnvKey} 键)与删除项预处理(七个被删旧键)引用同一个。 */
38
- export declare const MIGRATION_SIGNPOST: string;
@@ -1,14 +1,14 @@
1
1
  /**
2
- * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引** + 「迁移能搬哪些键」的**一份**键集与指路句,
3
- * 供 `types.ts`(写闸)与 `migrate-workers.ts`(一次性迁移)同读。
2
+ * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引** + 「迁移能搬哪些键」的**一份**键集,
3
+ * 供 `types.ts`(写闸)与 `one-time-migrations.ts`(一次性迁移 SS-M4)同读。
4
4
  * 不进主桶、不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势)—— 消费方只拿表本身,索引是本包的实现细节。
5
5
  * 索引只从表派生,不另存任何词:表改了,这里跟着变。
6
6
  *
7
- * ## 迁移指路(7.0.1)
7
+ * ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`)
8
8
  * 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**。
9
9
  * 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只谓词**
10
10
  * ({@link isMigratableEnvKey}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
11
- * (`test/migration-signpost-7.0.1.test.ts` 用真跑迁移的读数逐个保留名对拍)。
11
+ * (`test/migration-signpost-11.0.0.test.ts` 用真跑迁移的读数逐个保留名对拍)。
12
12
  */
13
13
  import { EXECUTION_LANES } from "./execution-lanes.js";
14
14
  /** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
@@ -40,7 +40,3 @@ export function isLaneEnvKey(name) {
40
40
  export function isMigratableEnvKey(name) {
41
41
  return isLaneEnvKey(name) || STORE_ENV_KEYS.some((k) => k === name);
42
42
  }
43
- /** 迁移命令的唯一拼写(design/327 §3.4 原句):`npx` 全名形 —— 没装本包的运维粘贴即可跑;裸 bin 名 `settings-schema …` 在没装本包的机器上是 `command not found`。 */
44
- export const MIGRATE_WORKERS_COMMAND = "npx @sema-agent/settings-schema migrate-workers";
45
- /** 拒因里的迁移指路 —— **一句话、一个常量**:保留名闸(`env` / `secretEnv` 里的 {@link isMigratableEnvKey} 键)与删除项预处理(七个被删旧键)引用同一个。 */
46
- export const MIGRATION_SIGNPOST = `lift a stored 6.x spec once with migrateWorkersDomain(doc) (on the raw document, ahead of parseDomain) or \`${MIGRATE_WORKERS_COMMAND} <file>\``;
@@ -4,9 +4,9 @@
4
4
  *
5
5
  * ## 为什么在这里
6
6
  * 车道词、每条车道读哪些 env、哪条车道分租户、存储后端词 —— 这几张表以前在四处各抄一份(引擎的能力表、
7
- * 部署面的车道清单、中心表单、本包 `RemoteExecSpec`),任何一处改了别处都不红。从本版起**只有这一份**:
8
- * · 中心写闸 = `WorkerSpec.safeParse`(本包 schema 读这张表判 `runtime.execution` / `runtime.store`);
9
- * · 中心渲染器按这张表把结构块渲成 env(唯一写者);
7
+ * 部署面的车道清单、web-admin 表单、本包 `RemoteExecSpec`),任何一处改了别处都不红。从本版起**只有这一份**:
8
+ * · web-admin 写闸 = `WorkerSpec.safeParse`(本包 schema 读这张表判 `runtime.execution` / `runtime.store`);
9
+ * · web-admin 渲染器按这张表把结构块渲成 env(唯一写者);
10
10
  * · 引擎提货时从这张表派生自己的车道词表 / 必需键表 / 租户分离位(引擎只留运行期事实,按导入联合类型穷举);
11
11
  * · 部署面车道清单由这张表生成。
12
12
  * 写闸与起服读同一张表 ⇒ 不会分叉。
@@ -15,7 +15,8 @@
15
15
  * · 车道词与次序:`src/execution-lane-caps.ts` `EXECUTION_LANE_WORDS`(:335)去掉 `in-process` =
16
16
  * `REMOTE_EXEC_PROVIDER_WORDS`(:366)。次序照抄,于是引擎改派生之后,拒启句里的 `valid:` 列表与出路列表逐字不变。
17
17
  * · `tenantSeparation`:同文件 `executionLaneCaps` 每臂的 `tenantSeparation` 位(:209 host / :230 e2b / :249 k8s /
18
- * :269 ssh / :288 adb / :308 local-docker / :327 device)。
18
+ * :269 ssh / :288 adb / :308 local-docker / :327 device)。`tenantSeparationBasis`(11.0.0):分租户的那几臂凭什么分 ——
19
+ * e2b / k8s / local-docker 每任务一只隔离目标(`isolation`),device 是发起者自己的设备(`principal-binding`)。
19
20
  * · `requiredEnv ∪ requiredSecretEnv`:`src/config.ts` `REQUIRED_ENVS_BY_LANE`(:1054)逐字;按 `src/config-catalog.ts`
20
21
  * 该键那一行的值类型分栏 —— 类型 `secret` / `opaque`(目录自己「绝不回显值」的两类)进 `*SecretEnv`,其余进 `*Env`。
21
22
  * · `optionalEnv ∪ optionalSecretEnv`:`src/config.ts` `parseOrchestrationDomain` 的 `REMOTE_EXEC` 三元链里**该车道那一臂**
@@ -31,25 +32,47 @@
31
32
  * 仍回 completed + 零工具,就是那个病换了个合法名字。要无手 worker 先另立设计、定用户面行为再给词。
32
33
  * · `remote-docker`:引擎有一只没接线的适配器工厂,但没有任何读点能把这个词变成一条在跑的车道
33
34
  * (`REMOTE_EXEC=remote-docker` 起服即拒;远端 docker 守护进程今天的真形 = `local-docker` + `DOCKER_HOST`)。
35
+ * 它在退役词墓碑 {@link RETIRED_LANE_WORDS} 里:读到即拒,拒句带出路。
34
36
  */
35
37
  /** 可声明的执行车道闭集(= 引擎 `REMOTE_EXEC` 收的词,逐字同序)。 */
36
38
  export declare const EXECUTION_LANE_WORDS: readonly ["host", "e2b", "k8s", "ssh", "adb", "local-docker", "device"];
37
39
  export type ExecutionLane = (typeof EXECUTION_LANE_WORDS)[number];
40
+ /**
41
+ * 已退役的车道**值** —— 墓碑(与退役 env 名的墓碑表 `RETIRED_ENVS` 同一条线:永久、只增不减、一跳 —— 去向恒是现行车道词)。
42
+ * 一行 = `[旧词, 去向车道, 怎么配出旧词当年要的那件事]`。读到旧词的每一处(worker 规格的 `runtime.execution.lane`、车道文件
43
+ * `config.d/remote-exec.json` 的 `provider`、引擎的 `REMOTE_EXEC`)拒,拒句取 {@link retiredLaneWordRefusal} —— 出路在源头给,一句。
44
+ */
45
+ export type RetiredLaneWord = readonly [old: string, to: ExecutionLane, how: string];
46
+ export declare const RETIRED_LANE_WORDS: readonly RetiredLaneWord[];
47
+ /** 一个车道值是不是退役词:是 ⇒ 带出路的整句;否(现行词、不认识的词、非串)⇒ `undefined`。纯函数。 */
48
+ export declare function retiredLaneWordRefusal(word: unknown): string | undefined;
38
49
  /** 车道分不分租户(引擎能力表的 `tenantSeparation` 位,两档,没有「不表态」)。
39
50
  * `supported` = 每个租户的命令落在各自的目标上;`single-user-only` = 所有任务落同一个共享目标 ⇒ 只许单用户 worker。 */
40
51
  export declare const TENANT_SEPARATION_WORDS: readonly ["supported", "single-user-only"];
41
52
  export type TenantSeparation = (typeof TENANT_SEPARATION_WORDS)[number];
53
+ /** 分租户车道**凭什么**分租户(两档):`isolation` = 每个任务一只隔离的目标(microVM / pod / 容器);`principal-binding` = 目标绑在
54
+ * 发起者身上(设备 ↔ principal 绑定),目标本身不隔离。只有 `tenantSeparation: "supported"` 的车道有依据。 */
55
+ export declare const TENANT_SEPARATION_BASES: readonly ["isolation", "principal-binding"];
56
+ export type TenantSeparationBasis = (typeof TENANT_SEPARATION_BASES)[number];
57
+ /** 车道的租户分离位与它的依据 —— 判别联合:分租户 ⇔ 依据在场(类型保证,不靠一句注释)。消费方的规则读依据列,不点名车道。 */
58
+ export type LaneTenancy = {
59
+ readonly tenantSeparation: "supported";
60
+ readonly tenantSeparationBasis: TenantSeparationBasis;
61
+ } | {
62
+ readonly tenantSeparation: "single-user-only";
63
+ readonly tenantSeparationBasis?: never;
64
+ };
42
65
  /** 一条车道的表行。四栏 env 名两两不交、跨车道也不交(`test/execution-lanes-7.0.0.test.ts` 钉)。
43
66
  * - `requiredEnv` / `optionalEnv`:非密坐标 —— 写在 `runtime.execution.coords`(字面值);
44
- * - `requiredSecretEnv` / `optionalSecretEnv`:密钥 —— 写在 `runtime.execution.secretRefs`(服务变量名 → 宿主 env-NAME,绝不写值)。 */
45
- export interface ExecutionLaneRow {
67
+ * - `requiredSecretEnv` / `optionalSecretEnv`:密钥 —— 写在 `runtime.execution.secretRefs`(服务变量名 → 宿主 env-NAME,绝不写值);
68
+ * - 租户分离位 + 依据:见 {@link LaneTenancy}。 */
69
+ export type ExecutionLaneRow = {
46
70
  readonly lane: ExecutionLane;
47
- readonly tenantSeparation: TenantSeparation;
48
71
  readonly requiredEnv: readonly string[];
49
72
  readonly requiredSecretEnv: readonly string[];
50
73
  readonly optionalEnv: readonly string[];
51
74
  readonly optionalSecretEnv: readonly string[];
52
- }
75
+ } & LaneTenancy;
53
76
  /** 车道表(次序 = {@link EXECUTION_LANE_WORDS})。整张表深冻结 —— 它是唯一正本,消费方拿到的不许被改。 */
54
77
  export declare const EXECUTION_LANES: readonly ExecutionLaneRow[];
55
78
  /** 存储后端(→ `DB_BACKEND`)。= server `src/config-types.ts` `DB_BACKEND_WORDS`(:67)逐字同序。 */