@telorun/analyzer 0.72.0 → 0.74.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 (208) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +76 -20
  3. package/dist/artifact-axes.d.ts +14 -0
  4. package/dist/artifact-axes.d.ts.map +1 -0
  5. package/dist/artifact-axes.js +12 -0
  6. package/dist/artifact-layer-index.d.ts +2 -2
  7. package/dist/artifact-layer-index.d.ts.map +1 -1
  8. package/dist/artifact-layer-index.js +9 -4
  9. package/dist/artifact-selector.d.ts +47 -21
  10. package/dist/artifact-selector.d.ts.map +1 -1
  11. package/dist/artifact-selector.js +85 -16
  12. package/dist/builtins.d.ts.map +1 -1
  13. package/dist/builtins.js +144 -113
  14. package/dist/call-graph.d.ts +12 -1
  15. package/dist/call-graph.d.ts.map +1 -1
  16. package/dist/call-graph.js +22 -0
  17. package/dist/catch-scope.js +1 -1
  18. package/dist/cel-scope-query.d.ts.map +1 -1
  19. package/dist/cel-scope-query.js +32 -11
  20. package/dist/cel-scope.d.ts.map +1 -1
  21. package/dist/cel-scope.js +1 -0
  22. package/dist/eval-paths.d.ts +36 -5
  23. package/dist/eval-paths.d.ts.map +1 -1
  24. package/dist/eval-paths.js +47 -6
  25. package/dist/extends-resolution.d.ts +12 -0
  26. package/dist/extends-resolution.d.ts.map +1 -1
  27. package/dist/extends-resolution.js +23 -0
  28. package/dist/forwarded-declaration.d.ts +26 -0
  29. package/dist/forwarded-declaration.d.ts.map +1 -0
  30. package/dist/forwarded-declaration.js +31 -0
  31. package/dist/index.d.ts +10 -4
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +5 -2
  34. package/dist/kernel-globals.d.ts.map +1 -1
  35. package/dist/kernel-globals.js +21 -5
  36. package/dist/module-alias-scope.d.ts +11 -0
  37. package/dist/module-alias-scope.d.ts.map +1 -1
  38. package/dist/module-alias-scope.js +10 -0
  39. package/dist/module-file-claims.d.ts +17 -1
  40. package/dist/module-file-claims.d.ts.map +1 -1
  41. package/dist/module-file-claims.js +38 -3
  42. package/dist/module-named-files.d.ts +72 -0
  43. package/dist/module-named-files.d.ts.map +1 -0
  44. package/dist/module-named-files.js +115 -0
  45. package/dist/native-entries.d.ts +83 -0
  46. package/dist/native-entries.d.ts.map +1 -0
  47. package/dist/native-entries.js +169 -0
  48. package/dist/nearest-name.d.ts +22 -0
  49. package/dist/nearest-name.d.ts.map +1 -0
  50. package/dist/nearest-name.js +43 -0
  51. package/dist/normalize-inline-resources.d.ts +41 -13
  52. package/dist/normalize-inline-resources.d.ts.map +1 -1
  53. package/dist/normalize-inline-resources.js +236 -54
  54. package/dist/reference-field-map.d.ts +59 -1
  55. package/dist/reference-field-map.d.ts.map +1 -1
  56. package/dist/reference-field-map.js +200 -34
  57. package/dist/release/destinations.d.ts +59 -0
  58. package/dist/release/destinations.d.ts.map +1 -0
  59. package/dist/release/destinations.js +75 -0
  60. package/dist/release/index.d.ts +6 -2
  61. package/dist/release/index.d.ts.map +1 -1
  62. package/dist/release/index.js +3 -1
  63. package/dist/release/ledger.d.ts +20 -11
  64. package/dist/release/ledger.d.ts.map +1 -1
  65. package/dist/release/ledger.js +43 -14
  66. package/dist/release/payload-digest.d.ts +2 -1
  67. package/dist/release/payload-digest.d.ts.map +1 -1
  68. package/dist/release/payload-digest.js +2 -1
  69. package/dist/release/release-plan.d.ts +3 -2
  70. package/dist/release/release-plan.d.ts.map +1 -1
  71. package/dist/release/release-plan.js +12 -19
  72. package/dist/release/workspace-config.d.ts +114 -34
  73. package/dist/release/workspace-config.d.ts.map +1 -1
  74. package/dist/release/workspace-config.js +322 -38
  75. package/dist/release/workspace-schema.d.ts +41 -0
  76. package/dist/release/workspace-schema.d.ts.map +1 -0
  77. package/dist/release/workspace-schema.js +77 -0
  78. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  79. package/dist/resolve-ref-sentinels.js +8 -5
  80. package/dist/resolve-throws-union.d.ts +25 -4
  81. package/dist/resolve-throws-union.d.ts.map +1 -1
  82. package/dist/resolve-throws-union.js +62 -32
  83. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  84. package/dist/resolve-zone-requirements.js +2 -1
  85. package/dist/rewrite-synthetic-origins.d.ts +6 -1
  86. package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
  87. package/dist/rewrite-synthetic-origins.js +107 -13
  88. package/dist/schema-compat.d.ts.map +1 -1
  89. package/dist/schema-compat.js +18 -5
  90. package/dist/schema-error-report.d.ts +7 -0
  91. package/dist/schema-error-report.d.ts.map +1 -1
  92. package/dist/schema-error-report.js +9 -4
  93. package/dist/schema-walk.d.ts +47 -13
  94. package/dist/schema-walk.d.ts.map +1 -1
  95. package/dist/schema-walk.js +150 -30
  96. package/dist/scope-declarations.d.ts +59 -0
  97. package/dist/scope-declarations.d.ts.map +1 -0
  98. package/dist/scope-declarations.js +45 -0
  99. package/dist/source-entries.d.ts +119 -0
  100. package/dist/source-entries.d.ts.map +1 -0
  101. package/dist/source-entries.js +349 -0
  102. package/dist/step-bodies.d.ts +24 -0
  103. package/dist/step-bodies.d.ts.map +1 -0
  104. package/dist/step-bodies.js +28 -0
  105. package/dist/telo-version.d.ts +1 -1
  106. package/dist/telo-version.js +1 -1
  107. package/dist/template-body.d.ts +10 -0
  108. package/dist/template-body.d.ts.map +1 -1
  109. package/dist/template-body.js +25 -0
  110. package/dist/validate-cel-context.d.ts +23 -8
  111. package/dist/validate-cel-context.d.ts.map +1 -1
  112. package/dist/validate-cel-context.js +51 -18
  113. package/dist/validate-exports.d.ts +28 -0
  114. package/dist/validate-exports.d.ts.map +1 -0
  115. package/dist/validate-exports.js +146 -0
  116. package/dist/validate-extends.d.ts.map +1 -1
  117. package/dist/validate-extends.js +37 -1
  118. package/dist/validate-module-artifact.d.ts +4 -2
  119. package/dist/validate-module-artifact.d.ts.map +1 -1
  120. package/dist/validate-module-artifact.js +43 -4
  121. package/dist/validate-native-entries.d.ts +15 -0
  122. package/dist/validate-native-entries.d.ts.map +1 -0
  123. package/dist/validate-native-entries.js +103 -0
  124. package/dist/validate-observed-state.d.ts +7 -1
  125. package/dist/validate-observed-state.d.ts.map +1 -1
  126. package/dist/validate-observed-state.js +24 -6
  127. package/dist/validate-provider-coherence.d.ts +4 -8
  128. package/dist/validate-provider-coherence.d.ts.map +1 -1
  129. package/dist/validate-provider-coherence.js +12 -123
  130. package/dist/validate-reference-forms.d.ts.map +1 -1
  131. package/dist/validate-reference-forms.js +2 -2
  132. package/dist/validate-references.d.ts.map +1 -1
  133. package/dist/validate-references.js +152 -65
  134. package/dist/validate-scope-reach.d.ts +31 -0
  135. package/dist/validate-scope-reach.d.ts.map +1 -0
  136. package/dist/validate-scope-reach.js +180 -0
  137. package/dist/validate-source-entries.d.ts +16 -0
  138. package/dist/validate-source-entries.d.ts.map +1 -0
  139. package/dist/validate-source-entries.js +90 -0
  140. package/dist/validate-template-body.d.ts +36 -0
  141. package/dist/validate-template-body.d.ts.map +1 -0
  142. package/dist/validate-template-body.js +246 -0
  143. package/dist/validate-throws-coverage.d.ts.map +1 -1
  144. package/dist/validate-throws-coverage.js +21 -39
  145. package/dist/value-type-keyword.d.ts +22 -5
  146. package/dist/value-type-keyword.d.ts.map +1 -1
  147. package/dist/value-type-keyword.js +16 -7
  148. package/package.json +4 -3
  149. package/src/analyzer.ts +97 -20
  150. package/src/artifact-axes.ts +24 -0
  151. package/src/artifact-layer-index.ts +9 -6
  152. package/src/artifact-selector.ts +110 -35
  153. package/src/builtins.ts +160 -113
  154. package/src/call-graph.ts +30 -0
  155. package/src/catch-scope.ts +1 -1
  156. package/src/cel-scope-query.ts +28 -9
  157. package/src/cel-scope.ts +1 -0
  158. package/src/eval-paths.ts +53 -6
  159. package/src/extends-resolution.ts +24 -0
  160. package/src/forwarded-declaration.ts +32 -0
  161. package/src/index.ts +37 -1
  162. package/src/kernel-globals.ts +28 -5
  163. package/src/migrations/CLAUDE.md +35 -0
  164. package/src/module-alias-scope.ts +17 -0
  165. package/src/module-file-claims.ts +63 -4
  166. package/src/module-named-files.ts +162 -0
  167. package/src/native-entries.ts +227 -0
  168. package/src/nearest-name.ts +47 -0
  169. package/src/normalize-inline-resources.ts +326 -70
  170. package/src/reference-field-map.ts +257 -52
  171. package/src/release/CLAUDE.md +32 -0
  172. package/src/release/destinations.ts +105 -0
  173. package/src/release/index.ts +33 -2
  174. package/src/release/ledger.ts +51 -21
  175. package/src/release/payload-digest.ts +2 -1
  176. package/src/release/release-plan.ts +16 -26
  177. package/src/release/workspace-config.ts +483 -47
  178. package/src/release/workspace-schema.ts +99 -0
  179. package/src/resolve-ref-sentinels.ts +7 -6
  180. package/src/resolve-throws-union.ts +87 -32
  181. package/src/resolve-zone-requirements.ts +2 -1
  182. package/src/rewrite-synthetic-origins.ts +107 -16
  183. package/src/schema-compat.ts +18 -4
  184. package/src/schema-error-report.ts +16 -4
  185. package/src/schema-walk.ts +162 -34
  186. package/src/scope-declarations.ts +98 -0
  187. package/src/source-entries.ts +502 -0
  188. package/src/step-bodies.ts +55 -0
  189. package/src/telo-version.ts +1 -1
  190. package/src/template-body.ts +25 -0
  191. package/src/validate-cel-context.ts +60 -18
  192. package/src/validate-exports.ts +185 -0
  193. package/src/validate-extends.ts +43 -0
  194. package/src/validate-module-artifact.ts +46 -3
  195. package/src/validate-native-entries.ts +147 -0
  196. package/src/validate-observed-state.ts +36 -7
  197. package/src/validate-provider-coherence.ts +17 -127
  198. package/src/validate-reference-forms.ts +2 -3
  199. package/src/validate-references.ts +151 -66
  200. package/src/validate-scope-reach.ts +213 -0
  201. package/src/validate-source-entries.ts +120 -0
  202. package/src/validate-template-body.ts +274 -0
  203. package/src/validate-throws-coverage.ts +24 -31
  204. package/src/value-type-keyword.ts +35 -9
  205. package/dist/validate-template-dispatch.d.ts +0 -27
  206. package/dist/validate-template-dispatch.d.ts.map +0 -1
  207. package/dist/validate-template-dispatch.js +0 -95
  208. package/src/validate-template-dispatch.ts +0 -99
