@telorun/analyzer 0.59.0 → 0.61.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 (95) hide show
  1. package/dist/analyzer.d.ts +9 -2
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +38 -10
  4. package/dist/artifact-layer-index.d.ts +13 -5
  5. package/dist/artifact-layer-index.d.ts.map +1 -1
  6. package/dist/artifact-layer-index.js +37 -13
  7. package/dist/artifact-selector.d.ts +10 -3
  8. package/dist/artifact-selector.d.ts.map +1 -1
  9. package/dist/artifact-selector.js +10 -1
  10. package/dist/builtins.d.ts.map +1 -1
  11. package/dist/builtins.js +44 -43
  12. package/dist/cel-environment.d.ts.map +1 -1
  13. package/dist/cel-environment.js +35 -0
  14. package/dist/flatten-for-analyzer.d.ts +4 -0
  15. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  16. package/dist/flatten-for-analyzer.js +8 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +9 -2
  20. package/dist/kernel-globals.d.ts +1 -1
  21. package/dist/kernel-globals.d.ts.map +1 -1
  22. package/dist/kernel-globals.js +15 -1
  23. package/dist/manifest-schemas.d.ts +407 -0
  24. package/dist/manifest-schemas.d.ts.map +1 -0
  25. package/dist/manifest-schemas.js +396 -0
  26. package/dist/module-file-claims.d.ts +13 -0
  27. package/dist/module-file-claims.d.ts.map +1 -1
  28. package/dist/module-file-claims.js +26 -2
  29. package/dist/module-library.d.ts +97 -0
  30. package/dist/module-library.d.ts.map +1 -0
  31. package/dist/module-library.js +162 -0
  32. package/dist/module-metadata-scope.d.ts +44 -0
  33. package/dist/module-metadata-scope.d.ts.map +1 -0
  34. package/dist/module-metadata-scope.js +82 -0
  35. package/dist/parse-loaded-file.d.ts.map +1 -1
  36. package/dist/parse-loaded-file.js +16 -0
  37. package/dist/release/bump-level.d.ts +46 -0
  38. package/dist/release/bump-level.d.ts.map +1 -0
  39. package/dist/release/bump-level.js +80 -0
  40. package/dist/release/changelog.d.ts +36 -0
  41. package/dist/release/changelog.d.ts.map +1 -0
  42. package/dist/release/changelog.js +53 -0
  43. package/dist/release/fragment.d.ts +48 -0
  44. package/dist/release/fragment.d.ts.map +1 -0
  45. package/dist/release/fragment.js +84 -0
  46. package/dist/release/index.d.ts +26 -0
  47. package/dist/release/index.d.ts.map +1 -0
  48. package/dist/release/index.js +18 -0
  49. package/dist/release/ledger.d.ts +53 -0
  50. package/dist/release/ledger.d.ts.map +1 -0
  51. package/dist/release/ledger.js +109 -0
  52. package/dist/release/payload-digest.d.ts +63 -0
  53. package/dist/release/payload-digest.d.ts.map +1 -0
  54. package/dist/release/payload-digest.js +65 -0
  55. package/dist/release/release-plan.d.ts +128 -0
  56. package/dist/release/release-plan.d.ts.map +1 -0
  57. package/dist/release/release-plan.js +277 -0
  58. package/dist/release/version-stamp.d.ts +50 -0
  59. package/dist/release/version-stamp.d.ts.map +1 -0
  60. package/dist/release/version-stamp.js +119 -0
  61. package/dist/release/workspace-config.d.ts +41 -0
  62. package/dist/release/workspace-config.d.ts.map +1 -0
  63. package/dist/release/workspace-config.js +60 -0
  64. package/dist/schema-compat.js +2 -1
  65. package/dist/validate-module-artifact.d.ts.map +1 -1
  66. package/dist/validate-module-artifact.js +53 -1
  67. package/dist/validate-step-inputs.d.ts +1 -1
  68. package/dist/validate-step-inputs.d.ts.map +1 -1
  69. package/dist/validate-step-inputs.js +99 -8
  70. package/package.json +3 -3
  71. package/src/analyzer.ts +41 -10
  72. package/src/artifact-layer-index.ts +47 -14
  73. package/src/artifact-selector.ts +15 -4
  74. package/src/builtins.ts +45 -43
  75. package/src/cel-environment.ts +37 -0
  76. package/src/flatten-for-analyzer.ts +12 -0
  77. package/src/index.ts +19 -2
  78. package/src/kernel-globals.ts +23 -1
  79. package/src/manifest-schemas.ts +408 -0
  80. package/src/module-file-claims.ts +42 -2
  81. package/src/module-library.ts +208 -0
  82. package/src/module-metadata-scope.ts +88 -0
  83. package/src/parse-loaded-file.ts +16 -0
  84. package/src/release/bump-level.ts +95 -0
  85. package/src/release/changelog.ts +62 -0
  86. package/src/release/fragment.ts +130 -0
  87. package/src/release/index.ts +66 -0
  88. package/src/release/ledger.ts +142 -0
  89. package/src/release/payload-digest.ts +83 -0
  90. package/src/release/release-plan.ts +392 -0
  91. package/src/release/version-stamp.ts +142 -0
  92. package/src/release/workspace-config.ts +81 -0
  93. package/src/schema-compat.ts +1 -1
  94. package/src/validate-module-artifact.ts +56 -0
  95. package/src/validate-step-inputs.ts +109 -9
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Turning evidence into a release plan: which modules bump, to what, and why.
3
+ *
4
+ * Three questions, three mechanisms, and keeping them apart is the whole design:
5
+ *
6
+ * - **Whether** a module bumps is decided by the **payload digest** — exact,
7
+ * from the bytes, so it sees an inlined sibling and a lockfile-only transitive
8
+ * bump that no path-scoped rule can.
9
+ * - **At what level** is decided by the **edge graph** — a declared fragment is a
10
+ * root, and a dependent mirrors its dependency's level, joined as the maximum
11
+ * over paths, because a module that inlines a breaking change is breaking for
12
+ * its own consumers.
13
+ * - **Whether a changelog line is requested** is decided by the path-scoped
14
+ * changed-files rule, which used to decide the version. Demoted, its guesswork
15
+ * costs one sentence rather than a spurious republish.
16
+ *
17
+ * The seam between digest and graph is *reported*, never papered over: a payload
18
+ * that moved with nothing to attribute it to — a third-party dependency, a
19
+ * changesets-owned package inlined into a module, a toolchain bump — resolves to
20
+ * `patch` and says so.
21
+ *
22
+ * Pure data in, plan out. Everything Node-shaped — building payloads, running
23
+ * the controller builder, reading git — is the CLI's half, so the editor can
24
+ * answer "what does changing this library bump?" from the same model.
25
+ */
26
+ import { applyBump, maxLevel, levelOfKind, } from "./bump-level.js";
27
+ import { diffLayerDigests } from "./payload-digest.js";
28
+ export function planRelease(evidence) {
29
+ const diagnostics = [];
30
+ const byKey = new Map(evidence.modules.map((module) => [module.key, module]));
31
+ checkRegistryAgreement(evidence, diagnostics);
32
+ // What the digest says, per module. A module with no ledger entry has never
33
+ // been published, which is not drift — there is nothing to differ from.
34
+ const drift = new Map();
35
+ for (const module of evidence.modules) {
36
+ const recorded = evidence.ledger.modules.get(module.key);
37
+ if (!recorded)
38
+ continue;
39
+ if (recorded.version !== module.version) {
40
+ diagnostics.push({
41
+ severity: "error",
42
+ code: "LEDGER_VERSION_MISMATCH",
43
+ message: `${module.key} declares version ${module.version} but the ledger records ` +
44
+ `${recorded.version} as published. The digests beside it were taken at that version, ` +
45
+ `so nothing here can be compared. Reconcile with \`telo release verify\` — or, if the ` +
46
+ `version was moved by hand, restore it and let \`telo release apply\` move it.`,
47
+ });
48
+ continue;
49
+ }
50
+ const changes = diffLayerDigests(recorded.layers, module.layers);
51
+ if (changes.length > 0)
52
+ drift.set(module.key, changes);
53
+ }
54
+ const levels = new Map();
55
+ const reasons = new Map();
56
+ const addReason = (key, reason) => {
57
+ const list = reasons.get(key);
58
+ if (list)
59
+ list.push(reason);
60
+ else
61
+ reasons.set(key, [reason]);
62
+ };
63
+ // Declared roots.
64
+ const consumed = new Set();
65
+ for (const fragment of evidence.fragments) {
66
+ for (const [key, kind] of fragment.modules) {
67
+ if (!byKey.has(key)) {
68
+ diagnostics.push({
69
+ severity: "error",
70
+ code: "FRAGMENT_UNKNOWN_MODULE",
71
+ message: `${fragment.source} names '${key}', which is not a module in this workspace. ` +
72
+ `A fragment names a module by its workspace-relative directory path.`,
73
+ });
74
+ continue;
75
+ }
76
+ const level = levelOfKind(kind);
77
+ if (level === "major") {
78
+ diagnostics.push({
79
+ severity: "error",
80
+ code: "MAJOR_BUMP_REJECTED",
81
+ message: `${fragment.source} declares '${key}: ${kind}', which induces a major bump. ` +
82
+ `Modules are intentionally pre-1.0 — a breaking change ships as a minor. ` +
83
+ `Use Added and describe the break in the body.`,
84
+ });
85
+ continue;
86
+ }
87
+ consumed.add(fragment.source);
88
+ levels.set(key, maxLevel(levels.get(key) ?? level, level));
89
+ addReason(key, { kind: "declared", fragment: fragment.source, as: kind });
90
+ }
91
+ }
92
+ // Edges already credited, so a second propagation pass does not repeat an
93
+ // explanation the author has already been given.
94
+ const credited = new Set();
95
+ propagateToFixedPoint(evidence.modules, drift, levels, credited, addReason);
96
+ // The seam. A payload that moved with nothing to attribute it to still has to
97
+ // ship — the fix would otherwise reach nobody — so it takes a patch and is
98
+ // named. Seeded after propagation and then propagated again, because an
99
+ // unattributed bump is itself a dependency move for anything importing it.
100
+ for (;;) {
101
+ let seeded = false;
102
+ for (const module of evidence.modules) {
103
+ if (levels.has(module.key) || !drift.has(module.key))
104
+ continue;
105
+ levels.set(module.key, "patch");
106
+ addReason(module.key, { kind: "unattributed" });
107
+ seeded = true;
108
+ }
109
+ if (!seeded)
110
+ break;
111
+ propagateToFixedPoint(evidence.modules, drift, levels, credited, addReason);
112
+ }
113
+ const entriesByModule = collectChangelogEntries(evidence.fragments, byKey);
114
+ requestMissingChangelogEntries(evidence.modules, entriesByModule, diagnostics);
115
+ const planned = [];
116
+ for (const module of orderByImports(evidence.modules)) {
117
+ const level = levels.get(module.key);
118
+ if (!level)
119
+ continue;
120
+ planned.push({
121
+ key: module.key,
122
+ name: module.name,
123
+ from: module.version,
124
+ to: applyBump(module.version, level),
125
+ level,
126
+ reasons: reasons.get(module.key) ?? [],
127
+ changed: drift.get(module.key) ?? [],
128
+ entries: entriesByModule.get(module.key) ?? [],
129
+ });
130
+ }
131
+ return { modules: planned, fragments: [...consumed].sort(), diagnostics };
132
+ }
133
+ /**
134
+ * Raise levels along release edges until nothing moves.
135
+ *
136
+ * The two edge kinds answer different questions and so are gated differently:
137
+ *
138
+ * - An **import** edge bumps unconditionally. Publishing rewrites a relative
139
+ * `imports:` source to `<base>/<sibling>@<version>`, so when the sibling's
140
+ * version moves this module's manifest layer provably changes — a fact about
141
+ * the plan, which the current digest cannot yet show because it was taken
142
+ * against the sibling's *current* version.
143
+ * - An **inline** edge only explains drift that already showed up. The inlined
144
+ * bytes are in this payload, so if they had changed the digest would say so;
145
+ * attributing without that check would bump every dependent of a module whose
146
+ * fragment covers a docs-only change.
147
+ */
148
+ function propagateToFixedPoint(modules, drift, levels, credited, addReason) {
149
+ for (let moved = true; moved;) {
150
+ moved = false;
151
+ for (const module of modules) {
152
+ for (const dependency of module.imports) {
153
+ moved = raise(module, dependency, { kind: "imports", module: dependency }) || moved;
154
+ }
155
+ if (!drift.has(module.key))
156
+ continue;
157
+ for (const [dependency, files] of module.inlines) {
158
+ moved =
159
+ raise(module, dependency, { kind: "inlines", module: dependency, files }) || moved;
160
+ }
161
+ }
162
+ }
163
+ function raise(module, dependency, reason) {
164
+ const from = levels.get(dependency);
165
+ if (!from)
166
+ return false;
167
+ const current = levels.get(module.key);
168
+ const next = current ? maxLevel(current, from) : from;
169
+ // The reason is recorded the first time this edge carries anything, even
170
+ // when it does not raise the level: it is the explanation the author reads,
171
+ // and an edge that merely agrees with another still says why this module is
172
+ // in the plan.
173
+ const edge = `${module.key}\0${dependency}\0${reason.kind}`;
174
+ if (!credited.has(edge)) {
175
+ credited.add(edge);
176
+ addReason(module.key, reason);
177
+ }
178
+ if (current === next)
179
+ return false;
180
+ levels.set(module.key, next);
181
+ return true;
182
+ }
183
+ }
184
+ function collectChangelogEntries(fragments, byKey) {
185
+ const entries = new Map();
186
+ for (const fragment of fragments) {
187
+ for (const [key, kind] of fragment.modules) {
188
+ if (!byKey.has(key))
189
+ continue;
190
+ const list = entries.get(key);
191
+ const entry = { kind, body: fragment.body, fragment: fragment.source };
192
+ if (list)
193
+ list.push(entry);
194
+ else
195
+ entries.set(key, [entry]);
196
+ }
197
+ }
198
+ return entries;
199
+ }
200
+ /**
201
+ * Ask for prose where a human made a semantic change.
202
+ *
203
+ * A warning, not an error: `check`'s job is "can a complete, consistent plan be
204
+ * formed", and a module that drifted through propagation or a toolchain bump is
205
+ * planned and released without anyone writing a word. This fires only for a
206
+ * module whose OWN files moved, which is the case where the changelog would
207
+ * otherwise be silent about a real change.
208
+ */
209
+ function requestMissingChangelogEntries(modules, entries, diagnostics) {
210
+ const missing = modules
211
+ .filter((module) => module.ownFilesChanged && !entries.has(module.key))
212
+ .map((module) => module.key);
213
+ if (missing.length === 0)
214
+ return;
215
+ // ONE diagnostic listing the modules, not one per module. A change that
216
+ // touches every module's build script or a shared config asks the same
217
+ // question about forty of them at once, and forty copies of one sentence bury
218
+ // the plan they are printed beside — the more so because they go to stderr
219
+ // while the plan goes to stdout, so a piped run interleaves them.
220
+ //
221
+ // The fix for all of them is also one fragment, since a fragment names as many
222
+ // modules as it likes.
223
+ diagnostics.push({
224
+ severity: "warning",
225
+ code: "CHANGELOG_ENTRY_REQUESTED",
226
+ message: `${missing.length} module(s) have their own changes but no fragment describes them, so ` +
227
+ `their changelogs will not mention this release: ${missing.join(", ")}. ` +
228
+ `One \`telo release add\` can name them all.`,
229
+ });
230
+ }
231
+ /**
232
+ * The base the digests were built against has to be the base they were recorded
233
+ * against, or the manifest layers are not comparable: canonicalization writes
234
+ * the destination into them.
235
+ */
236
+ function checkRegistryAgreement(evidence, diagnostics) {
237
+ const recorded = evidence.ledger.registry;
238
+ if (!recorded || !evidence.registry || recorded === evidence.registry)
239
+ return;
240
+ diagnostics.push({
241
+ severity: "error",
242
+ code: "LEDGER_REGISTRY_MISMATCH",
243
+ message: `The ledger's digests were taken against '${recorded}', but this run built against ` +
244
+ `'${evidence.registry}'. Publishing rewrites each relative import to ` +
245
+ `'<base>/<sibling>@<version>', so the manifest layers of the two are different bytes ` +
246
+ `and comparing them would report every module as changed.`,
247
+ });
248
+ }
249
+ /**
250
+ * Dependency order over in-repo imports — a dependency before its dependents.
251
+ *
252
+ * This is also the publish order, and it is not optional there: publishing
253
+ * canonicalizes a relative import and then hard-fails when the derived ref does
254
+ * not already resolve, so a sibling has to be pushed first.
255
+ *
256
+ * A cycle keeps its members in key order rather than throwing. A module graph
257
+ * should not have one, but a release is the wrong moment to discover it, and the
258
+ * ordering degrades to "arbitrary among the cycle" rather than to nothing.
259
+ */
260
+ export function orderByImports(modules) {
261
+ const byKey = new Map(modules.map((module) => [module.key, module]));
262
+ const ordered = [];
263
+ const state = new Map();
264
+ const visit = (key) => {
265
+ const module = byKey.get(key);
266
+ if (!module || state.get(key))
267
+ return;
268
+ state.set(key, "visiting");
269
+ for (const dependency of [...module.imports].sort())
270
+ visit(dependency);
271
+ state.set(key, "done");
272
+ ordered.push(module);
273
+ };
274
+ for (const key of [...byKey.keys()].sort())
275
+ visit(key);
276
+ return ordered;
277
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Writing a module's one version into every manifest it owns.
3
+ *
4
+ * A module has a single version across `telo.yaml`, `nodejs/package.json` and
5
+ * `rust/Cargo.toml`. Three formats, one rule: find the scalar, splice over its
6
+ * span, touch nothing else. That is `yaml-source-edit.ts`'s primitive — the same
7
+ * one the quick fix, `telo migrate` and `telo upgrade`'s pin rewrite use — so a
8
+ * bump lands as a one-line diff instead of a re-serialized file that re-folds
9
+ * every block scalar in a 900-line manifest.
10
+ *
11
+ * Each stamp returns `undefined` when the file carries no version to write,
12
+ * which is not an error: a module may own only a `telo.yaml`, and 42 of the
13
+ * standard library's packages have no Rust crate. A file that *has* a version in
14
+ * a shape this cannot address is a hard error instead, because silently skipping
15
+ * it would publish an artifact whose manifests disagree about what it is.
16
+ */
17
+ export declare class VersionStampError extends Error {
18
+ }
19
+ /**
20
+ * `metadata.version` on the **module doc** — the first document, the one whose
21
+ * kind is `Telo.Application` or `Telo.Library`. Deliberately not "any
22
+ * `metadata.version` in the file": a `Telo.Definition` further down may carry
23
+ * one, and the regex-replacement changie was configured with matched by line
24
+ * shape rather than by position, which is why it needed a hand-maintained count
25
+ * of how many lines it was allowed to hit.
26
+ */
27
+ export declare function stampManifestVersion(text: string, version: string, where: string): string | undefined;
28
+ /** Read the module doc's `metadata.version` without rewriting it. */
29
+ export declare function readManifestVersion(text: string): string | undefined;
30
+ /**
31
+ * The top-level `"version"` of a `package.json`.
32
+ *
33
+ * Parsed with the YAML reader rather than `JSON.parse`, because JSON is a YAML
34
+ * subset and this one needs the node's *source range* — `JSON.parse` discards it,
35
+ * and re-serializing with `JSON.stringify` would reformat a file whose
36
+ * indentation, key order and trailing newline are all conventions someone chose.
37
+ */
38
+ export declare function stampPackageVersion(text: string, version: string, where: string): string | undefined;
39
+ /**
40
+ * `version` in a `Cargo.toml`'s `[package]` table.
41
+ *
42
+ * Scanned rather than parsed: TOML is not YAML, adding a TOML parser to the
43
+ * browser-safe analyzer for one scalar is not a trade worth making, and the
44
+ * shape being addressed is the canonical one cargo itself writes. The scan is
45
+ * bounded to the `[package]` table so a `version` under `[dependencies.x]`
46
+ * cannot be hit, and a `[package]` whose version is not a simple quoted scalar
47
+ * is refused rather than guessed at.
48
+ */
49
+ export declare function stampCrateVersion(text: string, version: string, where: string): string | undefined;
50
+ //# sourceMappingURL=version-stamp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-stamp.d.ts","sourceRoot":"","sources":["../../src/release/version-stamp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAqB/C;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,SAAS,CAYpB;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMpE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,SAAS,CAKpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,SAAS,CA+BpB"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Writing a module's one version into every manifest it owns.
3
+ *
4
+ * A module has a single version across `telo.yaml`, `nodejs/package.json` and
5
+ * `rust/Cargo.toml`. Three formats, one rule: find the scalar, splice over its
6
+ * span, touch nothing else. That is `yaml-source-edit.ts`'s primitive — the same
7
+ * one the quick fix, `telo migrate` and `telo upgrade`'s pin rewrite use — so a
8
+ * bump lands as a one-line diff instead of a re-serialized file that re-folds
9
+ * every block scalar in a 900-line manifest.
10
+ *
11
+ * Each stamp returns `undefined` when the file carries no version to write,
12
+ * which is not an error: a module may own only a `telo.yaml`, and 42 of the
13
+ * standard library's packages have no Rust crate. A file that *has* a version in
14
+ * a shape this cannot address is a hard error instead, because silently skipping
15
+ * it would publish an artifact whose manifests disagree about what it is.
16
+ */
17
+ import { defaultCustomTags } from "@telorun/templating";
18
+ import { isScalar, parseAllDocuments, parseDocument } from "yaml";
19
+ import { applyTextEdits, renderFixReplacement } from "../yaml-source-edit.js";
20
+ export class VersionStampError extends Error {
21
+ }
22
+ /** Replace the scalar at `[start, end)` with `version`, re-quoted in the
23
+ * author's own style. */
24
+ function spliceScalar(text, node, version, where) {
25
+ const range = node.range;
26
+ if (!range) {
27
+ throw new VersionStampError(`${where}: the version scalar carries no source range.`);
28
+ }
29
+ const [start, end] = range;
30
+ const source = text.slice(start, end);
31
+ const replacement = renderFixReplacement(source, version);
32
+ if (replacement === undefined) {
33
+ throw new VersionStampError(`${where}: the version is written as '${source}', which cannot be rewritten in place. ` +
34
+ `Write it as a plain or quoted scalar on one line.`);
35
+ }
36
+ return applyTextEdits(text, [{ start, end, newText: replacement }]);
37
+ }
38
+ /**
39
+ * `metadata.version` on the **module doc** — the first document, the one whose
40
+ * kind is `Telo.Application` or `Telo.Library`. Deliberately not "any
41
+ * `metadata.version` in the file": a `Telo.Definition` further down may carry
42
+ * one, and the regex-replacement changie was configured with matched by line
43
+ * shape rather than by position, which is why it needed a hand-maintained count
44
+ * of how many lines it was allowed to hit.
45
+ */
46
+ export function stampManifestVersion(text, version, where) {
47
+ const docs = parseAllDocuments(text, { customTags: defaultCustomTags() });
48
+ const doc = docs[0];
49
+ if (!doc)
50
+ return undefined;
51
+ const metadata = doc.get("metadata", true);
52
+ if (!metadata || typeof metadata.get !== "function")
53
+ return undefined;
54
+ const node = metadata.get("version", true);
55
+ if (!isScalar(node))
56
+ return undefined;
57
+ return spliceScalar(text, node, version, where);
58
+ }
59
+ /** Read the module doc's `metadata.version` without rewriting it. */
60
+ export function readManifestVersion(text) {
61
+ const docs = parseAllDocuments(text, { customTags: defaultCustomTags() });
62
+ const first = docs[0]?.toJSON();
63
+ if (first?.kind !== "Telo.Application" && first?.kind !== "Telo.Library")
64
+ return undefined;
65
+ const version = first.metadata?.version;
66
+ return typeof version === "string" ? version : undefined;
67
+ }
68
+ /**
69
+ * The top-level `"version"` of a `package.json`.
70
+ *
71
+ * Parsed with the YAML reader rather than `JSON.parse`, because JSON is a YAML
72
+ * subset and this one needs the node's *source range* — `JSON.parse` discards it,
73
+ * and re-serializing with `JSON.stringify` would reformat a file whose
74
+ * indentation, key order and trailing newline are all conventions someone chose.
75
+ */
76
+ export function stampPackageVersion(text, version, where) {
77
+ const doc = parseDocument(text);
78
+ const node = doc.get("version", true);
79
+ if (!isScalar(node))
80
+ return undefined;
81
+ return spliceScalar(text, node, version, where);
82
+ }
83
+ /**
84
+ * `version` in a `Cargo.toml`'s `[package]` table.
85
+ *
86
+ * Scanned rather than parsed: TOML is not YAML, adding a TOML parser to the
87
+ * browser-safe analyzer for one scalar is not a trade worth making, and the
88
+ * shape being addressed is the canonical one cargo itself writes. The scan is
89
+ * bounded to the `[package]` table so a `version` under `[dependencies.x]`
90
+ * cannot be hit, and a `[package]` whose version is not a simple quoted scalar
91
+ * is refused rather than guessed at.
92
+ */
93
+ export function stampCrateVersion(text, version, where) {
94
+ const table = /^[ \t]*\[package\][ \t]*$/m.exec(text);
95
+ if (!table)
96
+ return undefined;
97
+ const bodyStart = table.index + table[0].length;
98
+ const next = /^[ \t]*\[/m.exec(text.slice(bodyStart));
99
+ const bodyEnd = next ? bodyStart + next.index : text.length;
100
+ const entry = /^([ \t]*version[ \t]*=[ \t]*)(".*?"|'.*?')[ \t]*$/m.exec(text.slice(bodyStart, bodyEnd));
101
+ if (!entry) {
102
+ // A `[package]` with `version.workspace = true` inherits from the workspace
103
+ // and genuinely has nothing here to stamp; anything else is a shape this
104
+ // cannot address, and writing nothing would leave the crate behind.
105
+ if (/^[ \t]*version[ \t]*\.[ \t]*workspace[ \t]*=/m.test(text.slice(bodyStart, bodyEnd))) {
106
+ return undefined;
107
+ }
108
+ if (/^[ \t]*version[ \t]*=/m.test(text.slice(bodyStart, bodyEnd))) {
109
+ throw new VersionStampError(`${where}: [package].version is not a quoted scalar on one line, so it cannot be ` +
110
+ `rewritten in place.`);
111
+ }
112
+ return undefined;
113
+ }
114
+ const start = bodyStart + entry.index + entry[1].length;
115
+ const quote = entry[2][0];
116
+ return applyTextEdits(text, [
117
+ { start, end: start + entry[2].length, newText: `${quote}${version}${quote}` },
118
+ ]);
119
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `telo-workspace.yaml` — the release anchor.
3
+ *
4
+ * Every path the release system names — a module key, a ledger entry, a
5
+ * fragment's `modules:` — is relative to this file's directory. That is the
6
+ * whole of its job: **its location is the anchor**, and its one field names the
7
+ * subtrees that may hold modules, which is not derivable (a whole-tree scan
8
+ * would read every example and every cached `.telo/manifests` copy as a released
9
+ * module).
10
+ *
11
+ * The file is **optional and read only by `telo release`**. Nothing else — not
12
+ * `run`, `check`, `publish`, `install`, `upgrade`, `migrate`, `module`, and not
13
+ * the kernel — looks for it, so a single-manifest repo, a bare `examples/`
14
+ * directory and a third-party module checkout keep working with nothing added.
15
+ * Any field added later must be true of the whole tree, not derivable from it,
16
+ * and harmless by its absence.
17
+ *
18
+ * Parsing lives here, in the browser-safe half, because the editor answers
19
+ * "what does changing this library bump?" from the same model. Finding the file
20
+ * on disk is the CLI's half — this side takes text.
21
+ */
22
+ export declare const WORKSPACE_FILENAME = "telo-workspace.yaml";
23
+ export interface WorkspaceConfig {
24
+ /**
25
+ * Gitignore-style patterns, workspace-relative, naming the subtrees that may
26
+ * hold modules (`modules/*`, `apps/*`). A pattern names a place to look, never
27
+ * a module: what makes a directory a module is its `telo.yaml`.
28
+ */
29
+ readonly modules: readonly string[];
30
+ }
31
+ export declare class WorkspaceConfigError extends Error {
32
+ }
33
+ /**
34
+ * Parse the marker file's text.
35
+ *
36
+ * Strict about its one field, because there is nothing here to be lenient with:
37
+ * an empty or absent `modules:` names no subtree, so discovery would find no
38
+ * module and every gate would silently pass over a whole repo.
39
+ */
40
+ export declare function parseWorkspaceConfig(text: string, where: string): WorkspaceConfig;
41
+ //# sourceMappingURL=workspace-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-config.d.ts","sourceRoot":"","sources":["../../src/release/workspace-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,qBAAa,oBAAqB,SAAQ,KAAK;CAAG;AAElD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,CAoCjF"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `telo-workspace.yaml` — the release anchor.
3
+ *
4
+ * Every path the release system names — a module key, a ledger entry, a
5
+ * fragment's `modules:` — is relative to this file's directory. That is the
6
+ * whole of its job: **its location is the anchor**, and its one field names the
7
+ * subtrees that may hold modules, which is not derivable (a whole-tree scan
8
+ * would read every example and every cached `.telo/manifests` copy as a released
9
+ * module).
10
+ *
11
+ * The file is **optional and read only by `telo release`**. Nothing else — not
12
+ * `run`, `check`, `publish`, `install`, `upgrade`, `migrate`, `module`, and not
13
+ * the kernel — looks for it, so a single-manifest repo, a bare `examples/`
14
+ * directory and a third-party module checkout keep working with nothing added.
15
+ * Any field added later must be true of the whole tree, not derivable from it,
16
+ * and harmless by its absence.
17
+ *
18
+ * Parsing lives here, in the browser-safe half, because the editor answers
19
+ * "what does changing this library bump?" from the same model. Finding the file
20
+ * on disk is the CLI's half — this side takes text.
21
+ */
22
+ import { parseDocument } from "yaml";
23
+ export const WORKSPACE_FILENAME = "telo-workspace.yaml";
24
+ export class WorkspaceConfigError extends Error {
25
+ }
26
+ /**
27
+ * Parse the marker file's text.
28
+ *
29
+ * Strict about its one field, because there is nothing here to be lenient with:
30
+ * an empty or absent `modules:` names no subtree, so discovery would find no
31
+ * module and every gate would silently pass over a whole repo.
32
+ */
33
+ export function parseWorkspaceConfig(text, where) {
34
+ let value;
35
+ try {
36
+ value = parseDocument(text).toJSON();
37
+ }
38
+ catch (err) {
39
+ throw new WorkspaceConfigError(`${where} is not valid YAML: ${err instanceof Error ? err.message : String(err)}`);
40
+ }
41
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
42
+ throw new WorkspaceConfigError(`${where} must be a YAML mapping.`);
43
+ }
44
+ const record = value;
45
+ for (const key of Object.keys(record)) {
46
+ if (key !== "modules") {
47
+ throw new WorkspaceConfigError(`${where}: unknown field '${key}'. The workspace marker carries only 'modules:' — ` +
48
+ `the subtrees that may hold modules.`);
49
+ }
50
+ }
51
+ const modules = record.modules;
52
+ if (!Array.isArray(modules) || modules.some((entry) => typeof entry !== "string")) {
53
+ throw new WorkspaceConfigError(`${where}: 'modules' must be a list of path patterns, e.g. [modules/*, apps/*].`);
54
+ }
55
+ if (modules.length === 0) {
56
+ throw new WorkspaceConfigError(`${where}: 'modules' is empty, so no directory can ever be discovered as a module. ` +
57
+ `List the subtrees that hold them, e.g. [modules/*, apps/*].`);
58
+ }
59
+ return { modules: modules };
60
+ }
@@ -1,7 +1,8 @@
1
1
  import AjvModule from "ajv";
2
2
  import addFormats from "ajv-formats";
3
- import { isRefSentinel, isTaggedSentinel, ManifestRootSchema, producedTypeOf, } from "@telorun/templating";
3
+ import { isRefSentinel, isTaggedSentinel, producedTypeOf, } from "@telorun/templating";
4
4
  import { celBaseOfValueType, celTypeOfValueType, readValueTypeSlot, valueBrandBases, valueTypeOf, valueTypePlaceholder, } from "@telorun/sdk";
5
+ import { ManifestRootSchema } from "./manifest-schemas.js";
5
6
  import { registerTeloKeywords } from "./value-type-keyword.js";
6
7
  const Ajv = AjvModule.default ?? AjvModule;
7
8
  /** Creates a configured AJV instance (allErrors, strict: false, with formats).
@@ -1 +1 @@
1
- {"version":3,"file":"validate-module-artifact.d.ts","sourceRoot":"","sources":["../src/validate-module-artifact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,kBAAkB,EAAE,CAO1F"}
1
+ {"version":3,"file":"validate-module-artifact.d.ts","sourceRoot":"","sources":["../src/validate-module-artifact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAUrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,kBAAkB,EAAE,CAQ1F"}
@@ -1,5 +1,6 @@
1
1
  import { parseLayerIndex, LayerIndexError } from "./artifact-layer-index.js";
2
- import { ArtifactSelectorError, PLATFORM_AXES, selectorFromQualifiers, } from "./artifact-selector.js";
2
+ import { ArtifactSelectorError, PLATFORM_AXES, selectorFromQualifiers, selectorKey, } from "./artifact-selector.js";
3
+ import { readLibraryCandidates } from "./module-library.js";
3
4
  import { DiagnosticSeverity } from "./types.js";
4
5
  const SOURCE = "telo-analyzer";
5
6
  /**
@@ -30,9 +31,60 @@ export function validateModuleArtifact(manifests) {
30
31
  for (const manifest of manifests) {
31
32
  validateLayerIndex(manifest, out);
32
33
  validateControllerSelectors(manifest, out);
34
+ validateLibraryCandidates(manifest, out);
33
35
  }
34
36
  return out;
35
37
  }
38
+ /**
39
+ * The `exports.code:` block on a `Telo.Library` doc.
40
+ *
41
+ * Reported here rather than left to the loader for the same reason a controller
42
+ * selector is: an entry that cannot be read names no entry point, so a
43
+ * consumer's bundle falls back to *inlining* the library — the module scope
44
+ * duplication this whole mechanism exists to remove — and it does so silently, on
45
+ * someone else's machine.
46
+ */
47
+ function validateLibraryCandidates(manifest, out) {
48
+ // `Telo.Library` only. An application is a root with no importer, so it has no
49
+ // `exports:` block at all — and its schema is `additionalProperties: false`,
50
+ // so AJV already rejects the key by name in this same pass. A second
51
+ // diagnostic on that node would be two squiggles saying one thing.
52
+ if (manifest.kind !== "Telo.Library")
53
+ return;
54
+ const metadata = manifest.metadata;
55
+ const { candidates, problems } = readLibraryCandidates(manifest);
56
+ const resource = { kind: manifest.kind, name: metadata?.name };
57
+ for (const problem of problems) {
58
+ out.push({
59
+ severity: DiagnosticSeverity.Error,
60
+ code: "LIBRARY_CANDIDATE_INVALID",
61
+ source: SOURCE,
62
+ message: `Telo.Library/${metadata?.name ?? "(unnamed)"}: ${problem.origin}: ${problem.detail}`,
63
+ data: { resource, filePath: metadata?.source, path: "exports/code" },
64
+ });
65
+ }
66
+ // One specifier per selector: two candidates of one format claiming the same
67
+ // specifier leave the resolution ambiguous, and two specifiers for one format
68
+ // mean a consumer's import resolves by whichever candidate is read first.
69
+ const seen = new Map();
70
+ for (const candidate of candidates) {
71
+ const key = selectorKey(candidate.selector);
72
+ const first = seen.get(key);
73
+ if (first) {
74
+ out.push({
75
+ severity: DiagnosticSeverity.Error,
76
+ code: "LIBRARY_CANDIDATE_DUPLICATE",
77
+ source: SOURCE,
78
+ message: `Telo.Library/${metadata?.name ?? "(unnamed)"}: two 'exports.code' entries declare the ` +
79
+ `selector ${key} ('${first.specifier}' and '${candidate.specifier}'). A module has one ` +
80
+ `entry point per format — which is what makes "one specifier, one module scope" true.`,
81
+ data: { resource, filePath: metadata?.source, path: "exports/code" },
82
+ });
83
+ continue;
84
+ }
85
+ seen.set(key, candidate);
86
+ }
87
+ }
36
88
  /** `local_path` names the source `path=` was built from, so a working copy runs
37
89
  * with no build step. It is inert in a published artifact — which ships no
38
90
  * `src/` — and contributes nothing to the selector, so it never affects which
@@ -6,7 +6,7 @@ export interface StepInputIssue {
6
6
  message: string;
7
7
  /** Set when the issue is a type-argument disagreement rather than a contract
8
8
  * shape violation — the two read differently and deserve their own code. */
9
- code?: "CEL_TYPE_ARGUMENT_MISMATCH";
9
+ code?: "CEL_TYPE_ARGUMENT_MISMATCH" | "LIVE_VALUE_RETRIED";
10
10
  }
11
11
  /**
12
12
  * Validate every step's `inputs:` against the invoked target's declared input
@@ -1 +1 @@
1
- {"version":3,"file":"validate-step-inputs.d.ts","sourceRoot":"","sources":["../src/validate-step-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAoBnE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB;iFAC6E;IAC7E,IAAI,CAAC,EAAE,4BAA4B,CAAC;CACrC;AAGD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY;AACpB;;mEAEmE;AACnE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,cAAc,EAAE,CAiHlB"}
1
+ {"version":3,"file":"validate-step-inputs.d.ts","sourceRoot":"","sources":["../src/validate-step-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAqBnE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB;iFAC6E;IAC7E,IAAI,CAAC,EAAE,4BAA4B,GAAG,oBAAoB,CAAC;CAC5D;AAGD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY;AACpB;;mEAEmE;AACnE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,cAAc,EAAE,CAsJlB"}