@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.
- package/dist/analyzer.d.ts +9 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +38 -10
- package/dist/artifact-layer-index.d.ts +13 -5
- package/dist/artifact-layer-index.d.ts.map +1 -1
- package/dist/artifact-layer-index.js +37 -13
- package/dist/artifact-selector.d.ts +10 -3
- package/dist/artifact-selector.d.ts.map +1 -1
- package/dist/artifact-selector.js +10 -1
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +44 -43
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +35 -0
- package/dist/flatten-for-analyzer.d.ts +4 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +8 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/kernel-globals.d.ts +1 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +15 -1
- package/dist/manifest-schemas.d.ts +407 -0
- package/dist/manifest-schemas.d.ts.map +1 -0
- package/dist/manifest-schemas.js +396 -0
- package/dist/module-file-claims.d.ts +13 -0
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +26 -2
- package/dist/module-library.d.ts +97 -0
- package/dist/module-library.d.ts.map +1 -0
- package/dist/module-library.js +162 -0
- package/dist/module-metadata-scope.d.ts +44 -0
- package/dist/module-metadata-scope.d.ts.map +1 -0
- package/dist/module-metadata-scope.js +82 -0
- package/dist/parse-loaded-file.d.ts.map +1 -1
- package/dist/parse-loaded-file.js +16 -0
- package/dist/release/bump-level.d.ts +46 -0
- package/dist/release/bump-level.d.ts.map +1 -0
- package/dist/release/bump-level.js +80 -0
- package/dist/release/changelog.d.ts +36 -0
- package/dist/release/changelog.d.ts.map +1 -0
- package/dist/release/changelog.js +53 -0
- package/dist/release/fragment.d.ts +48 -0
- package/dist/release/fragment.d.ts.map +1 -0
- package/dist/release/fragment.js +84 -0
- package/dist/release/index.d.ts +26 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +18 -0
- package/dist/release/ledger.d.ts +53 -0
- package/dist/release/ledger.d.ts.map +1 -0
- package/dist/release/ledger.js +109 -0
- package/dist/release/payload-digest.d.ts +63 -0
- package/dist/release/payload-digest.d.ts.map +1 -0
- package/dist/release/payload-digest.js +65 -0
- package/dist/release/release-plan.d.ts +128 -0
- package/dist/release/release-plan.d.ts.map +1 -0
- package/dist/release/release-plan.js +277 -0
- package/dist/release/version-stamp.d.ts +50 -0
- package/dist/release/version-stamp.d.ts.map +1 -0
- package/dist/release/version-stamp.js +119 -0
- package/dist/release/workspace-config.d.ts +41 -0
- package/dist/release/workspace-config.d.ts.map +1 -0
- package/dist/release/workspace-config.js +60 -0
- package/dist/schema-compat.js +2 -1
- package/dist/validate-module-artifact.d.ts.map +1 -1
- package/dist/validate-module-artifact.js +53 -1
- package/dist/validate-step-inputs.d.ts +1 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +99 -8
- package/package.json +3 -3
- package/src/analyzer.ts +41 -10
- package/src/artifact-layer-index.ts +47 -14
- package/src/artifact-selector.ts +15 -4
- package/src/builtins.ts +45 -43
- package/src/cel-environment.ts +37 -0
- package/src/flatten-for-analyzer.ts +12 -0
- package/src/index.ts +19 -2
- package/src/kernel-globals.ts +23 -1
- package/src/manifest-schemas.ts +408 -0
- package/src/module-file-claims.ts +42 -2
- package/src/module-library.ts +208 -0
- package/src/module-metadata-scope.ts +88 -0
- package/src/parse-loaded-file.ts +16 -0
- package/src/release/bump-level.ts +95 -0
- package/src/release/changelog.ts +62 -0
- package/src/release/fragment.ts +130 -0
- package/src/release/index.ts +66 -0
- package/src/release/ledger.ts +142 -0
- package/src/release/payload-digest.ts +83 -0
- package/src/release/release-plan.ts +392 -0
- package/src/release/version-stamp.ts +142 -0
- package/src/release/workspace-config.ts +81 -0
- package/src/schema-compat.ts +1 -1
- package/src/validate-module-artifact.ts +56 -0
- package/src/validate-step-inputs.ts +109 -9
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared JSON-Schema fragments every module's `telo.yaml` may point at with
|
|
3
|
+
* `$ref: "telo://manifest#/$defs/<Name>"`.
|
|
4
|
+
*
|
|
5
|
+
* WHY THE ANALYZER OWNS THEM. These describe manifest structure, which is what
|
|
6
|
+
* this package exists to read — and the layering forces the choice anyway: the
|
|
7
|
+
* editor validates in a browser through `@telorun/analyzer`, and the analyzer
|
|
8
|
+
* must not depend on the kernel. A fragment in the kernel would exist only at
|
|
9
|
+
* runtime, so `telo check` and the editor could not see a step's shape at all.
|
|
10
|
+
* The kernel re-exports these from its own `manifest-schemas` surface, and both
|
|
11
|
+
* halves register the same root document with AJV, so a `$ref` resolves
|
|
12
|
+
* identically in the editor, in `telo check` and at dispatch.
|
|
13
|
+
*
|
|
14
|
+
* A fragment here is STRUCTURE, not a named user type. The two are different
|
|
15
|
+
* mechanisms and both exist:
|
|
16
|
+
*
|
|
17
|
+
* - `#/$defs/<Name>` is private to the declaring kind's own schema — Run's
|
|
18
|
+
* `WhileStep` is Run's business and nothing outside it can name one.
|
|
19
|
+
* - `telo://manifest#/$defs/<Name>` is this set: shapes the kernel defines, so
|
|
20
|
+
* several unrelated documents can agree on one. `builtins.ts` is not a module
|
|
21
|
+
* document and has no `$defs` any module could reach, which is why a shared
|
|
22
|
+
* shape cannot live in one of the modules that use it.
|
|
23
|
+
* - `telo:<module>/<Type>` names a `Telo.JsonSchema` resource a MODULE
|
|
24
|
+
* declared, resolved through the type registry and carrying its owner so two
|
|
25
|
+
* libraries may both declare a `Filter`.
|
|
26
|
+
*
|
|
27
|
+
* Adding a fragment means putting it under `$defs` in {@link ManifestRootSchema}
|
|
28
|
+
* and `$ref`-ing it from module schemas. Browser-safe: no Node built-ins.
|
|
29
|
+
*/
|
|
30
|
+
export const MANIFEST_SCHEMA_URI = "telo://manifest";
|
|
31
|
+
/** `$ref` to a fragment in this set, as a module schema writes it. */
|
|
32
|
+
export function manifestFragmentRef(name) {
|
|
33
|
+
return `${MANIFEST_SCHEMA_URI}#/$defs/${name}`;
|
|
34
|
+
}
|
|
35
|
+
/** Schema fragment for a resource-reference slot. The only form a manifest
|
|
36
|
+
* author writes is the `!ref <name>` (or `!ref <Alias>.<name>`) YAML tag,
|
|
37
|
+
* which parses to a `TaggedSentinel` (engine "ref") whose `source` is the
|
|
38
|
+
* bare resource name. In practice module schemas mark a ref slot with a bare
|
|
39
|
+
* `x-telo-ref` annotation (plus, where the slot only ever holds a reference,
|
|
40
|
+
* `type: object` to reject a stray scalar); the analyzer's reference walker
|
|
41
|
+
* reads `x-telo-ref` to look the name up against that constraint, independent
|
|
42
|
+
* of this fragment. A slot opts into this fragment only when it wants this
|
|
43
|
+
* exact two-branch shape enforced at the AJV layer too — it is not required,
|
|
44
|
+
* and slots that also accept an inline value (e.g. `inputType` / `outputType`)
|
|
45
|
+
* deliberately do not use it (an inline JSON Schema has no `kind`).
|
|
46
|
+
*
|
|
47
|
+
* Two `anyOf` branches because the value's shape depends on the phase at
|
|
48
|
+
* which it is validated:
|
|
49
|
+
*
|
|
50
|
+
* 1. The raw `!ref` sentinel — what survives to AJV when a cross-module
|
|
51
|
+
* reference can't be resolved in standalone single-file analysis (the
|
|
52
|
+
* imported module isn't loaded). `substituteCelFields` deliberately
|
|
53
|
+
* keeps the sentinel so this branch matches.
|
|
54
|
+
* 2. A resolved reference object — `{kind, name, alias?}` substituted in
|
|
55
|
+
* place of a sentinel (or an inline definition `{kind, ...config}`
|
|
56
|
+
* reached through a local `$ref` that escapes extraction). Both the
|
|
57
|
+
* kernel and the analyzer validate ref slots *after* sentinel
|
|
58
|
+
* resolution, so this is the shape AJV usually sees.
|
|
59
|
+
*
|
|
60
|
+
* The object-form `{kind, name}` reference a user could once type directly
|
|
61
|
+
* is gone: a plain object at a ref slot is only ever an inline definition
|
|
62
|
+
* or the resolver's own substitution, never an author-written reference.
|
|
63
|
+
* That removal is enforced by the analyzer (it rejects an author-written
|
|
64
|
+
* `{kind, name}` before normalization), not by this schema — branch 2
|
|
65
|
+
* cannot distinguish an author's `{kind, name}` from the resolver's. */
|
|
66
|
+
export const ResourceRefSchema = {
|
|
67
|
+
title: "Resource reference",
|
|
68
|
+
anyOf: [
|
|
69
|
+
{
|
|
70
|
+
type: "object",
|
|
71
|
+
required: ["__tagged", "engine", "source"],
|
|
72
|
+
properties: {
|
|
73
|
+
__tagged: { const: true },
|
|
74
|
+
engine: { const: "ref" },
|
|
75
|
+
source: { type: "string", minLength: 1 },
|
|
76
|
+
},
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "object",
|
|
81
|
+
required: ["kind"],
|
|
82
|
+
properties: { kind: { type: "string" } },
|
|
83
|
+
additionalProperties: true,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* How a dispatch is re-attempted.
|
|
89
|
+
*
|
|
90
|
+
* ONE shape, because a retry policy is a cross-cutting primitive rather than
|
|
91
|
+
* each composer's own idea: it was written out six times across `run` and
|
|
92
|
+
* `http-client` and had already drifted — one copy declared no defaults, another
|
|
93
|
+
* carried a field the others lacked. Defaults live here, so the Node step leaf
|
|
94
|
+
* and a second-language one read the same numbers instead of each re-deriving
|
|
95
|
+
* them from the other's source.
|
|
96
|
+
*
|
|
97
|
+
* `delay` survives as the older duration-string spelling because published
|
|
98
|
+
* manifests carry it, and it cannot be migrated away: a migration entry writes a
|
|
99
|
+
* SCALAR, and `"250ms"` → `250` is a computation the vocabulary deliberately
|
|
100
|
+
* cannot express.
|
|
101
|
+
*/
|
|
102
|
+
export const RetryPolicySchema = {
|
|
103
|
+
title: "Retry policy",
|
|
104
|
+
description: "Re-attempts a failed dispatch with exponential backoff. A domain failure is " +
|
|
105
|
+
"retried; a cancellation and a contract violation are not — the latter is a " +
|
|
106
|
+
"property of the manifest and would fail identically every time.",
|
|
107
|
+
type: "object",
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
properties: {
|
|
110
|
+
attempts: {
|
|
111
|
+
title: "Attempts",
|
|
112
|
+
description: "Re-attempts after the first try. 0 disables retrying.",
|
|
113
|
+
type: "integer",
|
|
114
|
+
minimum: 0,
|
|
115
|
+
default: 0,
|
|
116
|
+
},
|
|
117
|
+
initialDelay: {
|
|
118
|
+
title: "Initial delay",
|
|
119
|
+
description: "Milliseconds to wait before the first re-attempt.",
|
|
120
|
+
type: "integer",
|
|
121
|
+
minimum: 0,
|
|
122
|
+
default: 250,
|
|
123
|
+
},
|
|
124
|
+
factor: {
|
|
125
|
+
title: "Factor",
|
|
126
|
+
description: "Multiplier applied to the delay after each re-attempt.",
|
|
127
|
+
type: "number",
|
|
128
|
+
minimum: 1,
|
|
129
|
+
default: 2,
|
|
130
|
+
},
|
|
131
|
+
maxDelay: {
|
|
132
|
+
title: "Max delay",
|
|
133
|
+
description: "Ceiling on the delay between re-attempts, in milliseconds.",
|
|
134
|
+
type: "integer",
|
|
135
|
+
minimum: 0,
|
|
136
|
+
default: 32000,
|
|
137
|
+
},
|
|
138
|
+
jitter: {
|
|
139
|
+
title: "Jitter",
|
|
140
|
+
description: "`full` picks each delay uniformly from [0, delay], which is what stops " +
|
|
141
|
+
"work that failed together from re-attempting together.",
|
|
142
|
+
type: "string",
|
|
143
|
+
enum: ["none", "full"],
|
|
144
|
+
default: "full",
|
|
145
|
+
},
|
|
146
|
+
delay: {
|
|
147
|
+
title: "Delay",
|
|
148
|
+
description: "DEPRECATED duration string (`250ms`, `1s`) — read as `initialDelay` when " +
|
|
149
|
+
"that is absent. The pattern is what makes a typo a `telo check` failure " +
|
|
150
|
+
"instead of a silently different backoff.",
|
|
151
|
+
type: "string",
|
|
152
|
+
pattern: "^[0-9]+(\\.[0-9]+)?\\s*(ms|s|m|h)$",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* The bare-count spelling of a re-attempt — `Http.Request.retries`, deprecated
|
|
158
|
+
* but carried by every manifest published before `retry:` existed.
|
|
159
|
+
*
|
|
160
|
+
* Its own fragment rather than a special case in the reader: what a consumer
|
|
161
|
+
* needs to know is WHERE the budget is, and pointing at this shape says "the
|
|
162
|
+
* value itself" as precisely as pointing at a policy says "its `attempts`". It
|
|
163
|
+
* cannot be migrated to the policy form — a migration entry writes a scalar, and
|
|
164
|
+
* wrapping one in an object is not something the vocabulary can express.
|
|
165
|
+
*/
|
|
166
|
+
export const RetryAttemptsSchema = {
|
|
167
|
+
title: "Retry attempts",
|
|
168
|
+
description: "Re-attempts after the first try. 0 disables retrying.",
|
|
169
|
+
type: "integer",
|
|
170
|
+
minimum: 0,
|
|
171
|
+
default: 0,
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* A DISPATCH SITE: name a target, pass it arguments, optionally guard it,
|
|
175
|
+
* optionally re-attempt it.
|
|
176
|
+
*
|
|
177
|
+
* The runtime has always had exactly one of these — `InvokeStep` in the SDK, run
|
|
178
|
+
* by `executeInvokeStep`, which every dispatch passes through. What did not exist
|
|
179
|
+
* was the schema half: the shape was hand-restated by each composer (four times
|
|
180
|
+
* in `run`, once in `builtins.ts`) and they drifted, which is why `retry:` worked
|
|
181
|
+
* in a sequence step and was a schema error one line away in `targets:` — not a
|
|
182
|
+
* decision anyone made about boot, just a copy that never grew the field.
|
|
183
|
+
*
|
|
184
|
+
* Owning it here is also what retired `x-telo-retry` for a step: the analyzer
|
|
185
|
+
* reads `step.retry.attempts` because that is what a step IS, rather than
|
|
186
|
+
* discovering a retry-bearing field through a marker the kind had to remember to
|
|
187
|
+
* write.
|
|
188
|
+
*
|
|
189
|
+
* `name` is optional: a boot target only needs one to publish
|
|
190
|
+
* `steps.<name>.result`, and a composer that requires one says so in its own
|
|
191
|
+
* schema. Closed, so a misspelled key is rejected wherever a dispatch is written.
|
|
192
|
+
*/
|
|
193
|
+
export const InvokeStepSchema = {
|
|
194
|
+
title: "Invoke step",
|
|
195
|
+
description: "Transfers control to a resource: what to call, what to pass it, whether to " +
|
|
196
|
+
"call it, and how to re-attempt it.",
|
|
197
|
+
type: "object",
|
|
198
|
+
required: ["invoke"],
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
properties: {
|
|
201
|
+
name: {
|
|
202
|
+
title: "Name",
|
|
203
|
+
description: "Publishes this dispatch's result as `steps.<name>.result`.",
|
|
204
|
+
type: "string",
|
|
205
|
+
},
|
|
206
|
+
invoke: {
|
|
207
|
+
title: "Invoke",
|
|
208
|
+
description: "Resource to invoke.",
|
|
209
|
+
"x-telo-topology-role": "invoke",
|
|
210
|
+
// A reference is always an object (a `!ref` sentinel or its resolved
|
|
211
|
+
// `{kind, name}`); requiring an object rejects a bare-string ref — which
|
|
212
|
+
// `validateReferenceForms` cannot catch at this nested slot — at
|
|
213
|
+
// `telo check` instead of as an obscure runtime failure.
|
|
214
|
+
type: "object",
|
|
215
|
+
"x-telo-ref": {
|
|
216
|
+
kind: "Telo.Executable",
|
|
217
|
+
use: "call",
|
|
218
|
+
inputs: "/inputs",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
inputs: {
|
|
222
|
+
title: "Inputs",
|
|
223
|
+
description: "Values passed to the invoked resource.",
|
|
224
|
+
"x-telo-topology-role": "inputs",
|
|
225
|
+
type: "object",
|
|
226
|
+
additionalProperties: true,
|
|
227
|
+
},
|
|
228
|
+
when: {
|
|
229
|
+
title: "When",
|
|
230
|
+
description: "CEL guard — the dispatch is skipped when it evaluates false.",
|
|
231
|
+
type: "string",
|
|
232
|
+
},
|
|
233
|
+
retry: {
|
|
234
|
+
title: "Retry",
|
|
235
|
+
$ref: `${MANIFEST_SCHEMA_URI}#/$defs/RetryPolicy`,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
/** Recursively freeze, so the fragment set cannot be edited through any of the
|
|
240
|
+
* references handed out. `fragmentFor` clones precisely because downstream
|
|
241
|
+
* passes rewrite schemas in place — `resolveSchemaRefKinds` rewrites the very
|
|
242
|
+
* `x-telo-ref` node `InvokeStep` carries — and a consumer that embedded a
|
|
243
|
+
* fragment WITHOUT cloning would corrupt every later expansion process-wide, in
|
|
244
|
+
* a host that outlives one load (the editor, the LSP). Freezing turns that from
|
|
245
|
+
* a rule someone has to remember into a throw at the write. */
|
|
246
|
+
function deepFreeze(value) {
|
|
247
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
248
|
+
Object.freeze(value);
|
|
249
|
+
for (const child of Object.values(value))
|
|
250
|
+
deepFreeze(child);
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
/** Root schema registered with AJV under {@link MANIFEST_SCHEMA_URI}. Carries
|
|
255
|
+
* `$defs` only — it isn't validated against directly. */
|
|
256
|
+
export const ManifestRootSchema = {
|
|
257
|
+
$id: MANIFEST_SCHEMA_URI,
|
|
258
|
+
$defs: {
|
|
259
|
+
ResourceRef: ResourceRefSchema,
|
|
260
|
+
RetryPolicy: RetryPolicySchema,
|
|
261
|
+
RetryAttempts: RetryAttemptsSchema,
|
|
262
|
+
InvokeStep: InvokeStepSchema,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
deepFreeze(ManifestRootSchema);
|
|
266
|
+
/** A private, expanded copy of a fragment, for a consumer that must EMBED one
|
|
267
|
+
* rather than `$ref` it — `builtins.ts` is not a manifest and never passes
|
|
268
|
+
* through the loader, so its dispatch site has to arrive already resolved and
|
|
269
|
+
* already stamped. Cloned for the reason {@link deepFreeze} explains. */
|
|
270
|
+
export function manifestFragment(name) {
|
|
271
|
+
const fragment = ManifestRootSchema.$defs[name];
|
|
272
|
+
if (!fragment || typeof fragment !== "object") {
|
|
273
|
+
throw new Error(`Unknown manifest fragment '${name}'`);
|
|
274
|
+
}
|
|
275
|
+
const copy = structuredClone(fragment);
|
|
276
|
+
expandManifestFragments(copy);
|
|
277
|
+
copy[X_TELO_FRAGMENT] = name;
|
|
278
|
+
return copy;
|
|
279
|
+
}
|
|
280
|
+
const FRAGMENT_PREFIX = `${MANIFEST_SCHEMA_URI}#/$defs/`;
|
|
281
|
+
/**
|
|
282
|
+
* Replace every `telo://manifest#/$defs/<Name>` reference with the fragment
|
|
283
|
+
* itself, in place, throughout a parsed manifest.
|
|
284
|
+
*
|
|
285
|
+
* EXPANDED rather than left as a reference, which is the opposite of what
|
|
286
|
+
* `resolveSchemaTypeRefs` does for a named user type — and for the opposite
|
|
287
|
+
* reasons. A user type must stay a reference because it can recurse and because
|
|
288
|
+
* the compiled-validator cache is keyed on schema identity. These fragments are a
|
|
289
|
+
* closed, non-recursive set the analyzer itself owns, and expanding them is what
|
|
290
|
+
* keeps a composer that points at a shared shape legible to walks that never
|
|
291
|
+
* resolved anything: the CEL-placeholder substitution, the eval-path collector,
|
|
292
|
+
* the editor's field walk. Teaching each of those to follow a reference is the
|
|
293
|
+
* same fix applied N times, and the failure mode when one is missed is silent —
|
|
294
|
+
* a role-driven lookup finds nothing and the check it feeds simply stops
|
|
295
|
+
* reporting.
|
|
296
|
+
*
|
|
297
|
+
* Runs in the shared loader, so both kernels' Node halves and every consumer of a
|
|
298
|
+
* loaded manifest see the same expanded shape.
|
|
299
|
+
*/
|
|
300
|
+
export function expandManifestFragments(node, seen = new Set()) {
|
|
301
|
+
if (!node || typeof node !== "object")
|
|
302
|
+
return;
|
|
303
|
+
if (seen.has(node))
|
|
304
|
+
return;
|
|
305
|
+
seen.add(node);
|
|
306
|
+
if (Array.isArray(node)) {
|
|
307
|
+
for (let i = 0; i < node.length; i++) {
|
|
308
|
+
const fragment = fragmentFor(node[i]);
|
|
309
|
+
if (fragment)
|
|
310
|
+
node[i] = fragment;
|
|
311
|
+
else
|
|
312
|
+
expandManifestFragments(node[i], seen);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const obj = node;
|
|
317
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
318
|
+
const fragment = fragmentFor(value);
|
|
319
|
+
if (fragment)
|
|
320
|
+
obj[key] = fragment;
|
|
321
|
+
else
|
|
322
|
+
expandManifestFragments(value, seen);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The fragment a node references, expanded and merged with whatever the node
|
|
327
|
+
* declared beside the `$ref`, or undefined when it references none.
|
|
328
|
+
*
|
|
329
|
+
* SIBLINGS ARE MERGED, which draft-07 would ignore — `$ref` is exclusive there,
|
|
330
|
+
* so `{ $ref, properties: {...} }` silently drops the properties and `allOf` is
|
|
331
|
+
* the only standard alternative. `allOf` cannot preserve
|
|
332
|
+
* `additionalProperties: false`, because a branch only ever sees its own
|
|
333
|
+
* `properties`; a kind extending the shared retry policy with one HTTP-specific
|
|
334
|
+
* field would have had to give up a closed schema to do it. Merging at expansion
|
|
335
|
+
* gives the 2019-09 reading — `$ref` composes rather than replaces — on the
|
|
336
|
+
* draft the validators actually run.
|
|
337
|
+
*
|
|
338
|
+
* The node's own keys WIN, and `properties` merge key-wise, so an extension adds
|
|
339
|
+
* fields without restating the shared ones.
|
|
340
|
+
*
|
|
341
|
+
* The result is STAMPED with the fragment it came from. That stamp is what
|
|
342
|
+
* replaced `x-telo-retry`: a consumer asking "does this field declare a
|
|
343
|
+
* re-attempt, and where is the budget" reads which shape the author pointed at,
|
|
344
|
+
* rather than a marker the author had to remember to write beside it. Derived,
|
|
345
|
+
* never authored — the same standing as `metadata.exportedKinds` — and stripped
|
|
346
|
+
* before AJV like every other `x-telo-*` key, so it cannot affect validation.
|
|
347
|
+
*
|
|
348
|
+
* Deep-copied because downstream passes (`resolveSchemaRefKinds`, migrations)
|
|
349
|
+
* rewrite schemas in place, and a shared object would let one manifest's rewrite
|
|
350
|
+
* reach every other manifest that pointed at the same shape.
|
|
351
|
+
*/
|
|
352
|
+
function fragmentFor(value) {
|
|
353
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
354
|
+
return undefined;
|
|
355
|
+
const node = value;
|
|
356
|
+
const ref = node.$ref;
|
|
357
|
+
if (typeof ref !== "string" || !ref.startsWith(FRAGMENT_PREFIX))
|
|
358
|
+
return undefined;
|
|
359
|
+
const name = ref.slice(FRAGMENT_PREFIX.length);
|
|
360
|
+
const fragment = ManifestRootSchema.$defs[name];
|
|
361
|
+
if (!fragment || typeof fragment !== "object")
|
|
362
|
+
return undefined;
|
|
363
|
+
const expanded = structuredClone(fragment);
|
|
364
|
+
// A fragment may reference another (InvokeStep holds a RetryPolicy); the copy
|
|
365
|
+
// is expanded too, so one pass leaves no reference behind.
|
|
366
|
+
expandManifestFragments(expanded);
|
|
367
|
+
for (const [key, own] of Object.entries(node)) {
|
|
368
|
+
if (key === "$ref")
|
|
369
|
+
continue;
|
|
370
|
+
if (key === "properties" && isPlainObject(own) && isPlainObject(expanded.properties)) {
|
|
371
|
+
expanded.properties = { ...expanded.properties, ...own };
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (key === "required" && Array.isArray(own) && Array.isArray(expanded.required)) {
|
|
375
|
+
expanded.required = [...new Set([...expanded.required, ...own])];
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
expanded[key] = own;
|
|
379
|
+
}
|
|
380
|
+
expanded[X_TELO_FRAGMENT] = name;
|
|
381
|
+
return expanded;
|
|
382
|
+
}
|
|
383
|
+
function isPlainObject(value) {
|
|
384
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
385
|
+
}
|
|
386
|
+
/** Stamped by {@link expandManifestFragments} with the name of the shared
|
|
387
|
+
* fragment a slot pointed at. Derived, never author-written. */
|
|
388
|
+
export const X_TELO_FRAGMENT = "x-telo-fragment";
|
|
389
|
+
/** The shared fragment a schema node was expanded from, or undefined. The one
|
|
390
|
+
* accessor every consumer reads the stamp through. */
|
|
391
|
+
export function manifestFragmentOf(schema) {
|
|
392
|
+
if (!schema || typeof schema !== "object")
|
|
393
|
+
return undefined;
|
|
394
|
+
const name = schema[X_TELO_FRAGMENT];
|
|
395
|
+
return typeof name === "string" ? name : undefined;
|
|
396
|
+
}
|
|
@@ -42,12 +42,25 @@ interface ClaimBase {
|
|
|
42
42
|
* controller claim with no selector that nothing would reject.
|
|
43
43
|
*/
|
|
44
44
|
export type ModuleFileClaim = (ClaimBase & {
|
|
45
|
+
readonly role: "library";
|
|
46
|
+
readonly selector: ArtifactSelector;
|
|
47
|
+
/** The bare specifier a consumer's bundle imports this entry point by. */
|
|
48
|
+
readonly specifier: string;
|
|
49
|
+
/** The source `path` was built from, as on a controller claim. */
|
|
50
|
+
readonly localPath?: string;
|
|
51
|
+
}) | (ClaimBase & {
|
|
45
52
|
readonly role: "controller";
|
|
46
53
|
readonly selector: ArtifactSelector;
|
|
47
54
|
/** Extra payload patterns that belong in the same layer as this claim —
|
|
48
55
|
* `.gitignore`-style globs over the selected files, matched by the
|
|
49
56
|
* caller, which is the side that knows what was selected. */
|
|
50
57
|
readonly siblings: readonly string[];
|
|
58
|
+
/** The source `path` was built from (`local_path=`), when the candidate
|
|
59
|
+
* names one. The release path builds the entry point rather than reading
|
|
60
|
+
* a prebuilt file, so it needs the source — and re-deriving it by parsing
|
|
61
|
+
* `origin` would put PURL knowledge back into the consumer, which is
|
|
62
|
+
* exactly what this module exists to hold. */
|
|
63
|
+
readonly localPath?: string;
|
|
51
64
|
}) | (ClaimBase & {
|
|
52
65
|
readonly role: "assets";
|
|
53
66
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-file-claims.d.ts","sourceRoot":"","sources":["../src/module-file-claims.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"module-file-claims.d.ts","sourceRoot":"","sources":["../src/module-file-claims.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAGpG;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,UAAU,SAAS;IACjB;;;uEAGmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;6CACyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GACvB,CAAC,SAAS,GAAG;IACX,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC,GACF,CAAC,SAAS,GAAG;IACX,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;kEAE8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;mDAI+C;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC,GACF,CAAC,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,CAAC;AAsG9C;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,QAAQ,GAAE,wBAA4C,GACrD,eAAe,EAAE,CAiBnB"}
|
|
@@ -2,6 +2,7 @@ import { defaultCustomTags, defaultRegistry, walkCelExpressions, } from "@teloru
|
|
|
2
2
|
import { PackageURL } from "packageurl-js";
|
|
3
3
|
import { parseAllDocuments } from "yaml";
|
|
4
4
|
import { selectorFromQualifiers, selectorKey } from "./artifact-selector.js";
|
|
5
|
+
import { readLibraryCandidates } from "./module-library.js";
|
|
5
6
|
/** `pkg:telo/local/<format>?path=…` — the bundled-controller delivery mode.
|
|
6
7
|
* Anything else (`pkg:npm`, `pkg:cargo`) fetches from its own ecosystem and
|
|
7
8
|
* contributes no layer. */
|
|
@@ -40,6 +41,7 @@ function controllerClaims(json) {
|
|
|
40
41
|
const entry = parsed.qualifiers?.path;
|
|
41
42
|
if (typeof entry !== "string" || entry === "")
|
|
42
43
|
continue;
|
|
44
|
+
const localPath = parsed.qualifiers?.local_path;
|
|
43
45
|
claims.push({
|
|
44
46
|
role: "controller",
|
|
45
47
|
path: normalizeRelative(entry),
|
|
@@ -48,11 +50,29 @@ function controllerClaims(json) {
|
|
|
48
50
|
.split(",")
|
|
49
51
|
.map((p) => p.trim())
|
|
50
52
|
.filter((p) => p !== ""),
|
|
53
|
+
...(typeof localPath === "string" && localPath !== ""
|
|
54
|
+
? { localPath: normalizeRelative(localPath) }
|
|
55
|
+
: {}),
|
|
51
56
|
origin: candidate,
|
|
52
57
|
});
|
|
53
58
|
}
|
|
54
59
|
return claims;
|
|
55
60
|
}
|
|
61
|
+
/** The library entry points one document's `library:` block names. Unlike a
|
|
62
|
+
* controller entry — reached only when this module's own kinds instantiate —
|
|
63
|
+
* this one is what a *sibling* resolves a bare specifier to, which is why it
|
|
64
|
+
* gets its own layer rather than riding in the controller layer: a consumer
|
|
65
|
+
* must reach it without loading this module's controllers. */
|
|
66
|
+
function libraryClaims(json) {
|
|
67
|
+
return readLibraryCandidates(json).candidates.map((candidate) => ({
|
|
68
|
+
role: "library",
|
|
69
|
+
path: candidate.path,
|
|
70
|
+
selector: candidate.selector,
|
|
71
|
+
specifier: candidate.specifier,
|
|
72
|
+
...(candidate.localPath ? { localPath: candidate.localPath } : {}),
|
|
73
|
+
origin: candidate.origin,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
56
76
|
/** Claims contributed by tagged values, asked of the engine that owns each tag.
|
|
57
77
|
* The walk reaches every tagged scalar in the document, so an engine that
|
|
58
78
|
* embeds files is discovered wherever its tag was written.
|
|
@@ -77,7 +97,7 @@ function taggedClaims(json, registry) {
|
|
|
77
97
|
* their layers — dropping one would leave a platform's layer short a file it
|
|
78
98
|
* declared it needs. */
|
|
79
99
|
function claimKey(claim) {
|
|
80
|
-
const selector = claim.role === "controller" ? selectorKey(claim.selector) : "";
|
|
100
|
+
const selector = claim.role === "controller" || claim.role === "library" ? selectorKey(claim.selector) : "";
|
|
81
101
|
return `${claim.role}\0${selector}\0${claim.path}`;
|
|
82
102
|
}
|
|
83
103
|
/**
|
|
@@ -94,7 +114,11 @@ export function collectModuleFileClaims(manifestText, registry = defaultRegistry
|
|
|
94
114
|
const claims = [];
|
|
95
115
|
for (const doc of parseAllDocuments(manifestText, { customTags: defaultCustomTags() })) {
|
|
96
116
|
const json = doc.toJSON();
|
|
97
|
-
for (const claim of [
|
|
117
|
+
for (const claim of [
|
|
118
|
+
...libraryClaims(json),
|
|
119
|
+
...controllerClaims(json),
|
|
120
|
+
...taggedClaims(json, registry),
|
|
121
|
+
]) {
|
|
98
122
|
const key = claimKey(claim);
|
|
99
123
|
if (seen.has(key))
|
|
100
124
|
continue;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A module's **exported code** — the `exports.code:` block on a `Telo.Library`
|
|
3
|
+
* doc, which names the entry point a *sibling module's* controller bundle
|
|
4
|
+
* resolves this module's bare specifier to.
|
|
5
|
+
*
|
|
6
|
+
* ```yaml
|
|
7
|
+
* exports:
|
|
8
|
+
* kinds:
|
|
9
|
+
* - Store
|
|
10
|
+
* code:
|
|
11
|
+
* - specifier: "@telorun/kv-store"
|
|
12
|
+
* format: js
|
|
13
|
+
* path: ./nodejs/kv-store.mjs
|
|
14
|
+
* source: ./nodejs/src/index.ts
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ## Why it sits under `exports:`
|
|
18
|
+
*
|
|
19
|
+
* A library already declares what crosses its boundary — the kinds importers may
|
|
20
|
+
* name, the resource instances they may `!ref`. This is the same statement about
|
|
21
|
+
* its *code*, and it gates the same way: a specifier nobody declares resolves to
|
|
22
|
+
* nothing. Putting it beside them keeps one block for "reachable from outside"
|
|
23
|
+
* rather than a second top-level key whose name (`library:` on a `Telo.Library`)
|
|
24
|
+
* meant a different thing from the kind one line above it.
|
|
25
|
+
*
|
|
26
|
+
* ## Why it is not a package URL
|
|
27
|
+
*
|
|
28
|
+
* `controllers:` names a PURL because it must be able to say `pkg:npm/…` or
|
|
29
|
+
* `pkg:cargo/…` — an ecosystem fetch. This entry never fetches: it names a file
|
|
30
|
+
* the module already ships, so `pkg:telo/local/` would be three constant segments
|
|
31
|
+
* before the first real datum. What is left after removing them is exactly these
|
|
32
|
+
* fields, and as data they are visually editable, where a query string is one
|
|
33
|
+
* opaque text box.
|
|
34
|
+
*
|
|
35
|
+
* The **model** is unchanged: `format` plus the optional platform axes build the
|
|
36
|
+
* same `ArtifactSelector` a controller candidate does, so layer matching, platform
|
|
37
|
+
* fallthrough and lazy materialization are inherited whole.
|
|
38
|
+
*
|
|
39
|
+
* ## Why the specifier is declared here
|
|
40
|
+
*
|
|
41
|
+
* A bundle imports the bare specifier `@telorun/sql`; the consumer's manifest
|
|
42
|
+
* declares the dependency as `Sql: ../sql`. Something has to connect the two, and
|
|
43
|
+
* it is the *library* that says so, once, rather than each of its consumers:
|
|
44
|
+
*
|
|
45
|
+
* - the specifier is a property of the library — its name in a host language's
|
|
46
|
+
* ecosystem — not of the relationship, so N consumers cannot disagree about it
|
|
47
|
+
* and adding a consumer restates nothing;
|
|
48
|
+
* - it sits beside the format, which keeps runtime **derived, never declared**:
|
|
49
|
+
* the entry says `format: js`, and a Rust entry carries `specifier:
|
|
50
|
+
* telorun-sql` with no runtime-keyed map anywhere.
|
|
51
|
+
*
|
|
52
|
+
* **One specifier, one entry point.** Subpaths are deliberately not
|
|
53
|
+
* representable: reproducing npm's `exports` map inside the artifact would pull a
|
|
54
|
+
* package manager's resolution semantics into Telo, which is what the "only
|
|
55
|
+
* workspace modules are de-inlined" rule refuses on `kysely`'s behalf.
|
|
56
|
+
*
|
|
57
|
+
* `Telo.Application` has no `exports:` block at all — an application is a root
|
|
58
|
+
* with no importer, so nothing could resolve a specifier to it.
|
|
59
|
+
*
|
|
60
|
+
* Browser-safe: string work only. Whether the named file EXISTS is a separate
|
|
61
|
+
* question, asked by the Node-side caller that has a directory.
|
|
62
|
+
*/
|
|
63
|
+
import { type ArtifactSelector } from "./artifact-selector.js";
|
|
64
|
+
export interface LibraryCandidate {
|
|
65
|
+
/** The bare specifier a sibling's controller bundle imports this library by. */
|
|
66
|
+
readonly specifier: string;
|
|
67
|
+
/** Module-root-relative path of the built entry point. */
|
|
68
|
+
readonly path: string;
|
|
69
|
+
/** Module-root-relative TypeScript source it is built from (`source:`), when
|
|
70
|
+
* the entry names one. Present only while the module is a working copy; a
|
|
71
|
+
* published artifact ships no `src/`. */
|
|
72
|
+
readonly localPath?: string;
|
|
73
|
+
readonly selector: ArtifactSelector;
|
|
74
|
+
/** Where the entry was written, for diagnostics. */
|
|
75
|
+
readonly origin: string;
|
|
76
|
+
}
|
|
77
|
+
/** Why an `exports.code` entry could not be read. Returned rather than thrown so
|
|
78
|
+
* the analyzer can report every entry of a block, and so a reader on the load
|
|
79
|
+
* path can carry on with the entries that are well-formed. */
|
|
80
|
+
export interface LibraryCandidateProblem {
|
|
81
|
+
readonly origin: string;
|
|
82
|
+
readonly detail: string;
|
|
83
|
+
}
|
|
84
|
+
export interface LibraryCandidates {
|
|
85
|
+
readonly candidates: LibraryCandidate[];
|
|
86
|
+
readonly problems: LibraryCandidateProblem[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Read the `exports.code:` block off an owner document's JSON projection.
|
|
90
|
+
*
|
|
91
|
+
* Everything malformed is a problem rather than a silent skip: an entry that
|
|
92
|
+
* cannot be read names no entry point, so a consumer's bundle falls back to
|
|
93
|
+
* *inlining* the library — the duplicated module scope this whole mechanism
|
|
94
|
+
* exists to remove — and it does so on someone else's machine.
|
|
95
|
+
*/
|
|
96
|
+
export declare function readLibraryCandidates(ownerJson: unknown): LibraryCandidates;
|
|
97
|
+
//# sourceMappingURL=module-library.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-library.d.ts","sourceRoot":"","sources":["../src/module-library.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;AAKhC,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;8CAE0C;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;+DAE+D;AAC/D,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,EAAE,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,EAAE,CAAC;CAC9C;AAmBD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAkF3E"}
|