@@ -1,5 +1,23 @@
1
+ import { resolveLocalRef } from "./manifest-navigation.js";
1
2
  import { readRefSlot } from "./ref-slot.js";
3
+ import { readStepSlot } from "./step-slot.js";
2
4
  export { readRefSlot, isRefSlot, hasDeclaredUse } from "./ref-slot.js";
5
+ /** The slot an entry records, as `readRefSlot` read it. */
6
+ export function refSlotOfEntry(entry) {
7
+ const slot = {
8
+ kinds: entry.refs,
9
+ uses: entry.uses,
10
+ inline: entry.inline === true,
11
+ valueBranches: entry.valueBranches ?? [],
12
+ };
13
+ if (entry.useCases)
14
+ slot.useCases = entry.useCases;
15
+ if (entry.inputs !== undefined)
16
+ slot.inputs = entry.inputs;
17
+ if (entry.throwsThrough)
18
+ slot.throwsThrough = true;
19
+ return slot;
20
+ }
3
21
  export function isRefEntry(entry) {
4
22
  return "refs" in entry;
5
23
  }
@@ -121,9 +139,10 @@ export function resolveFieldValues(obj, path) {
121
139
  */
122
140
  export function buildReferenceFieldMap(schema) {
123
141
  const map = new Map();
142
+ const sink = injectionSink(map);
124
143
  if (schema.properties) {
125
144
  for (const [key, propSchema] of Object.entries(schema.properties)) {
126
- traverseNode(propSchema, key, map, schema);
145
+ traverseNode(propSchema, key, sink, schema);
127
146
  }
128
147
  }
129
148
  return map;
@@ -140,32 +159,175 @@ export function collectRefs(node) {
140
159
  * hid behind the schema-from indirection. */
141
160
  export function buildFieldMapAtPath(schema, pathPrefix) {
142
161
  const map = new Map();
143
- traverseNode(schema, pathPrefix, map, schema);
162
+ traverseNode(schema, pathPrefix, injectionSink(map), schema);
144
163
  return map;
145
164
  }
146
- function traverseNode(node, path, map, root, visitedRefs = new Set()) {
147
- // Local `$ref` is intentionally NOT followed. This map is the kernel's
165
+ const drivenSlotMaps = new WeakMap();
166
+ /** The resource envelope: present on every resource document, and never
167
+ * configuration a kind's root `additionalProperties` describes. */
168
+ const RESOURCE_ENVELOPE_KEYS = ["kind", "metadata"];
169
+ /**
170
+ * Every slot through which a resource of this schema DRIVES another — the
171
+ * reference field map's own traversal, plus what a dispatch analysis needs and
172
+ * the injection surface must not have:
173
+ *
174
+ * - local `$ref` is followed, resolved against the root. The injection map
175
+ * stops there so a step's `invoke` never becomes an injection site; this map
176
+ * stops AT a step body instead, which removes that reason. A reference back
177
+ * to a node already on the descent is recorded as a back-edge rather than
178
+ * unrolled, so a recursive shape stays finite here and is followed as deep as
179
+ * the data goes by `forEachDrivenSlot`;
180
+ * - a step body is recorded and not descended — the step traversal owns
181
+ * everything below it;
182
+ * - the root's own variant branches and `additionalProperties` are walked, not
183
+ * only its `properties`;
184
+ * - a path keeps EVERY slot any branch declares there, where the injection map
185
+ * keeps the last one written (see {@link DrivenPath.refs});
186
+ * - each `{}` path records the keys its `additionalProperties` does not cover.
187
+ *
188
+ * Shared by every throws question (a kind's `inherit` union, a scope list's
189
+ * denominator, catch-scope enclosure, and whether `inherit` is legal at all),
190
+ * so none can reach a slot another cannot. Memoized per schema object.
191
+ */
192
+ export function buildDrivenSlotMap(schema) {
193
+ const cached = drivenSlotMaps.get(schema);
194
+ if (cached)
195
+ return cached;
196
+ const slots = { paths: new Map(), declaredKeys: new Map(), drives: false };
197
+ traverseNode(schema, "", drivenSink(slots), schema);
198
+ drivenSlotMaps.set(schema, slots);
199
+ return slots;
200
+ }
201
+ const joinPath = (path, key) => (path ? `${path}.${key}` : key);
202
+ /** The injection map: one entry per path, the last one written. Step bodies,
203
+ * back-edges and map-value keys are the driven map's alone. */
204
+ function injectionSink(map) {
205
+ return {
206
+ driven: false,
207
+ ref: (path, entry) => {
208
+ map.set(path, entry);
209
+ },
210
+ stop: (path, entry) => {
211
+ map.set(path, entry);
212
+ },
213
+ step: () => { },
214
+ recurse: () => { },
215
+ mapValue: () => { },
216
+ };
217
+ }
218
+ function drivenSink(slots) {
219
+ const recorded = new Map();
220
+ const at = (path) => {
221
+ let entry = slots.paths.get(path);
222
+ if (!entry) {
223
+ entry = { refs: [], steps: [], recurse: [] };
224
+ slots.paths.set(path, entry);
225
+ }
226
+ return entry;
227
+ };
228
+ // One schema node reached twice at one path (two branches `$ref`-ing one
229
+ // definition) is one slot.
230
+ const firstTime = (path, node) => {
231
+ let nodes = recorded.get(path);
232
+ if (!nodes)
233
+ recorded.set(path, (nodes = new Set()));
234
+ if (nodes.has(node))
235
+ return false;
236
+ nodes.add(node);
237
+ return true;
238
+ };
239
+ return {
240
+ driven: true,
241
+ ref: (path, entry, node) => {
242
+ if (!firstTime(path, node))
243
+ return;
244
+ at(path).refs.push(entry);
245
+ slots.drives = true;
246
+ },
247
+ stop: () => { },
248
+ step: (path, step, node) => {
249
+ if (!firstTime(path, node))
250
+ return;
251
+ at(path).steps.push(step);
252
+ slots.drives = true;
253
+ },
254
+ recurse: (path, to) => {
255
+ const entry = at(path);
256
+ if (!entry.recurse.includes(to))
257
+ entry.recurse.push(to);
258
+ },
259
+ mapValue: (mapPath, declared) => {
260
+ const previous = slots.declaredKeys.get(mapPath);
261
+ slots.declaredKeys.set(mapPath, new Set(previous ? declared.filter((key) => previous.has(key)) : declared));
262
+ },
263
+ };
264
+ }
265
+ /** Follow a local `$ref` in driven mode. `onStack` maps each node on the
266
+ * current descent to the path it was entered at: re-entering one records a
267
+ * back-edge to that path, and a cycle that made no structural progress (same
268
+ * path) records nothing, since its body is already recorded there. */
269
+ function followLocalRef(node, path, sink, root, onStack) {
270
+ if (!root)
271
+ return;
272
+ const target = resolveLocalRef(node, root);
273
+ if (!target || target === node)
274
+ return;
275
+ const entered = onStack.get(target);
276
+ if (entered !== undefined) {
277
+ if (entered !== path)
278
+ sink.recurse(path, entered);
279
+ return;
280
+ }
281
+ onStack.set(target, path);
282
+ try {
283
+ traverseNode(target, path, sink, root, onStack);
284
+ }
285
+ finally {
286
+ onStack.delete(target);
287
+ }
288
+ }
289
+ /** `owner.additionalProperties`, the schema of every value in an open-keyed
290
+ * object. JSON Schema applies it only to keys `owner` does not declare, which
291
+ * the driven map records so a walk skips them. */
292
+ function traverseMapValue(owner, path, sink, root, onStack) {
293
+ const valueSchema = owner.additionalProperties;
294
+ if (!valueSchema || typeof valueSchema !== "object" || Array.isArray(valueSchema))
295
+ return;
296
+ const mapPath = joinPath(path, "{}");
297
+ const declared = owner.properties && typeof owner.properties === "object" ? Object.keys(owner.properties) : [];
298
+ sink.mapValue(mapPath, path === "" ? [...declared, ...RESOURCE_ENVELOPE_KEYS] : declared);
299
+ traverseNode(valueSchema, mapPath, sink, root, onStack);
300
+ }
301
+ function traverseNode(node, path, sink, root, onStack = new Map()) {
302
+ const driven = sink.driven;
303
+ // Local `$ref` is intentionally NOT followed here. This map is the kernel's
148
304
  // Phase-5 injection surface: descending into shared `$defs` (notably
149
305
  // `Run.Sequence`'s `step` definition) would make every step's `invoke` an
150
306
  // injection site, and step slots resolve at dispatch — injecting there is
151
307
  // unwanted regardless of tracing (the original dispatcher-bypass blocker
152
308
  // has since shipped via the `REF_IDENTITY` stamp). Static analysis is NOT
153
309
  // limited by this stop: the call graph (`call-graph.ts`) reads step slots
154
- // from the item schema itself and scans the value tree, so it sees what
155
- // this map deliberately hides. `visitedRefs` stays as a parameter so the
156
- // recursive calls below thread the right signature.
157
- if (typeof node?.$ref === "string")
310
+ // from the item schema itself and scans the value tree, and the driven-slot
311
+ // mode (`buildDrivenSlotMap`) follows the reference.
312
+ if (typeof node?.$ref === "string" && !driven)
158
313
  return;
159
314
  // Scope slot — record and stop; do not recurse into scope contents
160
315
  if ("x-telo-scope" in node) {
161
- map.set(path, { scope: node["x-telo-scope"] });
316
+ sink.stop(path, { scope: node["x-telo-scope"] });
162
317
  return;
163
318
  }
164
319
  // Schema-from slot — record and stop; no further traversal needed
165
320
  if ("x-telo-schema-from" in node) {
166
- map.set(path, { schemaFrom: node["x-telo-schema-from"] });
321
+ sink.stop(path, { schemaFrom: node["x-telo-schema-from"] });
167
322
  return;
168
323
  }
324
+ if (driven) {
325
+ const step = readStepSlot(node);
326
+ if (step) {
327
+ sink.step(path, step, node);
328
+ return;
329
+ }
330
+ }
169
331
  // Reference slot (direct, via a `kind:` list, or via anyOf)
170
332
  const slot = readRefSlot(node);
171
333
  if (slot && slot.kinds.length > 0) {
@@ -184,7 +346,9 @@ function traverseNode(node, path, map, root, visitedRefs = new Set()) {
184
346
  entry.context = node["x-telo-context"];
185
347
  if (slot.inline)
186
348
  entry.inline = true;
187
- map.set(path, entry);
349
+ if (slot.throwsThrough)
350
+ entry.throwsThrough = true;
351
+ sink.ref(path, entry, node);
188
352
  // A node can mix item-level ref branches (a bare string / `{kind, name}`)
189
353
  // with object branches that carry their OWN nested refs — e.g. Application
190
354
  // `targets`: a bare ref vs inline `{ invoke }` vs gated `{ ref }`. Descend
@@ -198,21 +362,24 @@ function traverseNode(node, path, map, root, visitedRefs = new Set()) {
198
362
  for (const variant of variants) {
199
363
  if (!variant || typeof variant !== "object")
200
364
  continue;
201
- traverseVariant(variant, path, map, root, visitedRefs);
365
+ traverseVariant(variant, path, sink, root, onStack);
202
366
  }
203
367
  }
204
368
  return;
205
369
  }
206
- if (typeof node?.$ref === "string")
370
+ // Reached only in driven mode — the injection map stopped at the top.
371
+ if (typeof node?.$ref === "string") {
372
+ followLocalRef(node, path, sink, root, onStack);
207
373
  return;
374
+ }
208
375
  // Array — recurse into items
209
376
  if (node.type === "array" && node.items) {
210
- traverseNode(node.items, path + "[]", map, root, visitedRefs);
377
+ traverseNode(node.items, path + "[]", sink, root, onStack);
211
378
  }
212
379
  // Object — recurse into properties
213
380
  if (node.properties) {
214
381
  for (const [key, propSchema] of Object.entries(node.properties)) {
215
- traverseNode(propSchema, `${path}.${key}`, map, root, visitedRefs);
382
+ traverseNode(propSchema, joinPath(path, key), sink, root, onStack);
216
383
  }
217
384
  }
218
385
  // Variant branches — descend into every alternative's properties / items.
@@ -221,10 +388,11 @@ function traverseNode(node, path, map, root, visitedRefs = new Set()) {
221
388
  // slots inside the branch. Walking each branch surfaces those slots into
222
389
  // the field map so downstream passes (ref validation, sentinel
223
390
  // resolution, dependency graph) cover them without a runtime fallback.
224
- // The same field path may be added by multiple branches; the later
225
- // assignment wins, which is fine — branches with the same field path
226
- // share the same ref/context configuration (any divergence is already
227
- // a schema bug).
391
+ // The same field path may be added by multiple branches. The injection map
392
+ // keeps the later assignment, which is fine for injection — branches with
393
+ // the same field path share the same ref/context configuration. The driven
394
+ // map keeps every one, because which branch applies decides what a
395
+ // dispatch can throw.
228
396
  for (const variantKey of ["oneOf", "anyOf", "allOf"]) {
229
397
  const variants = node[variantKey];
230
398
  if (!Array.isArray(variants))
@@ -232,34 +400,32 @@ function traverseNode(node, path, map, root, visitedRefs = new Set()) {
232
400
  for (const variant of variants) {
233
401
  if (!variant || typeof variant !== "object")
234
402
  continue;
235
- traverseVariant(variant, path, map, root, visitedRefs);
403
+ traverseVariant(variant, path, sink, root, onStack);
236
404
  }
237
405
  }
238
406
  // Map — `additionalProperties: { ... }` describes every value in an
239
407
  // open-keyed object. Encoder refs nested inside `content[mime]` map
240
408
  // entries reach Phase 5 through this branch.
241
- if (node.additionalProperties &&
242
- typeof node.additionalProperties === "object" &&
243
- !Array.isArray(node.additionalProperties)) {
244
- traverseNode(node.additionalProperties, `${path}.{}`, map, root, visitedRefs);
245
- }
409
+ traverseMapValue(node, path, sink, root, onStack);
246
410
  }
247
411
  /** Walk a single variant of a `oneOf` / `anyOf` / `allOf` branch. Only
248
412
  * the properties / items / map slots are followed — collectRefs at the
249
413
  * variant root is handled by the parent's `collectRefs(node)` already
250
- * (anyOf of x-telo-ref branches is the canonical multi-ref shape). */
251
- function traverseVariant(variant, path, map, root, visitedRefs = new Set()) {
414
+ * (anyOf of x-telo-ref branches is the canonical multi-ref shape). In
415
+ * driven mode a `$ref` branch is followed as a node in its own right, since
416
+ * `readRefSlot` does not look through one. */
417
+ function traverseVariant(variant, path, sink, root, onStack = new Map()) {
418
+ if (sink.driven && typeof variant.$ref === "string") {
419
+ followLocalRef(variant, path, sink, root, onStack);
420
+ return;
421
+ }
252
422
  if (variant.properties) {
253
423
  for (const [key, propSchema] of Object.entries(variant.properties)) {
254
- traverseNode(propSchema, `${path}.${key}`, map, root, visitedRefs);
424
+ traverseNode(propSchema, joinPath(path, key), sink, root, onStack);
255
425
  }
256
426
  }
257
427
  if (variant.type === "array" && variant.items) {
258
- traverseNode(variant.items, path + "[]", map, root, visitedRefs);
259
- }
260
- if (variant.additionalProperties &&
261
- typeof variant.additionalProperties === "object" &&
262
- !Array.isArray(variant.additionalProperties)) {
263
- traverseNode(variant.additionalProperties, `${path}.{}`, map, root, visitedRefs);
428
+ traverseNode(variant.items, path + "[]", sink, root, onStack);
264
429
  }
430
+ traverseMapValue(variant, path, sink, root, onStack);
265
431
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Where each module publishes, and the two ways a set of destinations can be
3
+ * inconsistent.
4
+ *
5
+ * A destination is the module's resolved registry base plus its own directory
6
+ * name. That rule held silently for as long as every module sat at one depth
7
+ * under one base with a unique directory name; per-subtree bases remove that
8
+ * coincidence, so both failures it was hiding become reachable and are checked
9
+ * here — **at plan time, before any payload is built**, because the payload
10
+ * builder's own refusal speaks about a manifest published to two places rather
11
+ * than about the workspace file that said so.
12
+ *
13
+ * Pure data in, diagnostics out: the destinations are already computed for
14
+ * `telo release order`, so one derivation feeds that payload and both checks.
15
+ */
16
+ import type { ModuleKey } from "./fragment.js";
17
+ import type { ReleaseDiagnostic } from "./release-plan.js";
18
+ export interface ModuleDestination {
19
+ readonly key: ModuleKey;
20
+ /** `<registry>/<the module's own directory name>`. */
21
+ readonly destination: string;
22
+ }
23
+ /**
24
+ * A relative import between two workspace modules, with both answers about
25
+ * where the target publishes.
26
+ *
27
+ * `derived` is what the importer's own destination yields when the import path
28
+ * is applied to it — the transport's rule, and what ref canonicalization writes
29
+ * into the published manifest. `assigned` is what discovery independently gives
30
+ * that module.
31
+ */
32
+ export interface ImportDestination {
33
+ readonly from: ModuleKey;
34
+ readonly to: ModuleKey;
35
+ readonly derived: string;
36
+ readonly assigned: string;
37
+ }
38
+ /**
39
+ * Two modules must not resolve to one ref.
40
+ *
41
+ * Nothing else catches it: the payload builder is keyed by manifest, so it sees
42
+ * one module claimed by two destinations and never two modules claiming one,
43
+ * while the ledger keys by module — both entries would record digests for a
44
+ * single published artifact and reconciliation could never settle.
45
+ */
46
+ export declare function checkDestinationCollisions(destinations: readonly ModuleDestination[]): ReleaseDiagnostic[];
47
+ /**
48
+ * A relative import is valid only where the importer's derived ref for its
49
+ * target equals the destination that module is independently assigned.
50
+ *
51
+ * Equal registries are necessary and not sufficient — two modules under one base
52
+ * at different directory depths derive differently too. Making the builder take
53
+ * the assigned destination instead is not the fix: it would put a ref in the
54
+ * artifact that the transport's resolution rule does not produce, so the
55
+ * manifest would say one thing and every consumer resolving relatively would
56
+ * compute another.
57
+ */
58
+ export declare function checkImportDestinations(edges: readonly ImportDestination[]): ReleaseDiagnostic[];
59
+ //# sourceMappingURL=destinations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"destinations.d.ts","sourceRoot":"","sources":["../../src/release/destinations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,sDAAsD;IACtD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,SAAS,iBAAiB,EAAE,GACzC,iBAAiB,EAAE,CAsBrB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,iBAAiB,EAAE,GAClC,iBAAiB,EAAE,CAgBrB"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Where each module publishes, and the two ways a set of destinations can be
3
+ * inconsistent.
4
+ *
5
+ * A destination is the module's resolved registry base plus its own directory
6
+ * name. That rule held silently for as long as every module sat at one depth
7
+ * under one base with a unique directory name; per-subtree bases remove that
8
+ * coincidence, so both failures it was hiding become reachable and are checked
9
+ * here — **at plan time, before any payload is built**, because the payload
10
+ * builder's own refusal speaks about a manifest published to two places rather
11
+ * than about the workspace file that said so.
12
+ *
13
+ * Pure data in, diagnostics out: the destinations are already computed for
14
+ * `telo release order`, so one derivation feeds that payload and both checks.
15
+ */
16
+ /**
17
+ * Two modules must not resolve to one ref.
18
+ *
19
+ * Nothing else catches it: the payload builder is keyed by manifest, so it sees
20
+ * one module claimed by two destinations and never two modules claiming one,
21
+ * while the ledger keys by module — both entries would record digests for a
22
+ * single published artifact and reconciliation could never settle.
23
+ */
24
+ export function checkDestinationCollisions(destinations) {
25
+ const byDestination = new Map();
26
+ for (const { key, destination } of destinations) {
27
+ const sharing = byDestination.get(destination);
28
+ if (sharing)
29
+ sharing.push(key);
30
+ else
31
+ byDestination.set(destination, [key]);
32
+ }
33
+ const diagnostics = [];
34
+ for (const [destination, keys] of byDestination) {
35
+ if (keys.length < 2)
36
+ continue;
37
+ diagnostics.push({
38
+ severity: "error",
39
+ code: "DESTINATION_COLLISION",
40
+ message: `${keys.sort().join(" and ")} both publish to '${destination}'. A module's ref is its ` +
41
+ `registry base plus its own directory name, so they would overwrite one artifact and ` +
42
+ `their ledger entries could never reconcile. Rename one directory, or give one of the ` +
43
+ `subtrees its own 'registry:' in telo-workspace.yaml.`,
44
+ });
45
+ }
46
+ return diagnostics;
47
+ }
48
+ /**
49
+ * A relative import is valid only where the importer's derived ref for its
50
+ * target equals the destination that module is independently assigned.
51
+ *
52
+ * Equal registries are necessary and not sufficient — two modules under one base
53
+ * at different directory depths derive differently too. Making the builder take
54
+ * the assigned destination instead is not the fix: it would put a ref in the
55
+ * artifact that the transport's resolution rule does not produce, so the
56
+ * manifest would say one thing and every consumer resolving relatively would
57
+ * compute another.
58
+ */
59
+ export function checkImportDestinations(edges) {
60
+ const diagnostics = [];
61
+ for (const edge of edges) {
62
+ if (edge.derived === edge.assigned)
63
+ continue;
64
+ diagnostics.push({
65
+ severity: "error",
66
+ code: "IMPORT_DESTINATION_CONFLICT",
67
+ message: `${edge.from} imports ${edge.to} by relative path, which canonicalizes to ` +
68
+ `'${edge.derived}' — but ${edge.to} publishes to '${edge.assigned}'. Publishing rewrites ` +
69
+ `the import to the ref its own path yields, so the artifact would name a module nobody ` +
70
+ `pushes. Give the two the same registry base and the same directory depth, or make it a ` +
71
+ `pinned remote import, which is what a dependency across a publish boundary is.`,
72
+ });
73
+ }
74
+ return diagnostics;
75
+ }
@@ -21,6 +21,10 @@ export type { LayerChange, LayerDigests } from "./payload-digest.js";
21
21
  export { orderByImports, planRelease } from "./release-plan.js";
22
22
  export type { ArtifactKind, BumpReason, ChangelogEntry, ModuleEvidence, PlannedModule, ReleaseDiagnostic, ReleaseEvidence, ReleasePlan, } from "./release-plan.js";
23
23
  export { VersionStampError, readManifestVersion, stampCrateVersion, stampManifestVersion, stampSelfNpmPins, stampPackageVersion, } from "./version-stamp.js";
24
- export { WORKSPACE_FILENAME, WorkspaceConfigError, parseWorkspaceConfig } from "./workspace-config.js";
25
- export type { WorkspaceConfig } from "./workspace-config.js";
24
+ export { checkDestinationCollisions, checkImportDestinations } from "./destinations.js";
25
+ export type { ImportDestination, ModuleDestination } from "./destinations.js";
26
+ export { DEFAULT_ENV_FILES, DEFAULT_RELEASE_IGNORE, WORKSPACE_FILENAME, WorkspaceConfigError, diagnosticsFor, hasError, matchesPatterns, readWorkspaceConfig, requireReleaseSettings, settingsForModule, } from "./workspace-config.js";
27
+ export type { EnvSettings, ModuleEntry, ModuleSettings, PatternMatch, ReleaseSettings, WorkspaceConfig, WorkspaceDiagnostic, WorkspaceDiagnosticCode, WorkspaceRead, } from "./workspace-config.js";
28
+ export { MODULE_ENTRY_KEYS, WORKSPACE_BLOCKS, WORKSPACE_SCHEMA, } from "./workspace-schema.js";
29
+ export type { WorkspaceBlockSchema, WorkspaceKeySchema } from "./workspace-schema.js";
26
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/release/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,QAAQ,GACT,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/D,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACpG,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnG,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,gBAAgB,EAChB,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAErE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChE,YAAY,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACvG,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/release/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,QAAQ,GACT,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/D,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACpG,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnG,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,gBAAgB,EAChB,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAErE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChE,YAAY,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACxF,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE9E,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,EAClB,oBAAoB,EACpB,cAAc,EACd,QAAQ,EACR,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,YAAY,EACZ,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,uBAAuB,EACvB,aAAa,GACd,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -15,4 +15,6 @@ export { EMPTY_LEDGER, LEDGER_PATH, LedgerError, parseLedger, serializeLedger }
15
15
  export { LOCALLY_DERIVED_LAYERS, MANIFEST_LAYER, diffLayerDigests, layerDigestKey, } from "./payload-digest.js";
16
16
  export { orderByImports, planRelease } from "./release-plan.js";
17
17
  export { VersionStampError, readManifestVersion, stampCrateVersion, stampManifestVersion, stampSelfNpmPins, stampPackageVersion, } from "./version-stamp.js";
18
- export { WORKSPACE_FILENAME, WorkspaceConfigError, parseWorkspaceConfig } from "./workspace-config.js";
18
+ export { checkDestinationCollisions, checkImportDestinations } from "./destinations.js";
19
+ export { DEFAULT_ENV_FILES, DEFAULT_RELEASE_IGNORE, WORKSPACE_FILENAME, WorkspaceConfigError, diagnosticsFor, hasError, matchesPatterns, readWorkspaceConfig, requireReleaseSettings, settingsForModule, } from "./workspace-config.js";
20
+ export { MODULE_ENTRY_KEYS, WORKSPACE_BLOCKS, WORKSPACE_SCHEMA, } from "./workspace-schema.js";
@@ -14,10 +14,22 @@
14
14
  * drift: it means nothing is published, which is the correct reading for a
15
15
  * module that has never shipped.
16
16
  *
17
- * **It records the registry base**, because canonicalizing a relative `imports:`
18
- * source writes the destination into the manifest layer — so the digests below
19
- * are digests *against that base*, and comparing them to digests taken against
20
- * another one would be comparing two different artifacts.
17
+ * **Every entry records its own registry base**, because canonicalizing a
18
+ * relative `imports:` source writes the destination into the manifest layer — so
19
+ * each digest is a digest *against that base*, and comparing it to one taken
20
+ * against another would be comparing two different artifacts.
21
+ *
22
+ * Per entry rather than one top-level base with per-entry deltas: a workspace
23
+ * whose `release.modules` entries each author a destination has no meaningful
24
+ * top-level base, an absent one already means *nothing has been published yet*,
25
+ * and the agreement check returns early on that — so the multi-destination
26
+ * workspace would be the one whose bases are never compared. The file is
27
+ * generated and never hand-maintained, so the redundancy costs nothing and it
28
+ * removes the "differs from what?" question and the absent-versus-unknown
29
+ * conflation together. A **top-level `registry:` is a legacy form the reader
30
+ * accepts and never writes**, applied to every entry: the credential-free PR
31
+ * gate reads whatever ledger is committed on the branch, and that stays in the
32
+ * old shape until a release regenerates it.
21
33
  */
22
34
  import { type ModuleKey } from "./fragment.js";
23
35
  import type { LayerDigests } from "./payload-digest.js";
@@ -26,16 +38,13 @@ export interface LedgerEntry {
26
38
  /** The version these digests were taken at — the tag the artifact published
27
39
  * under. */
28
40
  readonly version: string;
41
+ /** The publish destination base these digests were taken against
42
+ * (`oci://ghcr.io/telorun`). Absent only in a ledger written before the base
43
+ * was recorded at all. */
44
+ readonly registry?: string;
29
45
  readonly layers: LayerDigests;
30
46
  }
31
47
  export interface Ledger {
32
- /**
33
- * The publish destination base the digests were taken against
34
- * (`oci://ghcr.io/telorun`). Absent in a workspace that has published nothing
35
- * yet, which is why it is optional rather than required — but a `check` that
36
- * has entries and no base cannot reproduce them, and says so.
37
- */
38
- readonly registry?: string;
39
48
  readonly modules: ReadonlyMap<ModuleKey, LedgerEntry>;
40
49
  }
41
50
  export declare class LedgerError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/release/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,eAAO,MAAM,WAAW,yBAAyB,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B;iBACa;IACb,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B;AAED,MAAM,WAAW,MAAM;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;CACvD;AAED,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAEzC,eAAO,MAAM,YAAY,EAAE,MAA+B,CAAC;AAE3D,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CA+D/D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAkBtD"}
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/release/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,eAAO,MAAM,WAAW,yBAAyB,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B;iBACa;IACb,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;+BAE2B;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;CACvD;AAED,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAEzC,eAAO,MAAM,YAAY,EAAE,MAA+B,CAAC;AAE3D,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAmF/D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAmBtD"}
@@ -14,10 +14,22 @@
14
14
  * drift: it means nothing is published, which is the correct reading for a
15
15
  * module that has never shipped.
16
16
  *
17
- * **It records the registry base**, because canonicalizing a relative `imports:`
18
- * source writes the destination into the manifest layer — so the digests below
19
- * are digests *against that base*, and comparing them to digests taken against
20
- * another one would be comparing two different artifacts.
17
+ * **Every entry records its own registry base**, because canonicalizing a
18
+ * relative `imports:` source writes the destination into the manifest layer — so
19
+ * each digest is a digest *against that base*, and comparing it to one taken
20
+ * against another would be comparing two different artifacts.
21
+ *
22
+ * Per entry rather than one top-level base with per-entry deltas: a workspace
23
+ * whose `release.modules` entries each author a destination has no meaningful
24
+ * top-level base, an absent one already means *nothing has been published yet*,
25
+ * and the agreement check returns early on that — so the multi-destination
26
+ * workspace would be the one whose bases are never compared. The file is
27
+ * generated and never hand-maintained, so the redundancy costs nothing and it
28
+ * removes the "differs from what?" question and the absent-versus-unknown
29
+ * conflation together. A **top-level `registry:` is a legacy form the reader
30
+ * accepts and never writes**, applied to every entry: the credential-free PR
31
+ * gate reads whatever ledger is committed on the branch, and that stays in the
32
+ * old shape until a release regenerates it.
21
33
  */
22
34
  import { Document, parseDocument } from "yaml";
23
35
  import { isReleaseVersion } from "./bump-level.js";
@@ -45,13 +57,14 @@ export function parseLedger(text, where) {
45
57
  throw new LedgerError(`${where}: unknown field '${key}'. The ledger carries 'registry:' and 'modules:'.`);
46
58
  }
47
59
  }
48
- const registry = record.registry;
49
- if (registry !== undefined && typeof registry !== "string") {
60
+ // The legacy top-level base: read and applied to every entry, never written.
61
+ const inherited = record.registry;
62
+ if (inherited !== undefined && typeof inherited !== "string") {
50
63
  throw new LedgerError(`${where}: 'registry' must be the publish destination base, as a string.`);
51
64
  }
52
65
  const rawModules = record.modules;
53
66
  if (rawModules === undefined || rawModules === null)
54
- return { ...(registry ? { registry } : {}), modules: new Map() };
67
+ return { modules: new Map() };
55
68
  if (typeof rawModules !== "object" || Array.isArray(rawModules)) {
56
69
  throw new LedgerError(`${where}: 'modules' must be a mapping of module path to entry.`);
57
70
  }
@@ -61,6 +74,16 @@ export function parseLedger(text, where) {
61
74
  throw new LedgerError(`${where}: entry '${key}' must be a mapping.`);
62
75
  }
63
76
  const entry = raw;
77
+ for (const field of Object.keys(entry)) {
78
+ if (field !== "version" && field !== "registry" && field !== "layers") {
79
+ throw new LedgerError(`${where}: entry '${key}' has unknown field '${field}'. An entry carries 'version:', ` +
80
+ `'registry:' and 'layers:'.`);
81
+ }
82
+ }
83
+ if (entry.registry !== undefined && typeof entry.registry !== "string") {
84
+ throw new LedgerError(`${where}: entry '${key}' has a non-string 'registry'. It is the publish destination base ` +
85
+ `the digests beside it were taken against.`);
86
+ }
64
87
  if (!isReleaseVersion(entry.version)) {
65
88
  throw new LedgerError(`${where}: entry '${key}' has no major.minor.patch 'version'. The ledger records the ` +
66
89
  `version each digest was taken at; without it the digest says nothing.`);
@@ -78,9 +101,14 @@ export function parseLedger(text, where) {
78
101
  layers[layer] = digest;
79
102
  }
80
103
  }
81
- modules.set(normalizeModuleKey(key), { version: entry.version, layers });
104
+ const registry = entry.registry ?? inherited;
105
+ modules.set(normalizeModuleKey(key), {
106
+ version: entry.version,
107
+ ...(registry ? { registry } : {}),
108
+ layers,
109
+ });
82
110
  }
83
- return { ...(registry ? { registry } : {}), modules };
111
+ return { modules };
84
112
  }
85
113
  /**
86
114
  * Render the ledger.
@@ -96,12 +124,13 @@ export function serializeLedger(ledger) {
96
124
  const layers = {};
97
125
  for (const layer of Object.keys(entry.layers).sort())
98
126
  layers[layer] = entry.layers[layer];
99
- modules[key] = { version: entry.version, layers };
127
+ modules[key] = {
128
+ version: entry.version,
129
+ ...(entry.registry ? { registry: entry.registry } : {}),
130
+ layers,
131
+ };
100
132
  }
101
- const doc = new Document({
102
- ...(ledger.registry ? { registry: ledger.registry } : {}),
103
- modules,
104
- });
133
+ const doc = new Document({ modules });
105
134
  return ("# Generated by `telo release apply` — what each module looks like as published.\n" +
106
135
  "# A cache of the registry's answer, so a PR gate needs no credentials.\n" +
107
136
  "# `telo release verify` reconciles it against the registry.\n" +