@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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +76 -20
- package/dist/artifact-axes.d.ts +14 -0
- package/dist/artifact-axes.d.ts.map +1 -0
- package/dist/artifact-axes.js +12 -0
- package/dist/artifact-layer-index.d.ts +2 -2
- package/dist/artifact-layer-index.d.ts.map +1 -1
- package/dist/artifact-layer-index.js +9 -4
- package/dist/artifact-selector.d.ts +47 -21
- package/dist/artifact-selector.d.ts.map +1 -1
- package/dist/artifact-selector.js +85 -16
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +144 -113
- package/dist/call-graph.d.ts +12 -1
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +22 -0
- package/dist/catch-scope.js +1 -1
- package/dist/cel-scope-query.d.ts.map +1 -1
- package/dist/cel-scope-query.js +32 -11
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +1 -0
- package/dist/eval-paths.d.ts +36 -5
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +47 -6
- package/dist/extends-resolution.d.ts +12 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +23 -0
- package/dist/forwarded-declaration.d.ts +26 -0
- package/dist/forwarded-declaration.d.ts.map +1 -0
- package/dist/forwarded-declaration.js +31 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +21 -5
- package/dist/module-alias-scope.d.ts +11 -0
- package/dist/module-alias-scope.d.ts.map +1 -1
- package/dist/module-alias-scope.js +10 -0
- package/dist/module-file-claims.d.ts +17 -1
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +38 -3
- package/dist/module-named-files.d.ts +72 -0
- package/dist/module-named-files.d.ts.map +1 -0
- package/dist/module-named-files.js +115 -0
- package/dist/native-entries.d.ts +83 -0
- package/dist/native-entries.d.ts.map +1 -0
- package/dist/native-entries.js +169 -0
- package/dist/nearest-name.d.ts +22 -0
- package/dist/nearest-name.d.ts.map +1 -0
- package/dist/nearest-name.js +43 -0
- package/dist/normalize-inline-resources.d.ts +41 -13
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +236 -54
- package/dist/reference-field-map.d.ts +59 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +200 -34
- package/dist/release/destinations.d.ts +59 -0
- package/dist/release/destinations.d.ts.map +1 -0
- package/dist/release/destinations.js +75 -0
- package/dist/release/index.d.ts +6 -2
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +3 -1
- package/dist/release/ledger.d.ts +20 -11
- package/dist/release/ledger.d.ts.map +1 -1
- package/dist/release/ledger.js +43 -14
- package/dist/release/payload-digest.d.ts +2 -1
- package/dist/release/payload-digest.d.ts.map +1 -1
- package/dist/release/payload-digest.js +2 -1
- package/dist/release/release-plan.d.ts +3 -2
- package/dist/release/release-plan.d.ts.map +1 -1
- package/dist/release/release-plan.js +12 -19
- package/dist/release/workspace-config.d.ts +114 -34
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +322 -38
- package/dist/release/workspace-schema.d.ts +41 -0
- package/dist/release/workspace-schema.d.ts.map +1 -0
- package/dist/release/workspace-schema.js +77 -0
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +8 -5
- package/dist/resolve-throws-union.d.ts +25 -4
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +62 -32
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +2 -1
- package/dist/rewrite-synthetic-origins.d.ts +6 -1
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
- package/dist/rewrite-synthetic-origins.js +107 -13
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +18 -5
- package/dist/schema-error-report.d.ts +7 -0
- package/dist/schema-error-report.d.ts.map +1 -1
- package/dist/schema-error-report.js +9 -4
- package/dist/schema-walk.d.ts +47 -13
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +150 -30
- package/dist/scope-declarations.d.ts +59 -0
- package/dist/scope-declarations.d.ts.map +1 -0
- package/dist/scope-declarations.js +45 -0
- package/dist/source-entries.d.ts +119 -0
- package/dist/source-entries.d.ts.map +1 -0
- package/dist/source-entries.js +349 -0
- package/dist/step-bodies.d.ts +24 -0
- package/dist/step-bodies.d.ts.map +1 -0
- package/dist/step-bodies.js +28 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts +10 -0
- package/dist/template-body.d.ts.map +1 -1
- package/dist/template-body.js +25 -0
- package/dist/validate-cel-context.d.ts +23 -8
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +51 -18
- package/dist/validate-exports.d.ts +28 -0
- package/dist/validate-exports.d.ts.map +1 -0
- package/dist/validate-exports.js +146 -0
- package/dist/validate-extends.d.ts.map +1 -1
- package/dist/validate-extends.js +37 -1
- package/dist/validate-module-artifact.d.ts +4 -2
- package/dist/validate-module-artifact.d.ts.map +1 -1
- package/dist/validate-module-artifact.js +43 -4
- package/dist/validate-native-entries.d.ts +15 -0
- package/dist/validate-native-entries.d.ts.map +1 -0
- package/dist/validate-native-entries.js +103 -0
- package/dist/validate-observed-state.d.ts +7 -1
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +24 -6
- package/dist/validate-provider-coherence.d.ts +4 -8
- package/dist/validate-provider-coherence.d.ts.map +1 -1
- package/dist/validate-provider-coherence.js +12 -123
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +2 -2
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +152 -65
- package/dist/validate-scope-reach.d.ts +31 -0
- package/dist/validate-scope-reach.d.ts.map +1 -0
- package/dist/validate-scope-reach.js +180 -0
- package/dist/validate-source-entries.d.ts +16 -0
- package/dist/validate-source-entries.d.ts.map +1 -0
- package/dist/validate-source-entries.js +90 -0
- package/dist/validate-template-body.d.ts +36 -0
- package/dist/validate-template-body.d.ts.map +1 -0
- package/dist/validate-template-body.js +246 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +21 -39
- package/dist/value-type-keyword.d.ts +22 -5
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +16 -7
- package/package.json +4 -3
- package/src/analyzer.ts +97 -20
- package/src/artifact-axes.ts +24 -0
- package/src/artifact-layer-index.ts +9 -6
- package/src/artifact-selector.ts +110 -35
- package/src/builtins.ts +160 -113
- package/src/call-graph.ts +30 -0
- package/src/catch-scope.ts +1 -1
- package/src/cel-scope-query.ts +28 -9
- package/src/cel-scope.ts +1 -0
- package/src/eval-paths.ts +53 -6
- package/src/extends-resolution.ts +24 -0
- package/src/forwarded-declaration.ts +32 -0
- package/src/index.ts +37 -1
- package/src/kernel-globals.ts +28 -5
- package/src/migrations/CLAUDE.md +35 -0
- package/src/module-alias-scope.ts +17 -0
- package/src/module-file-claims.ts +63 -4
- package/src/module-named-files.ts +162 -0
- package/src/native-entries.ts +227 -0
- package/src/nearest-name.ts +47 -0
- package/src/normalize-inline-resources.ts +326 -70
- package/src/reference-field-map.ts +257 -52
- package/src/release/CLAUDE.md +32 -0
- package/src/release/destinations.ts +105 -0
- package/src/release/index.ts +33 -2
- package/src/release/ledger.ts +51 -21
- package/src/release/payload-digest.ts +2 -1
- package/src/release/release-plan.ts +16 -26
- package/src/release/workspace-config.ts +483 -47
- package/src/release/workspace-schema.ts +99 -0
- package/src/resolve-ref-sentinels.ts +7 -6
- package/src/resolve-throws-union.ts +87 -32
- package/src/resolve-zone-requirements.ts +2 -1
- package/src/rewrite-synthetic-origins.ts +107 -16
- package/src/schema-compat.ts +18 -4
- package/src/schema-error-report.ts +16 -4
- package/src/schema-walk.ts +162 -34
- package/src/scope-declarations.ts +98 -0
- package/src/source-entries.ts +502 -0
- package/src/step-bodies.ts +55 -0
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +25 -0
- package/src/validate-cel-context.ts +60 -18
- package/src/validate-exports.ts +185 -0
- package/src/validate-extends.ts +43 -0
- package/src/validate-module-artifact.ts +46 -3
- package/src/validate-native-entries.ts +147 -0
- package/src/validate-observed-state.ts +36 -7
- package/src/validate-provider-coherence.ts +17 -127
- package/src/validate-reference-forms.ts +2 -3
- package/src/validate-references.ts +151 -66
- package/src/validate-scope-reach.ts +213 -0
- package/src/validate-source-entries.ts +120 -0
- package/src/validate-template-body.ts +274 -0
- package/src/validate-throws-coverage.ts +24 -31
- package/src/value-type-keyword.ts +35 -9
- package/dist/validate-template-dispatch.d.ts +0 -27
- package/dist/validate-template-dispatch.d.ts.map +0 -1
- package/dist/validate-template-dispatch.js +0 -95
- package/src/validate-template-dispatch.ts +0 -99
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { resolveLocalRef } from "./manifest-navigation.js";
|
|
1
2
|
import { type RefSlot, type RefUse, type RefUseCases, readRefSlot } from "./ref-slot.js";
|
|
3
|
+
import { readStepSlot, type StepSlot } from "./step-slot.js";
|
|
2
4
|
|
|
3
5
|
export { readRefSlot, isRefSlot, hasDeclaredUse } from "./ref-slot.js";
|
|
4
6
|
export type { RefSlot, RefUse, RefUseCases } from "./ref-slot.js";
|
|
@@ -38,6 +40,53 @@ export interface RefFieldEntry {
|
|
|
38
40
|
* an unconditional opt-in would rewrite the value into a valid shape before
|
|
39
41
|
* the schema ever saw it. */
|
|
40
42
|
inline?: boolean;
|
|
43
|
+
/** See {@link RefSlot.throwsThrough}. */
|
|
44
|
+
throwsThrough?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The slot an entry records, as `readRefSlot` read it. */
|
|
48
|
+
export function refSlotOfEntry(entry: RefFieldEntry): RefSlot {
|
|
49
|
+
const slot: RefSlot = {
|
|
50
|
+
kinds: entry.refs,
|
|
51
|
+
uses: entry.uses,
|
|
52
|
+
inline: entry.inline === true,
|
|
53
|
+
valueBranches: entry.valueBranches ?? [],
|
|
54
|
+
};
|
|
55
|
+
if (entry.useCases) slot.useCases = entry.useCases;
|
|
56
|
+
if (entry.inputs !== undefined) slot.inputs = entry.inputs;
|
|
57
|
+
if (entry.throwsThrough) slot.throwsThrough = true;
|
|
58
|
+
return slot;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Everything a driven-slot map records at one field path. */
|
|
62
|
+
export interface DrivenPath {
|
|
63
|
+
/** Every reference slot declared here, by any branch. Kept apart rather than
|
|
64
|
+
* merged into one slot and read as a union: a branch that does not apply to a
|
|
65
|
+
* resource can only ADD what counts — more coverage demanded, never less — and
|
|
66
|
+
* one merged slot could not hold two case-map selectors or a branch that
|
|
67
|
+
* declares no use. */
|
|
68
|
+
refs: RefFieldEntry[];
|
|
69
|
+
/** Every step body declared here, by any branch. */
|
|
70
|
+
steps: StepSlot[];
|
|
71
|
+
/** Back-edges of a recursive schema: the schema here is also the one entered
|
|
72
|
+
* at each of these paths, so everything below them applies again below this
|
|
73
|
+
* one, as deep as a resource's data goes. Recorded BESIDE `refs`, never in
|
|
74
|
+
* place of them — a `!ref` here resolves as a reference, an inline object
|
|
75
|
+
* recurses. */
|
|
76
|
+
recurse: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** See {@link buildDrivenSlotMap}. */
|
|
80
|
+
export interface DrivenSlots {
|
|
81
|
+
paths: Map<string, DrivenPath>;
|
|
82
|
+
/** Per `{}` field path, the keys JSON Schema does NOT apply that
|
|
83
|
+
* `additionalProperties` to: those the same schema declares under
|
|
84
|
+
* `properties`, plus the resource envelope at the root. Where several schemas
|
|
85
|
+
* meet at one `{}` path, only keys every one of them declares are skipped. */
|
|
86
|
+
declaredKeys: Map<string, Set<string>>;
|
|
87
|
+
/** True when some path holds a step or reference slot. A map of back-edges
|
|
88
|
+
* alone drives nothing, so a walk over it is skipped. */
|
|
89
|
+
drives: boolean;
|
|
41
90
|
}
|
|
42
91
|
|
|
43
92
|
/** An entry for a field that declares an execution scope (x-telo-scope). */
|
|
@@ -207,9 +256,10 @@ export function resolveFieldValues(obj: unknown, path: string): unknown[] {
|
|
|
207
256
|
*/
|
|
208
257
|
export function buildReferenceFieldMap(schema: Record<string, any>): ReferenceFieldMap {
|
|
209
258
|
const map: ReferenceFieldMap = new Map();
|
|
259
|
+
const sink = injectionSink(map);
|
|
210
260
|
if (schema.properties) {
|
|
211
261
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
212
|
-
traverseNode(propSchema as Record<string, any>, key,
|
|
262
|
+
traverseNode(propSchema as Record<string, any>, key, sink, schema);
|
|
213
263
|
}
|
|
214
264
|
}
|
|
215
265
|
return map;
|
|
@@ -231,40 +281,207 @@ export function buildFieldMapAtPath(
|
|
|
231
281
|
pathPrefix: string,
|
|
232
282
|
): ReferenceFieldMap {
|
|
233
283
|
const map: ReferenceFieldMap = new Map();
|
|
234
|
-
traverseNode(schema, pathPrefix, map, schema);
|
|
284
|
+
traverseNode(schema, pathPrefix, injectionSink(map), schema);
|
|
235
285
|
return map;
|
|
236
286
|
}
|
|
237
287
|
|
|
288
|
+
const drivenSlotMaps = new WeakMap<object, DrivenSlots>();
|
|
289
|
+
|
|
290
|
+
/** The resource envelope: present on every resource document, and never
|
|
291
|
+
* configuration a kind's root `additionalProperties` describes. */
|
|
292
|
+
const RESOURCE_ENVELOPE_KEYS = ["kind", "metadata"];
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Every slot through which a resource of this schema DRIVES another — the
|
|
296
|
+
* reference field map's own traversal, plus what a dispatch analysis needs and
|
|
297
|
+
* the injection surface must not have:
|
|
298
|
+
*
|
|
299
|
+
* - local `$ref` is followed, resolved against the root. The injection map
|
|
300
|
+
* stops there so a step's `invoke` never becomes an injection site; this map
|
|
301
|
+
* stops AT a step body instead, which removes that reason. A reference back
|
|
302
|
+
* to a node already on the descent is recorded as a back-edge rather than
|
|
303
|
+
* unrolled, so a recursive shape stays finite here and is followed as deep as
|
|
304
|
+
* the data goes by `forEachDrivenSlot`;
|
|
305
|
+
* - a step body is recorded and not descended — the step traversal owns
|
|
306
|
+
* everything below it;
|
|
307
|
+
* - the root's own variant branches and `additionalProperties` are walked, not
|
|
308
|
+
* only its `properties`;
|
|
309
|
+
* - a path keeps EVERY slot any branch declares there, where the injection map
|
|
310
|
+
* keeps the last one written (see {@link DrivenPath.refs});
|
|
311
|
+
* - each `{}` path records the keys its `additionalProperties` does not cover.
|
|
312
|
+
*
|
|
313
|
+
* Shared by every throws question (a kind's `inherit` union, a scope list's
|
|
314
|
+
* denominator, catch-scope enclosure, and whether `inherit` is legal at all),
|
|
315
|
+
* so none can reach a slot another cannot. Memoized per schema object.
|
|
316
|
+
*/
|
|
317
|
+
export function buildDrivenSlotMap(schema: Record<string, any>): DrivenSlots {
|
|
318
|
+
const cached = drivenSlotMaps.get(schema);
|
|
319
|
+
if (cached) return cached;
|
|
320
|
+
const slots: DrivenSlots = { paths: new Map(), declaredKeys: new Map(), drives: false };
|
|
321
|
+
traverseNode(schema, "", drivenSink(slots), schema);
|
|
322
|
+
drivenSlotMaps.set(schema, slots);
|
|
323
|
+
return slots;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const joinPath = (path: string, key: string): string => (path ? `${path}.${key}` : key);
|
|
327
|
+
|
|
328
|
+
/** Where a traversal records what it finds. */
|
|
329
|
+
interface FieldMapSink {
|
|
330
|
+
readonly driven: boolean;
|
|
331
|
+
ref(path: string, entry: RefFieldEntry, node: Record<string, any>): void;
|
|
332
|
+
stop(path: string, entry: ScopeFieldEntry | SchemaFromFieldEntry): void;
|
|
333
|
+
step(path: string, step: StepSlot, node: Record<string, any>): void;
|
|
334
|
+
recurse(path: string, to: string): void;
|
|
335
|
+
mapValue(mapPath: string, declared: string[]): void;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** The injection map: one entry per path, the last one written. Step bodies,
|
|
339
|
+
* back-edges and map-value keys are the driven map's alone. */
|
|
340
|
+
function injectionSink(map: ReferenceFieldMap): FieldMapSink {
|
|
341
|
+
return {
|
|
342
|
+
driven: false,
|
|
343
|
+
ref: (path, entry) => {
|
|
344
|
+
map.set(path, entry);
|
|
345
|
+
},
|
|
346
|
+
stop: (path, entry) => {
|
|
347
|
+
map.set(path, entry);
|
|
348
|
+
},
|
|
349
|
+
step: () => {},
|
|
350
|
+
recurse: () => {},
|
|
351
|
+
mapValue: () => {},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function drivenSink(slots: DrivenSlots): FieldMapSink {
|
|
356
|
+
const recorded = new Map<string, Set<object>>();
|
|
357
|
+
const at = (path: string): DrivenPath => {
|
|
358
|
+
let entry = slots.paths.get(path);
|
|
359
|
+
if (!entry) {
|
|
360
|
+
entry = { refs: [], steps: [], recurse: [] };
|
|
361
|
+
slots.paths.set(path, entry);
|
|
362
|
+
}
|
|
363
|
+
return entry;
|
|
364
|
+
};
|
|
365
|
+
// One schema node reached twice at one path (two branches `$ref`-ing one
|
|
366
|
+
// definition) is one slot.
|
|
367
|
+
const firstTime = (path: string, node: object): boolean => {
|
|
368
|
+
let nodes = recorded.get(path);
|
|
369
|
+
if (!nodes) recorded.set(path, (nodes = new Set()));
|
|
370
|
+
if (nodes.has(node)) return false;
|
|
371
|
+
nodes.add(node);
|
|
372
|
+
return true;
|
|
373
|
+
};
|
|
374
|
+
return {
|
|
375
|
+
driven: true,
|
|
376
|
+
ref: (path, entry, node) => {
|
|
377
|
+
if (!firstTime(path, node)) return;
|
|
378
|
+
at(path).refs.push(entry);
|
|
379
|
+
slots.drives = true;
|
|
380
|
+
},
|
|
381
|
+
stop: () => {},
|
|
382
|
+
step: (path, step, node) => {
|
|
383
|
+
if (!firstTime(path, node)) return;
|
|
384
|
+
at(path).steps.push(step);
|
|
385
|
+
slots.drives = true;
|
|
386
|
+
},
|
|
387
|
+
recurse: (path, to) => {
|
|
388
|
+
const entry = at(path);
|
|
389
|
+
if (!entry.recurse.includes(to)) entry.recurse.push(to);
|
|
390
|
+
},
|
|
391
|
+
mapValue: (mapPath, declared) => {
|
|
392
|
+
const previous = slots.declaredKeys.get(mapPath);
|
|
393
|
+
slots.declaredKeys.set(
|
|
394
|
+
mapPath,
|
|
395
|
+
new Set(previous ? declared.filter((key) => previous.has(key)) : declared),
|
|
396
|
+
);
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Follow a local `$ref` in driven mode. `onStack` maps each node on the
|
|
402
|
+
* current descent to the path it was entered at: re-entering one records a
|
|
403
|
+
* back-edge to that path, and a cycle that made no structural progress (same
|
|
404
|
+
* path) records nothing, since its body is already recorded there. */
|
|
405
|
+
function followLocalRef(
|
|
406
|
+
node: Record<string, any>,
|
|
407
|
+
path: string,
|
|
408
|
+
sink: FieldMapSink,
|
|
409
|
+
root: Record<string, any> | undefined,
|
|
410
|
+
onStack: Map<Record<string, any>, string>,
|
|
411
|
+
): void {
|
|
412
|
+
if (!root) return;
|
|
413
|
+
const target = resolveLocalRef(node, root);
|
|
414
|
+
if (!target || target === node) return;
|
|
415
|
+
const entered = onStack.get(target);
|
|
416
|
+
if (entered !== undefined) {
|
|
417
|
+
if (entered !== path) sink.recurse(path, entered);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
onStack.set(target, path);
|
|
421
|
+
try {
|
|
422
|
+
traverseNode(target, path, sink, root, onStack);
|
|
423
|
+
} finally {
|
|
424
|
+
onStack.delete(target);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** `owner.additionalProperties`, the schema of every value in an open-keyed
|
|
429
|
+
* object. JSON Schema applies it only to keys `owner` does not declare, which
|
|
430
|
+
* the driven map records so a walk skips them. */
|
|
431
|
+
function traverseMapValue(
|
|
432
|
+
owner: Record<string, any>,
|
|
433
|
+
path: string,
|
|
434
|
+
sink: FieldMapSink,
|
|
435
|
+
root: Record<string, any> | undefined,
|
|
436
|
+
onStack: Map<Record<string, any>, string>,
|
|
437
|
+
): void {
|
|
438
|
+
const valueSchema = owner.additionalProperties;
|
|
439
|
+
if (!valueSchema || typeof valueSchema !== "object" || Array.isArray(valueSchema)) return;
|
|
440
|
+
const mapPath = joinPath(path, "{}");
|
|
441
|
+
const declared =
|
|
442
|
+
owner.properties && typeof owner.properties === "object" ? Object.keys(owner.properties) : [];
|
|
443
|
+
sink.mapValue(mapPath, path === "" ? [...declared, ...RESOURCE_ENVELOPE_KEYS] : declared);
|
|
444
|
+
traverseNode(valueSchema as Record<string, any>, mapPath, sink, root, onStack);
|
|
445
|
+
}
|
|
446
|
+
|
|
238
447
|
function traverseNode(
|
|
239
448
|
node: Record<string, any>,
|
|
240
449
|
path: string,
|
|
241
|
-
|
|
450
|
+
sink: FieldMapSink,
|
|
242
451
|
root?: Record<string, any>,
|
|
243
|
-
|
|
452
|
+
onStack: Map<Record<string, any>, string> = new Map(),
|
|
244
453
|
): void {
|
|
245
|
-
|
|
454
|
+
const driven = sink.driven;
|
|
455
|
+
// Local `$ref` is intentionally NOT followed here. This map is the kernel's
|
|
246
456
|
// Phase-5 injection surface: descending into shared `$defs` (notably
|
|
247
457
|
// `Run.Sequence`'s `step` definition) would make every step's `invoke` an
|
|
248
458
|
// injection site, and step slots resolve at dispatch — injecting there is
|
|
249
459
|
// unwanted regardless of tracing (the original dispatcher-bypass blocker
|
|
250
460
|
// has since shipped via the `REF_IDENTITY` stamp). Static analysis is NOT
|
|
251
461
|
// limited by this stop: the call graph (`call-graph.ts`) reads step slots
|
|
252
|
-
// from the item schema itself and scans the value tree,
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
if (typeof node?.$ref === "string") return;
|
|
462
|
+
// from the item schema itself and scans the value tree, and the driven-slot
|
|
463
|
+
// mode (`buildDrivenSlotMap`) follows the reference.
|
|
464
|
+
if (typeof node?.$ref === "string" && !driven) return;
|
|
256
465
|
// Scope slot — record and stop; do not recurse into scope contents
|
|
257
466
|
if ("x-telo-scope" in node) {
|
|
258
|
-
|
|
467
|
+
sink.stop(path, { scope: node["x-telo-scope"] });
|
|
259
468
|
return;
|
|
260
469
|
}
|
|
261
470
|
|
|
262
471
|
// Schema-from slot — record and stop; no further traversal needed
|
|
263
472
|
if ("x-telo-schema-from" in node) {
|
|
264
|
-
|
|
473
|
+
sink.stop(path, { schemaFrom: node["x-telo-schema-from"] });
|
|
265
474
|
return;
|
|
266
475
|
}
|
|
267
476
|
|
|
477
|
+
if (driven) {
|
|
478
|
+
const step = readStepSlot(node);
|
|
479
|
+
if (step) {
|
|
480
|
+
sink.step(path, step, node);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
268
485
|
// Reference slot (direct, via a `kind:` list, or via anyOf)
|
|
269
486
|
const slot = readRefSlot(node);
|
|
270
487
|
if (slot && slot.kinds.length > 0) {
|
|
@@ -278,7 +495,8 @@ function traverseNode(
|
|
|
278
495
|
if (slot.valueBranches.length > 0) entry.valueBranches = slot.valueBranches;
|
|
279
496
|
if (node["x-telo-context"]) entry.context = node["x-telo-context"] as Record<string, any>;
|
|
280
497
|
if (slot.inline) entry.inline = true;
|
|
281
|
-
|
|
498
|
+
if (slot.throwsThrough) entry.throwsThrough = true;
|
|
499
|
+
sink.ref(path, entry, node);
|
|
282
500
|
// A node can mix item-level ref branches (a bare string / `{kind, name}`)
|
|
283
501
|
// with object branches that carry their OWN nested refs — e.g. Application
|
|
284
502
|
// `targets`: a bare ref vs inline `{ invoke }` vs gated `{ ref }`. Descend
|
|
@@ -290,22 +508,26 @@ function traverseNode(
|
|
|
290
508
|
if (!Array.isArray(variants)) continue;
|
|
291
509
|
for (const variant of variants) {
|
|
292
510
|
if (!variant || typeof variant !== "object") continue;
|
|
293
|
-
traverseVariant(variant as Record<string, any>, path,
|
|
511
|
+
traverseVariant(variant as Record<string, any>, path, sink, root, onStack);
|
|
294
512
|
}
|
|
295
513
|
}
|
|
296
514
|
return;
|
|
297
515
|
}
|
|
298
|
-
|
|
516
|
+
// Reached only in driven mode — the injection map stopped at the top.
|
|
517
|
+
if (typeof node?.$ref === "string") {
|
|
518
|
+
followLocalRef(node, path, sink, root, onStack);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
299
521
|
|
|
300
522
|
// Array — recurse into items
|
|
301
523
|
if (node.type === "array" && node.items) {
|
|
302
|
-
traverseNode(node.items as Record<string, any>, path + "[]",
|
|
524
|
+
traverseNode(node.items as Record<string, any>, path + "[]", sink, root, onStack);
|
|
303
525
|
}
|
|
304
526
|
|
|
305
527
|
// Object — recurse into properties
|
|
306
528
|
if (node.properties) {
|
|
307
529
|
for (const [key, propSchema] of Object.entries(node.properties)) {
|
|
308
|
-
traverseNode(propSchema as Record<string, any>,
|
|
530
|
+
traverseNode(propSchema as Record<string, any>, joinPath(path, key), sink, root, onStack);
|
|
309
531
|
}
|
|
310
532
|
}
|
|
311
533
|
|
|
@@ -315,67 +537,50 @@ function traverseNode(
|
|
|
315
537
|
// slots inside the branch. Walking each branch surfaces those slots into
|
|
316
538
|
// the field map so downstream passes (ref validation, sentinel
|
|
317
539
|
// resolution, dependency graph) cover them without a runtime fallback.
|
|
318
|
-
// The same field path may be added by multiple branches
|
|
319
|
-
// assignment
|
|
320
|
-
// share the same ref/context configuration
|
|
321
|
-
//
|
|
540
|
+
// The same field path may be added by multiple branches. The injection map
|
|
541
|
+
// keeps the later assignment, which is fine for injection — branches with
|
|
542
|
+
// the same field path share the same ref/context configuration. The driven
|
|
543
|
+
// map keeps every one, because which branch applies decides what a
|
|
544
|
+
// dispatch can throw.
|
|
322
545
|
for (const variantKey of ["oneOf", "anyOf", "allOf"] as const) {
|
|
323
546
|
const variants = node[variantKey];
|
|
324
547
|
if (!Array.isArray(variants)) continue;
|
|
325
548
|
for (const variant of variants) {
|
|
326
549
|
if (!variant || typeof variant !== "object") continue;
|
|
327
|
-
traverseVariant(variant as Record<string, any>, path,
|
|
550
|
+
traverseVariant(variant as Record<string, any>, path, sink, root, onStack);
|
|
328
551
|
}
|
|
329
552
|
}
|
|
330
553
|
|
|
331
554
|
// Map — `additionalProperties: { ... }` describes every value in an
|
|
332
555
|
// open-keyed object. Encoder refs nested inside `content[mime]` map
|
|
333
556
|
// entries reach Phase 5 through this branch.
|
|
334
|
-
|
|
335
|
-
node.additionalProperties &&
|
|
336
|
-
typeof node.additionalProperties === "object" &&
|
|
337
|
-
!Array.isArray(node.additionalProperties)
|
|
338
|
-
) {
|
|
339
|
-
traverseNode(
|
|
340
|
-
node.additionalProperties as Record<string, any>,
|
|
341
|
-
`${path}.{}`,
|
|
342
|
-
map,
|
|
343
|
-
root,
|
|
344
|
-
visitedRefs,
|
|
345
|
-
);
|
|
346
|
-
}
|
|
557
|
+
traverseMapValue(node, path, sink, root, onStack);
|
|
347
558
|
}
|
|
348
559
|
|
|
349
560
|
/** Walk a single variant of a `oneOf` / `anyOf` / `allOf` branch. Only
|
|
350
561
|
* the properties / items / map slots are followed — collectRefs at the
|
|
351
562
|
* variant root is handled by the parent's `collectRefs(node)` already
|
|
352
|
-
* (anyOf of x-telo-ref branches is the canonical multi-ref shape).
|
|
563
|
+
* (anyOf of x-telo-ref branches is the canonical multi-ref shape). In
|
|
564
|
+
* driven mode a `$ref` branch is followed as a node in its own right, since
|
|
565
|
+
* `readRefSlot` does not look through one. */
|
|
353
566
|
function traverseVariant(
|
|
354
567
|
variant: Record<string, any>,
|
|
355
568
|
path: string,
|
|
356
|
-
|
|
569
|
+
sink: FieldMapSink,
|
|
357
570
|
root?: Record<string, any>,
|
|
358
|
-
|
|
571
|
+
onStack: Map<Record<string, any>, string> = new Map(),
|
|
359
572
|
): void {
|
|
573
|
+
if (sink.driven && typeof variant.$ref === "string") {
|
|
574
|
+
followLocalRef(variant, path, sink, root, onStack);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
360
577
|
if (variant.properties) {
|
|
361
578
|
for (const [key, propSchema] of Object.entries(variant.properties)) {
|
|
362
|
-
traverseNode(propSchema as Record<string, any>,
|
|
579
|
+
traverseNode(propSchema as Record<string, any>, joinPath(path, key), sink, root, onStack);
|
|
363
580
|
}
|
|
364
581
|
}
|
|
365
582
|
if (variant.type === "array" && variant.items) {
|
|
366
|
-
traverseNode(variant.items as Record<string, any>, path + "[]",
|
|
367
|
-
}
|
|
368
|
-
if (
|
|
369
|
-
variant.additionalProperties &&
|
|
370
|
-
typeof variant.additionalProperties === "object" &&
|
|
371
|
-
!Array.isArray(variant.additionalProperties)
|
|
372
|
-
) {
|
|
373
|
-
traverseNode(
|
|
374
|
-
variant.additionalProperties as Record<string, any>,
|
|
375
|
-
`${path}.{}`,
|
|
376
|
-
map,
|
|
377
|
-
root,
|
|
378
|
-
visitedRefs,
|
|
379
|
-
);
|
|
583
|
+
traverseNode(variant.items as Record<string, any>, path + "[]", sink, root, onStack);
|
|
380
584
|
}
|
|
585
|
+
traverseMapValue(variant, path, sink, root, onStack);
|
|
381
586
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Release model — guide
|
|
2
|
+
|
|
3
|
+
Loaded when working under `analyzer/nodejs/src/release/`. The release RULES every change follows (changesets for published packages, `telo release add` fragments for modules, pre-1.0 minors) are in the root `CLAUDE.md`; publishing (payload builder, pins, the publication split) is in `cli/nodejs/CLAUDE.md`. Guide: `docs/extend/releasing-modules.md`.
|
|
4
|
+
|
|
5
|
+
**The release system exists because a module's artifact EMBEDS its dependencies** — esbuild inlines a sibling library's source into the controller bundle, and publish pins each relative import to a hash of the sibling's manifest — so bumping the dependents is a correctness requirement, not a courtesy. No general-purpose changelog tool can see that — a per-project ledger has no dependency graph at all, and changesets' stops at the npm boundary, where a module's inlined sibling is invisible.
|
|
6
|
+
|
|
7
|
+
**Three questions, three mechanisms.** The **payload digest** (exact, from the bytes; `computeFilesIntegrity` over the layers publish builds, or over an image module's `.dockerignore`-defined file set) decides *whether* a module bumps, so it fires for an inlined sibling, a shared-library fix and a lockfile-only transitive bump alike. The **edge graph** decides *at what level*: edges come from the controller build's own **metafile** (a declared-deps graph cannot see `--external`, so `@telorun/sdk` — declared by 54 modules, inlined by none — would bump the whole stdlib on every SDK change) plus in-repo relative `imports:`; a dependent mirrors its dependency's level, joined as the maximum over paths. An **import edge bumps unconditionally** (publishing rewrites the sibling's version into the manifest, so the layer provably changes) while an **inline edge only explains drift that already showed up**. A digest that moved with nothing to attribute it to takes a **patch** and is reported as *unattributed*, never silently. The path-scoped **changed-files rule** survives but is demoted to deciding only *whether a changelog line is requested* — it used to decide the version, which is why its guesswork (`docs/` vs `tests/` vs `nodejs/`, `bundlesAController`) had to be sound and was not; now a false positive costs one sentence.
|
|
8
|
+
|
|
9
|
+
**The ledger is a cache; the registry is the authority.** `.changes/ledger.yaml` records per module its version, its per-layer integrity **as published**, and the **registry base** those digests were taken against — per ENTRY, since a workspace may publish its subtrees to different bases (canonicalization writes the destination into every relative import, so digests against another base are digests of different bytes). A top-level `registry:` is a legacy form the reader accepts and never writes, applied to every entry: the credential-free PR gate reads whatever ledger is committed on the branch. That is the load-bearing property: **the PR gate and the publish gate compute the same number**, and the PR gate needs no merge base and no credentials, so a fork runs the identical computation. A *missing* entry is not drift — nothing is published, the right reading for a new module. `telo publish` still reads the registry, and `telo release verify [--write]` reconciles.
|
|
10
|
+
|
|
11
|
+
**`telo release check` does NOT fail because a payload moved.** Under a toolchain bump every digest moves; demanding sixty fragments for that is a tax on nobody's behalf, and those ship as unattributed patches. It fails when no consistent plan can be formed: a fragment naming an unknown module, a major-inducing kind (`Changed`/`Removed`), a manifest version disagreeing with its ledger entry, or a registry-base mismatch. It *warns* (`CHANGELOG_ENTRY_REQUESTED`) when a module's own files changed with no fragment naming it.
|
|
12
|
+
|
|
13
|
+
## The workspace marker
|
|
14
|
+
|
|
15
|
+
**The workspace anchor.** `telo-workspace.yaml` at the repo root. **Its location is the anchor** for every module key, ledger entry, fragment path and the `.telo` cache; **every FIELD lives in a block scoped to what it governs**, which is the file's one rule. `release:` carries `registry:` (the publish base), `ignore:` (module-relative gitignore-style paths whose changes ask for no changelog fragment) and `modules:` (the subtrees that may hold modules — not derivable, since hundreds of manifests elsewhere carry a `metadata.version`). `env:` carries `roots:` (how far up `telo run` walks collecting env files) and `files:` (which filenames, later winning within one directory). Reader + shape as data: `analyzer/nodejs/src/release/workspace-{config,schema}.ts`; guide `docs/guides/workspaces.md`.
|
|
16
|
+
|
|
17
|
+
**`modules:` moved under `release:` and no second spelling is kept** — top-level is a recognized-and-moved key naming the move. It is a release INVENTORY, not an inventory of manifests, and leaving it at the top level made it read as a fact about the tree: the runner seeded `modules: ["*"]` into every session workspace purely because an empty list was a parse error, shipping release scope a session never reads. **The file and every block in it are optional**, so a marker whose whole content is comments is valid — which is what the runner seeds now.
|
|
18
|
+
|
|
19
|
+
**The reader is lenient and the strict half is diagnostics; which are FATAL is the caller's policy.** `readWorkspaceConfig` returns config + diagnostics anchored at key paths (`WORKSPACE_MODULES_MOVED`, `WORKSPACE_UNKNOWN_KEY`, `WORKSPACE_INVALID_VALUE`, plus the repo-shaped `WORKSPACE_ENTRY_MATCHES_NOTHING` / `_SHADOWED` / `WORKSPACE_MARKER_SHADOWED` the editor adds). `telo release` refuses on any error; **`telo run` consumes `env:` alone** — a typo under `release.modules` is printed and the run proceeds, while one INSIDE `env:` fails the run, because degrading to the marker-wide bound would WIDEN the walk and a block that opted into a boundary and got nothing is a defect, not a default. An unknown top-level BLOCK is a warning (so the next block ships without breaking today's runs); a near-miss of a known one is an error. Gitignore matching is **injected** (`PatternMatch`), because the grammar is `@telorun/glob`'s and the analyzer is browser-safe; `lastMatchIndex` is what makes a decision attributable to the entry that made it.
|
|
20
|
+
|
|
21
|
+
**Settings cascade key-wise and each value REPLACES whole**: built-in → `release:`'s own key → the matching entry's. A `modules:` entry is a bare pattern string or `{path, registry?, ignore?}`, evaluated **last-match-wins** — the rule the list already had — so a later, more specific entry overrides one module and a `!` entry is an exclusion that supplies nothing. A list is never unioned across levels, so the set in force is always exactly one authored list.
|
|
22
|
+
|
|
23
|
+
**A destination is the resolved base plus the module's own directory name, and both ways that can be inconsistent are checked at plan time, before any payload is built**: `DESTINATION_COLLISION` (two modules on one ref — the payload builder is keyed by manifest, so it catches one module claimed twice and never the reverse, while the ledger keys by module and both entries would record digests for one artifact) and `IMPORT_DESTINATION_CONFLICT` (a relative import whose derived ref is not where the target publishes; equal registries are necessary and not sufficient, since two modules under one base at different depths derive differently). **The registry cascade is entry → block → `--registry` → `TELO_OCI_REGISTRY` → the base that module's own ledger entry recorded** — the ledger is LAST, a change: a workspace that authors its destination has said where it publishes, and a cache of a past answer must not outrank it. A disagreement is a per-module `LEDGER_REGISTRY_MISMATCH` in a plan that is still produced, rather than an abort. **`telo release order` emits `{key, destination}`** and the module publisher reads the destination from it: deriving `$TELO_OCI_REGISTRY/<directory name>` for itself was the last fact that script still answered, and a multi-destination workspace would have planned several bases and pushed them all to one.
|
|
24
|
+
|
|
25
|
+
Modules are **discovered, not registered**: a module is a directory under a named subtree holding a `telo.yaml` whose **module doc** carries a `metadata.version`, and its key is its workspace-relative path (never a bare name — two subtrees can share one). Selection and attribution are ONE decision, since the entry that decides whether a directory is a module is the entry whose settings it resolves. Changed-file attribution is to the **nearest** enclosing module, matching how build inputs are attributed. A `Dockerfile` beside the manifest makes it an **image module**; that decides only which file set is digested. An image module's base image is deliberately outside its digest — the every-push `:latest` + `:sha-<short>` policy already carries a kernel change to the deployed app.
|
|
26
|
+
|
|
27
|
+
The model splits on the browser-safety line: `analyzer/nodejs/src/release/` holds module identity, fragment parsing, the ledger, the edge graph, propagation and version planning (pure data in, plan out, so the editor can answer "what does changing this library bump?"); `cli/nodejs/src/release/` holds evidence collection (driving the kernel's builder, workspace and git reads, file writing) and the commands. Version rewriting reuses `yaml-source-edit.ts`, so a bump is a one-line diff rather than a re-serialized file.
|
|
28
|
+
|
|
29
|
+
## Where to look
|
|
30
|
+
|
|
31
|
+
- "will this change bump anything, and why?" → `analyzer/nodejs/src/release/` (`release-plan.ts` the planner, `payload-digest.ts` the digest keys, `ledger.ts`, `fragment.ts`, `version-stamp.ts`), `cli/nodejs/src/release/` (`workspace.ts` discovery, `evidence.ts` collection, `targets.ts` the registry cascade + the import graph), `cli/nodejs/src/bundle/module-payload.ts` (the one payload builder publish and release share), `docs/extend/releasing-modules.md`
|
|
32
|
+
- "what may the workspace marker say, and who reads which half?" → `analyzer/nodejs/src/release/workspace-schema.ts` (the shape, as data), `workspace-config.ts` (the lenient reader + the cascade), `destinations.ts` (the two plan-time checks), `cli/nodejs/src/env-files.ts` (the `env:` half and the run path's policy), `packages/ide-support/src/workspace/` (diagnostics + completion), `docs/guides/workspaces.md`
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
import type { ModuleKey } from "./fragment.js";
|
|
18
|
+
import type { ReleaseDiagnostic } from "./release-plan.js";
|
|
19
|
+
|
|
20
|
+
export interface ModuleDestination {
|
|
21
|
+
readonly key: ModuleKey;
|
|
22
|
+
/** `<registry>/<the module's own directory name>`. */
|
|
23
|
+
readonly destination: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A relative import between two workspace modules, with both answers about
|
|
28
|
+
* where the target publishes.
|
|
29
|
+
*
|
|
30
|
+
* `derived` is what the importer's own destination yields when the import path
|
|
31
|
+
* is applied to it — the transport's rule, and what ref canonicalization writes
|
|
32
|
+
* into the published manifest. `assigned` is what discovery independently gives
|
|
33
|
+
* that module.
|
|
34
|
+
*/
|
|
35
|
+
export interface ImportDestination {
|
|
36
|
+
readonly from: ModuleKey;
|
|
37
|
+
readonly to: ModuleKey;
|
|
38
|
+
readonly derived: string;
|
|
39
|
+
readonly assigned: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Two modules must not resolve to one ref.
|
|
44
|
+
*
|
|
45
|
+
* Nothing else catches it: the payload builder is keyed by manifest, so it sees
|
|
46
|
+
* one module claimed by two destinations and never two modules claiming one,
|
|
47
|
+
* while the ledger keys by module — both entries would record digests for a
|
|
48
|
+
* single published artifact and reconciliation could never settle.
|
|
49
|
+
*/
|
|
50
|
+
export function checkDestinationCollisions(
|
|
51
|
+
destinations: readonly ModuleDestination[],
|
|
52
|
+
): ReleaseDiagnostic[] {
|
|
53
|
+
const byDestination = new Map<string, ModuleKey[]>();
|
|
54
|
+
for (const { key, destination } of destinations) {
|
|
55
|
+
const sharing = byDestination.get(destination);
|
|
56
|
+
if (sharing) sharing.push(key);
|
|
57
|
+
else byDestination.set(destination, [key]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const diagnostics: ReleaseDiagnostic[] = [];
|
|
61
|
+
for (const [destination, keys] of byDestination) {
|
|
62
|
+
if (keys.length < 2) continue;
|
|
63
|
+
diagnostics.push({
|
|
64
|
+
severity: "error",
|
|
65
|
+
code: "DESTINATION_COLLISION",
|
|
66
|
+
message:
|
|
67
|
+
`${keys.sort().join(" and ")} both publish to '${destination}'. A module's ref is its ` +
|
|
68
|
+
`registry base plus its own directory name, so they would overwrite one artifact and ` +
|
|
69
|
+
`their ledger entries could never reconcile. Rename one directory, or give one of the ` +
|
|
70
|
+
`subtrees its own 'registry:' in telo-workspace.yaml.`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return diagnostics;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A relative import is valid only where the importer's derived ref for its
|
|
78
|
+
* target equals the destination that module is independently assigned.
|
|
79
|
+
*
|
|
80
|
+
* Equal registries are necessary and not sufficient — two modules under one base
|
|
81
|
+
* at different directory depths derive differently too. Making the builder take
|
|
82
|
+
* the assigned destination instead is not the fix: it would put a ref in the
|
|
83
|
+
* artifact that the transport's resolution rule does not produce, so the
|
|
84
|
+
* manifest would say one thing and every consumer resolving relatively would
|
|
85
|
+
* compute another.
|
|
86
|
+
*/
|
|
87
|
+
export function checkImportDestinations(
|
|
88
|
+
edges: readonly ImportDestination[],
|
|
89
|
+
): ReleaseDiagnostic[] {
|
|
90
|
+
const diagnostics: ReleaseDiagnostic[] = [];
|
|
91
|
+
for (const edge of edges) {
|
|
92
|
+
if (edge.derived === edge.assigned) continue;
|
|
93
|
+
diagnostics.push({
|
|
94
|
+
severity: "error",
|
|
95
|
+
code: "IMPORT_DESTINATION_CONFLICT",
|
|
96
|
+
message:
|
|
97
|
+
`${edge.from} imports ${edge.to} by relative path, which canonicalizes to ` +
|
|
98
|
+
`'${edge.derived}' — but ${edge.to} publishes to '${edge.assigned}'. Publishing rewrites ` +
|
|
99
|
+
`the import to the ref its own path yields, so the artifact would name a module nobody ` +
|
|
100
|
+
`pushes. Give the two the same registry base and the same directory depth, or make it a ` +
|
|
101
|
+
`pinned remote import, which is what a dependency across a publish boundary is.`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return diagnostics;
|
|
105
|
+
}
|
package/src/release/index.ts
CHANGED
|
@@ -63,5 +63,36 @@ export {
|
|
|
63
63
|
stampPackageVersion,
|
|
64
64
|
} from "./version-stamp.js";
|
|
65
65
|
|
|
66
|
-
export {
|
|
67
|
-
export type {
|
|
66
|
+
export { checkDestinationCollisions, checkImportDestinations } from "./destinations.js";
|
|
67
|
+
export type { ImportDestination, ModuleDestination } from "./destinations.js";
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
DEFAULT_ENV_FILES,
|
|
71
|
+
DEFAULT_RELEASE_IGNORE,
|
|
72
|
+
WORKSPACE_FILENAME,
|
|
73
|
+
WorkspaceConfigError,
|
|
74
|
+
diagnosticsFor,
|
|
75
|
+
hasError,
|
|
76
|
+
matchesPatterns,
|
|
77
|
+
readWorkspaceConfig,
|
|
78
|
+
requireReleaseSettings,
|
|
79
|
+
settingsForModule,
|
|
80
|
+
} from "./workspace-config.js";
|
|
81
|
+
export type {
|
|
82
|
+
EnvSettings,
|
|
83
|
+
ModuleEntry,
|
|
84
|
+
ModuleSettings,
|
|
85
|
+
PatternMatch,
|
|
86
|
+
ReleaseSettings,
|
|
87
|
+
WorkspaceConfig,
|
|
88
|
+
WorkspaceDiagnostic,
|
|
89
|
+
WorkspaceDiagnosticCode,
|
|
90
|
+
WorkspaceRead,
|
|
91
|
+
} from "./workspace-config.js";
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
MODULE_ENTRY_KEYS,
|
|
95
|
+
WORKSPACE_BLOCKS,
|
|
96
|
+
WORKSPACE_SCHEMA,
|
|
97
|
+
} from "./workspace-schema.js";
|
|
98
|
+
export type { WorkspaceBlockSchema, WorkspaceKeySchema } from "./workspace-schema.js";
|