@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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolveContract } from "./invocation-contract.js";
|
|
2
2
|
import { checkSchemaCompatibility, navigateSchemaToExprPath, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
|
|
3
3
|
import { plainChainOf } from "@telorun/templating";
|
|
4
|
-
import { valueTypeOf } from "@telorun/sdk";
|
|
4
|
+
import { isLiveSlot, valueTypeOf } from "@telorun/sdk";
|
|
5
|
+
import { manifestFragmentOf } from "./manifest-schemas.js";
|
|
5
6
|
import { analyzerContractScope, containerOf, gatherPropertySchemas, missingRequired, resolveLocalRef, walkStepArray, } from "./analyzer.js";
|
|
6
7
|
/**
|
|
7
8
|
* Validate every step's `inputs:` against the invoked target's declared input
|
|
@@ -79,16 +80,27 @@ stepContext) {
|
|
|
79
80
|
// that silence is exactly where a stream of the wrong element used to
|
|
80
81
|
// flow. The comparison is covariant and gradual: an omitted argument is
|
|
81
82
|
// *any* in both directions, so only a definite conflict is reported.
|
|
82
|
-
|
|
83
|
+
// The roots a plain chain may name here, each paired with the schema it is
|
|
84
|
+
// navigated against. `steps.` is the step map (analyzer state, supplied by
|
|
85
|
+
// the caller). `inputs.` is the ENCLOSING kind's own declared inputType,
|
|
86
|
+
// which is how a value produced OUTSIDE this resource reaches a step at
|
|
87
|
+
// all: an HTTP route maps `request.body` into its handler's inputs, and the
|
|
88
|
+
// handler forwards `inputs.body` onward — the shape a live value most often
|
|
89
|
+
// arrives in, and the one covering only `steps.` missed entirely. A root
|
|
90
|
+
// this cannot resolve contributes nothing rather than guessing at a schema.
|
|
91
|
+
const roots = [];
|
|
92
|
+
if (stepContext)
|
|
93
|
+
roots.push(["steps.", stepContext]);
|
|
94
|
+
const ownContract = resolveContract("inputType", manifest, contractScope.resolveIn(manifest.kind, readingModule), contractScope);
|
|
95
|
+
if (ownContract)
|
|
96
|
+
roots.push(["inputs.", ownContract.schema]);
|
|
97
|
+
if (roots.length > 0) {
|
|
83
98
|
for (const [inputName, inputValue] of Object.entries(values)) {
|
|
84
99
|
const chain = plainChainOf(inputValue);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// navigated: `inputs.` and a named binding resolve elsewhere, and
|
|
88
|
-
// guessing at a root this does not hold would compare the wrong schema.
|
|
89
|
-
if (!chain?.startsWith("steps."))
|
|
100
|
+
const root = chain ? roots.find(([prefix]) => chain.startsWith(prefix)) : undefined;
|
|
101
|
+
if (!chain || !root)
|
|
90
102
|
continue;
|
|
91
|
-
const produced = navigateSchemaToExprPath(
|
|
103
|
+
const produced = navigateSchemaToExprPath(root[1], chain.slice(root[0].length));
|
|
92
104
|
const slotSchema = contract.schema.properties?.[inputName];
|
|
93
105
|
if (!produced || !slotSchema)
|
|
94
106
|
continue;
|
|
@@ -99,6 +111,27 @@ stepContext) {
|
|
|
99
111
|
// broad new Error-severity check hidden behind an argument-specific
|
|
100
112
|
// name. Both sides must declare a value type for the question to be
|
|
101
113
|
// about arguments at all.
|
|
114
|
+
// A LIVE value is consumed by reading, so it exists exactly once —
|
|
115
|
+
// that is what `live` says in the vocabulary, and re-attempting a
|
|
116
|
+
// dispatch that already read it re-sends nothing. Reported here rather
|
|
117
|
+
// than through a slot-specific annotation because both facts are
|
|
118
|
+
// already declared: the value's liveness by its value type, and the
|
|
119
|
+
// re-attempt by the retry policy. No kind is named.
|
|
120
|
+
if (isLiveSlot(produced)) {
|
|
121
|
+
const retry = declaredRetry(step, stepItemSchema, invokedManifest, invokedDef);
|
|
122
|
+
if (retry !== undefined) {
|
|
123
|
+
out.push({
|
|
124
|
+
path: `${stepPath}.${inputsField}.${inputName}`,
|
|
125
|
+
targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
|
|
126
|
+
message: `'${inputName}' is a live value, which is consumed by reading and so exists ` +
|
|
127
|
+
`once — but ${retry} re-attempts the dispatch, and a re-attempt would pass ` +
|
|
128
|
+
`nothing. Collect it to a value first, or chunk the work so each attempt ` +
|
|
129
|
+
`carries its own replayable piece.`,
|
|
130
|
+
code: "LIVE_VALUE_RETRIED",
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
102
135
|
if (!valueTypeOf(produced) || !valueTypeOf(slotSchema))
|
|
103
136
|
continue;
|
|
104
137
|
const { compatible, issues } = checkSchemaCompatibility(produced, slotSchema, (ref) => defs.schemaForId(ref));
|
|
@@ -130,3 +163,61 @@ stepContext) {
|
|
|
130
163
|
}
|
|
131
164
|
return out;
|
|
132
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Where a re-attempt is declared for this dispatch, described for a diagnostic,
|
|
168
|
+
* or undefined when none is.
|
|
169
|
+
*
|
|
170
|
+
* A field declares one when its schema was expanded from a shared retry fragment
|
|
171
|
+
* — the shape the author pointed at, rather than a marker they had to remember to
|
|
172
|
+
* write beside it. Which fragment also says WHERE the budget is, so the two
|
|
173
|
+
* spellings a kind may carry (a policy object, or the deprecated bare count) need
|
|
174
|
+
* no guessing between them and no rule about which one wins.
|
|
175
|
+
*
|
|
176
|
+
* Two sites are consulted because there are two real ones: the STEP's own policy
|
|
177
|
+
* — `retry` on the kernel-owned dispatch site — and the TARGET's, a field on an
|
|
178
|
+
* arbitrary kind, because `Http.Request` re-attempts inside its own `invoke()`
|
|
179
|
+
* where only it can tell a 429 from a 500. A live value is equally doomed by
|
|
180
|
+
* either. EVERY retry-bearing field at a site is checked, not the first, since
|
|
181
|
+
* `Http.Request` carries both spellings and property order must not decide which
|
|
182
|
+
* is seen.
|
|
183
|
+
*
|
|
184
|
+
* Only a STATICALLY KNOWN non-zero budget counts. An `attempts` written as CEL
|
|
185
|
+
* says nothing here, and guessing would report a conflict against a manifest that
|
|
186
|
+
* may never retry — the same posture the `use` case-map selector takes.
|
|
187
|
+
*/
|
|
188
|
+
function declaredRetry(step, stepItemSchema, invokedManifest, invokedDef) {
|
|
189
|
+
for (const [field, budget] of retryFields(stepItemSchema)) {
|
|
190
|
+
if (budget(step?.[field]) > 0)
|
|
191
|
+
return `the step's \`${field}\``;
|
|
192
|
+
}
|
|
193
|
+
for (const [field, budget] of retryFields(invokedDef?.schema)) {
|
|
194
|
+
if (budget(invokedManifest?.[field]) > 0)
|
|
195
|
+
return `the target's \`${field}\``;
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
/** How each shared retry fragment carries its budget. Keyed on fragment name —
|
|
200
|
+
* the analyzer's own built-ins, never a module's kind — so a kind that adopts a
|
|
201
|
+
* shape is covered without naming it here. */
|
|
202
|
+
const RETRY_BUDGET = {
|
|
203
|
+
RetryPolicy: (value) => {
|
|
204
|
+
if (!value || typeof value !== "object")
|
|
205
|
+
return 0;
|
|
206
|
+
const attempts = value.attempts;
|
|
207
|
+
return typeof attempts === "number" ? attempts : 0;
|
|
208
|
+
},
|
|
209
|
+
RetryAttempts: (value) => (typeof value === "number" ? value : 0),
|
|
210
|
+
};
|
|
211
|
+
/** Every property of `schema` whose shape came from a retry fragment, paired with
|
|
212
|
+
* the reader for that fragment's budget. */
|
|
213
|
+
function retryFields(schema) {
|
|
214
|
+
if (!schema)
|
|
215
|
+
return [];
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const [key, sub] of gatherPropertySchemas(schema)) {
|
|
218
|
+
const budget = RETRY_BUDGET[manifestFragmentOf(sub) ?? ""];
|
|
219
|
+
if (budget)
|
|
220
|
+
out.push([key, budget]);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"jsonpath-plus": "^10.3.0",
|
|
44
44
|
"packageurl-js": "^2.0.1",
|
|
45
45
|
"yaml": "^2.8.3",
|
|
46
|
-
"@telorun/templating": "0.
|
|
46
|
+
"@telorun/templating": "0.16.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^20.0.0",
|
|
50
50
|
"typescript": "^5.0.0",
|
|
51
51
|
"vitest": "^2.1.8",
|
|
52
|
-
"@telorun/sdk": "0.
|
|
52
|
+
"@telorun/sdk": "0.75.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@telorun/sdk": "*"
|
package/src/analyzer.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
|
51
51
|
import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
|
|
52
52
|
import { resolveSchemaRefKinds, type RefConstraintIssue } from "./resolve-schema-ref-kinds.js";
|
|
53
53
|
import { runZoneAnalysis, type ZoneExportCache } from "./resolve-zone-requirements.js";
|
|
54
|
+
import { MANIFEST_SCHEMA_URI, ManifestRootSchema } from "./manifest-schemas.js";
|
|
54
55
|
import { validateZoneSlotDeclarations, type ZoneSlotIssue } from "./validate-zone-slots.js";
|
|
55
56
|
import {
|
|
56
57
|
validateDynamicSelectors,
|
|
@@ -316,23 +317,50 @@ export function resolveLocalRef(
|
|
|
316
317
|
const resolved = root.$defs?.[defName];
|
|
317
318
|
if (resolved && typeof resolved === "object") return resolved as Record<string, any>;
|
|
318
319
|
}
|
|
320
|
+
// A kernel-owned structural fragment (`telo://manifest#/$defs/InvokeStep`).
|
|
321
|
+
// Resolved HERE rather than by each walker: this is the one chokepoint every
|
|
322
|
+
// structural walk already goes through — the step-array walks, the call graph,
|
|
323
|
+
// the zone projection, the eval-path collector — so a composer that points at a
|
|
324
|
+
// shared shape stays legible to all of them at once. Nothing is inlined into
|
|
325
|
+
// the stored schema, which keeps validator-cache identity stable and matches
|
|
326
|
+
// what `resolveSchemaTypeRefs` does for a named user type.
|
|
327
|
+
if (typeof ref === "string" && ref.startsWith(BUILTIN_FRAGMENT_PREFIX)) {
|
|
328
|
+
const defName = ref.slice(BUILTIN_FRAGMENT_PREFIX.length);
|
|
329
|
+
const resolved = (ManifestRootSchema.$defs as Record<string, unknown>)[defName];
|
|
330
|
+
if (resolved && typeof resolved === "object") return resolved as Record<string, any>;
|
|
331
|
+
}
|
|
319
332
|
return schema;
|
|
320
333
|
}
|
|
321
334
|
|
|
335
|
+
const BUILTIN_FRAGMENT_PREFIX = `${MANIFEST_SCHEMA_URI}#/$defs/`;
|
|
336
|
+
|
|
322
337
|
/** Gather property schemas from a (possibly variant-bearing) object schema:
|
|
323
|
-
* top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch.
|
|
324
|
-
|
|
338
|
+
* top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch.
|
|
339
|
+
*
|
|
340
|
+
* Each branch is resolved through {@link resolveLocalRef} first, so a branch
|
|
341
|
+
* that points at a shared shape — a `oneOf` arm that IS the kernel's dispatch
|
|
342
|
+
* site — contributes its properties like an inline one. Without that, pointing a
|
|
343
|
+
* composer at a shared shape would silently empty every role-driven lookup that
|
|
344
|
+
* reads this (the inputs slot, the retry policy, the eval paths), which is a
|
|
345
|
+
* failure with no diagnostic attached to it. */
|
|
346
|
+
export function gatherPropertySchemas(
|
|
347
|
+
schema: Record<string, any>,
|
|
348
|
+
root?: Record<string, any>,
|
|
349
|
+
): Array<[string, Record<string, any>]> {
|
|
325
350
|
const out: Array<[string, Record<string, any>]> = [];
|
|
326
|
-
|
|
327
|
-
|
|
351
|
+
const base = resolveLocalRef(schema, root ?? schema) ?? schema;
|
|
352
|
+
if (base.properties && typeof base.properties === "object") {
|
|
353
|
+
for (const [k, v] of Object.entries(base.properties as Record<string, any>)) {
|
|
328
354
|
out.push([k, v as Record<string, any>]);
|
|
329
355
|
}
|
|
330
356
|
}
|
|
331
357
|
for (const variantKey of ["oneOf", "anyOf", "allOf"] as const) {
|
|
332
|
-
const arr =
|
|
358
|
+
const arr = base[variantKey];
|
|
333
359
|
if (!Array.isArray(arr)) continue;
|
|
334
|
-
for (const
|
|
335
|
-
if (
|
|
360
|
+
for (const raw of arr) {
|
|
361
|
+
if (!raw || typeof raw !== "object") continue;
|
|
362
|
+
const variant = resolveLocalRef(raw as Record<string, any>, root ?? schema) ?? raw;
|
|
363
|
+
if (variant.properties) {
|
|
336
364
|
for (const [k, v] of Object.entries(variant.properties as Record<string, any>)) {
|
|
337
365
|
out.push([k, v as Record<string, any>]);
|
|
338
366
|
}
|
|
@@ -2012,9 +2040,12 @@ export class StaticAnalyzer {
|
|
|
2012
2040
|
severity: DiagnosticSeverity.Error,
|
|
2013
2041
|
code: issue.code ?? "CONTRACT_INPUTS_MISMATCH",
|
|
2014
2042
|
source: SOURCE,
|
|
2015
|
-
message:
|
|
2016
|
-
|
|
2017
|
-
|
|
2043
|
+
message:
|
|
2044
|
+
issue.code === "LIVE_VALUE_RETRIED"
|
|
2045
|
+
? `${m.kind}/${stepName}: at '${issue.path}', ${issue.message}`
|
|
2046
|
+
: issue.code
|
|
2047
|
+
? `${m.kind}/${stepName}: inputs at '${issue.path}' flow into ${issue.targetLabel} with disagreeing type arguments: ${issue.message}`
|
|
2048
|
+
: `${m.kind}/${stepName}: inputs at '${issue.path}' do not satisfy ${issue.targetLabel}'s declared inputType: ${issue.message}`,
|
|
2018
2049
|
data: {
|
|
2019
2050
|
resource: { kind: m.kind, name: stepName ?? "" },
|
|
2020
2051
|
filePath: stepFile,
|
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import {
|
|
32
|
+
LAYER_ROLES,
|
|
32
33
|
isLayerRole,
|
|
33
34
|
normalizeSelector,
|
|
35
|
+
roleCarriesSelector,
|
|
34
36
|
selectorKey,
|
|
35
37
|
selectorMatches,
|
|
36
38
|
type ArtifactSelector,
|
|
@@ -46,7 +48,7 @@ const CONTENT_DIGEST = /^sha256-[A-Za-z0-9_-]{43}$/;
|
|
|
46
48
|
|
|
47
49
|
export interface ArtifactLayer {
|
|
48
50
|
role: LayerRole;
|
|
49
|
-
/** Present on `controller`
|
|
51
|
+
/** Present on the code-bearing roles (`controller`, `library`) only. */
|
|
50
52
|
selector?: ArtifactSelector;
|
|
51
53
|
/** OCI blob digest — addresses the layer and verifies the transfer. */
|
|
52
54
|
blob: string;
|
|
@@ -97,25 +99,35 @@ export function parseLayerIndex(value: unknown, describe = "layers"): ArtifactLa
|
|
|
97
99
|
throw new LayerIndexError(`${where}: expected an object.`);
|
|
98
100
|
}
|
|
99
101
|
const entry = raw as Record<string, unknown>;
|
|
100
|
-
if (
|
|
102
|
+
if (typeof entry.role !== "string" || entry.role === "") {
|
|
101
103
|
throw new LayerIndexError(
|
|
102
|
-
`${where}: role must be one of
|
|
103
|
-
|
|
104
|
+
`${where}: role is required and must be one of ${LAYER_ROLES.map((r) => `'${r}'`).join(", ")}; ` +
|
|
105
|
+
`got ${entry.role === undefined ? "nothing" : `'${String(entry.role)}'`}.`,
|
|
104
106
|
);
|
|
105
107
|
}
|
|
108
|
+
// A role this runtime does not know is SKIPPED, never rejected. Roles are
|
|
109
|
+
// added over time, and a runtime that cannot name one cannot need it — while
|
|
110
|
+
// throwing would make the whole manifest unreadable, so a module gaining a
|
|
111
|
+
// layer for a newer runtime would stop loading on an older one entirely
|
|
112
|
+
// rather than merely lacking that layer. The error stays for a structurally
|
|
113
|
+
// invalid entry, which is a malformed index rather than a newer one.
|
|
114
|
+
if (!isLayerRole(entry.role)) return;
|
|
106
115
|
const role = entry.role;
|
|
107
116
|
|
|
108
117
|
let selector: ArtifactSelector | undefined;
|
|
109
|
-
if (role
|
|
118
|
+
if (roleCarriesSelector(role)) {
|
|
110
119
|
if (entry.selector === undefined) {
|
|
111
|
-
throw new LayerIndexError(`${where}: a
|
|
120
|
+
throw new LayerIndexError(`${where}: a ${role} layer must declare a selector.`);
|
|
112
121
|
}
|
|
113
122
|
selector = normalizeSelector(entry.selector, where);
|
|
114
|
-
|
|
123
|
+
// Scoped by role: a module's `js` controller layer and its `js` library
|
|
124
|
+
// layer are different layers with the same selector, and only a collision
|
|
125
|
+
// *within* one role means two layers claim one address.
|
|
126
|
+
const key = `${role}\0${selectorKey(selector)}`;
|
|
115
127
|
if (seenSelectors.has(key)) {
|
|
116
128
|
throw new LayerIndexError(
|
|
117
|
-
`${where}: a second
|
|
118
|
-
`Each selector addresses exactly one layer.`,
|
|
129
|
+
`${where}: a second ${role} layer claims the selector ${selectorKey(selector)}. ` +
|
|
130
|
+
`Each selector addresses exactly one layer of a role.`,
|
|
119
131
|
);
|
|
120
132
|
}
|
|
121
133
|
seenSelectors.add(key);
|
|
@@ -145,18 +157,39 @@ export function parseLayerIndex(value: unknown, describe = "layers"): ArtifactLa
|
|
|
145
157
|
/** The singleton layer for a role, or undefined when the artifact has none. */
|
|
146
158
|
export function singletonLayer(
|
|
147
159
|
layers: readonly ArtifactLayer[],
|
|
148
|
-
role: Exclude<LayerRole, "controller">,
|
|
160
|
+
role: Exclude<LayerRole, "controller" | "library">,
|
|
149
161
|
): ArtifactLayer | undefined {
|
|
150
162
|
return layers.find((l) => l.role === role);
|
|
151
163
|
}
|
|
152
164
|
|
|
153
|
-
/**
|
|
154
|
-
*
|
|
155
|
-
|
|
165
|
+
/** The layer of one code role carrying exactly `selector`, or undefined.
|
|
166
|
+
*
|
|
167
|
+
* By exact key rather than by re-matching a host: the candidate being resolved
|
|
168
|
+
* already *is* one selector, and it is by construction the key of the layer
|
|
169
|
+
* that carries it. */
|
|
170
|
+
export function codeLayerFor(
|
|
171
|
+
layers: readonly ArtifactLayer[],
|
|
172
|
+
role: Extract<LayerRole, "controller" | "library">,
|
|
173
|
+
selector: ArtifactSelector,
|
|
174
|
+
): ArtifactLayer | undefined {
|
|
175
|
+
const key = selectorKey(selector);
|
|
176
|
+
return layers.find(
|
|
177
|
+
(l) => l.role === role && l.selector !== undefined && selectorKey(l.selector) === key,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Every code layer — controller and library alike — matching `target`, in
|
|
182
|
+
* declaration order. Used by `telo install` to warm a cache for one platform:
|
|
183
|
+
* a library layer is as much a prerequisite of an offline run as the controller
|
|
184
|
+
* layer that imports it. */
|
|
185
|
+
export function matchCodeLayers(
|
|
156
186
|
layers: readonly ArtifactLayer[],
|
|
157
187
|
target: PlatformTarget,
|
|
158
188
|
): ArtifactLayer[] {
|
|
159
189
|
return layers.filter(
|
|
160
|
-
(l) =>
|
|
190
|
+
(l) =>
|
|
191
|
+
(l.role === "controller" || l.role === "library") &&
|
|
192
|
+
l.selector !== undefined &&
|
|
193
|
+
selectorMatches(l.selector, target),
|
|
161
194
|
);
|
|
162
195
|
}
|
package/src/artifact-selector.ts
CHANGED
|
@@ -25,16 +25,27 @@
|
|
|
25
25
|
* published into OCI descriptors.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
/** The role a layer plays in a module artifact. `controller`
|
|
29
|
-
* selector; `assets` and `common` are singletons and carry none. */
|
|
30
|
-
export type LayerRole = "controller" | "assets" | "common";
|
|
28
|
+
/** The role a layer plays in a module artifact. `controller` and `library` layers
|
|
29
|
+
* carry a selector; `assets` and `common` are singletons and carry none. */
|
|
30
|
+
export type LayerRole = "controller" | "library" | "assets" | "common";
|
|
31
31
|
|
|
32
|
-
export const LAYER_ROLES: readonly LayerRole[] = ["controller", "assets", "common"];
|
|
32
|
+
export const LAYER_ROLES: readonly LayerRole[] = ["controller", "library", "assets", "common"];
|
|
33
33
|
|
|
34
34
|
export function isLayerRole(value: unknown): value is LayerRole {
|
|
35
35
|
return typeof value === "string" && (LAYER_ROLES as readonly string[]).includes(value);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** The roles that hold executable code, and are therefore per format rather than
|
|
39
|
+
* singletons. A `library` layer is per selector for the same reason a
|
|
40
|
+
* `controller` layer is: a module's JS entry point and its future Rust one are
|
|
41
|
+
* different files, and a consumer resolves the one its own runtime can import.
|
|
42
|
+
* A singleton would be wrong the moment a second runtime ships. */
|
|
43
|
+
export const CODE_LAYER_ROLES: readonly LayerRole[] = ["controller", "library"];
|
|
44
|
+
|
|
45
|
+
export function roleCarriesSelector(role: LayerRole): boolean {
|
|
46
|
+
return (CODE_LAYER_ROLES as readonly string[]).includes(role);
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
/** The platform axes, in canonical order. Not a closed vocabulary of *values* —
|
|
39
50
|
* new architectures appear without a Telo release — only of axis names. */
|
|
40
51
|
export const PLATFORM_AXES = ["os", "arch", "libc"] as const;
|
package/src/builtins.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { manifestFragment } from "./manifest-schemas.js";
|
|
1
2
|
import type { ResourceDefinition } from "@telorun/sdk";
|
|
2
3
|
import { OBSERVED_STATE_SCHEMA } from "./validate-observed-state.js";
|
|
3
4
|
|
|
@@ -32,6 +33,33 @@ const ASSETS_FILES_SCHEMA = {
|
|
|
32
33
|
items: { type: "string" },
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
/** `exports.code` — the entry point a sibling module's controller bundle resolves
|
|
37
|
+
* this library's bare specifier to, one per format.
|
|
38
|
+
*
|
|
39
|
+
* Data rather than a package URL: `controllers:` needs a PURL because it can name
|
|
40
|
+
* an ecosystem fetch (`pkg:npm`, `pkg:cargo`), while this always names a file the
|
|
41
|
+
* module already ships, so the type/namespace segments would be constant noise —
|
|
42
|
+
* and a query string is one opaque box to the visual editor. `format` plus the
|
|
43
|
+
* platform axes build the same `ArtifactSelector` a controller candidate does.
|
|
44
|
+
* Semantics and diagnostics live in `analyzer/nodejs/src/module-library.ts`. */
|
|
45
|
+
const LIBRARY_CANDIDATES_SCHEMA = {
|
|
46
|
+
type: "array",
|
|
47
|
+
items: {
|
|
48
|
+
type: "object",
|
|
49
|
+
required: ["specifier", "format", "path"],
|
|
50
|
+
properties: {
|
|
51
|
+
specifier: { type: "string" },
|
|
52
|
+
format: { type: "string" },
|
|
53
|
+
path: { type: "string" },
|
|
54
|
+
source: { type: "string" },
|
|
55
|
+
os: { type: "string" },
|
|
56
|
+
arch: { type: "string" },
|
|
57
|
+
libc: { type: "string" },
|
|
58
|
+
},
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
35
63
|
/** The published layer index, written by `telo publish` (never hand-authored).
|
|
36
64
|
* One entry per layer except the manifest layer, which cannot list its own hash
|
|
37
65
|
* inside itself and is pinned by the importer's `#sha256-...` instead. Shape and
|
|
@@ -43,7 +71,7 @@ const LAYER_INDEX_SCHEMA = {
|
|
|
43
71
|
type: "object",
|
|
44
72
|
required: ["role", "blob", "integrity"],
|
|
45
73
|
properties: {
|
|
46
|
-
role: { type: "string", enum: ["controller", "assets", "common"] },
|
|
74
|
+
role: { type: "string", enum: ["controller", "library", "assets", "common"] },
|
|
47
75
|
selector: {
|
|
48
76
|
type: "object",
|
|
49
77
|
required: ["format"],
|
|
@@ -620,48 +648,21 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
620
648
|
},
|
|
621
649
|
additionalProperties: false,
|
|
622
650
|
},
|
|
623
|
-
// Inline flat invoke step
|
|
624
|
-
// with
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
"x-telo-topology-role": "invoke",
|
|
639
|
-
"x-telo-ref": {
|
|
640
|
-
kind: "Telo.Executable",
|
|
641
|
-
use: "call",
|
|
642
|
-
inputs: "/inputs",
|
|
643
|
-
},
|
|
644
|
-
type: "object",
|
|
645
|
-
required: ["kind", "name"],
|
|
646
|
-
properties: {
|
|
647
|
-
kind: { type: "string" },
|
|
648
|
-
name: { type: "string" },
|
|
649
|
-
},
|
|
650
|
-
additionalProperties: true,
|
|
651
|
-
},
|
|
652
|
-
inputs: {
|
|
653
|
-
// Same annotation Run.Sequence steps carry: it is what makes
|
|
654
|
-
// a boot target's inputs visible to the call-site contract
|
|
655
|
-
// check and to the wiring rule. Without it the kernel would
|
|
656
|
-
// validate these at dispatch and nothing before it.
|
|
657
|
-
"x-telo-topology-role": "inputs",
|
|
658
|
-
type: "object",
|
|
659
|
-
additionalProperties: true,
|
|
660
|
-
},
|
|
661
|
-
when: { type: "string" },
|
|
662
|
-
},
|
|
663
|
-
additionalProperties: false,
|
|
664
|
-
},
|
|
651
|
+
// Inline flat invoke step, discriminated by the `invoke` key —
|
|
652
|
+
// THE dispatch site, shared with every `Run` step array rather
|
|
653
|
+
// than restated here. Restating it is what made `retry:` a schema
|
|
654
|
+
// error at boot while working one line away in a sequence: not a
|
|
655
|
+
// decision about boot, just a copy that never grew the field.
|
|
656
|
+
// Control flow (if/while/switch/try) is still not available here —
|
|
657
|
+
// reach for Run.Sequence.
|
|
658
|
+
// An expanded, stamped COPY. `builtins.ts` is not a manifest, so it
|
|
659
|
+
// never passes through the loader's expansion — embedding the
|
|
660
|
+
// fragment object itself would leave the nested `retry` as an
|
|
661
|
+
// unresolved `$ref` with no `x-telo-fragment` stamp, which is
|
|
662
|
+
// exactly what made LIVE_VALUE_RETRIED silently skip every boot
|
|
663
|
+
// target. A copy, because this is a module-level singleton and
|
|
664
|
+
// `resolveSchemaRefKinds` rewrites the `x-telo-ref` inside it.
|
|
665
|
+
manifestFragment("InvokeStep"),
|
|
665
666
|
],
|
|
666
667
|
},
|
|
667
668
|
},
|
|
@@ -872,6 +873,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
872
873
|
type: "array",
|
|
873
874
|
items: { type: "string", not: { enum: ["variables", "secrets"] } },
|
|
874
875
|
},
|
|
876
|
+
code: LIBRARY_CANDIDATES_SCHEMA,
|
|
875
877
|
},
|
|
876
878
|
additionalProperties: true,
|
|
877
879
|
},
|
package/src/cel-environment.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Environment } from "@marcbachmann/cel-js";
|
|
2
2
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
3
|
+
import { authoredModuleMetadata, moduleMetadataSchema } from "./module-metadata-scope.js";
|
|
3
4
|
import { jsonSchemaToCelType, VALUE_BRAND_BASE } from "./schema-compat.js";
|
|
4
5
|
|
|
5
6
|
/** Transport protocol on a `ports` entry → the nominal CEL brand its resolved
|
|
@@ -89,6 +90,32 @@ export function buildTypedCelEnvironment(
|
|
|
89
90
|
env.registerVariable("secrets", "map");
|
|
90
91
|
env.registerVariable("resources", "map");
|
|
91
92
|
|
|
93
|
+
// `module` — the declaring module's own `metadata`, so a manifest reads its
|
|
94
|
+
// version instead of restating it. A resource forwarded from an imported
|
|
95
|
+
// library reads THAT library's metadata, stamped as
|
|
96
|
+
// `metadata.moduleGlobals.module`.
|
|
97
|
+
//
|
|
98
|
+
// Falls back to an OPEN map, never to `manifest.metadata`: for a resource
|
|
99
|
+
// doc that is the RESOURCE's metadata (`{name: <resource name>}`), and
|
|
100
|
+
// closing `module` over it would turn a `module.version` that resolves
|
|
101
|
+
// perfectly well at runtime into a hard error the author cannot act on. A
|
|
102
|
+
// static check that is wrong in the rejecting direction is the worse
|
|
103
|
+
// polarity.
|
|
104
|
+
const moduleSchema = moduleMetadataSchema(
|
|
105
|
+
((manifest.metadata as Record<string, any> | undefined)?.moduleGlobals?.module as
|
|
106
|
+
| Record<string, unknown>
|
|
107
|
+
| undefined) ?? (rootModuleManifest?.metadata as Record<string, unknown> | undefined),
|
|
108
|
+
);
|
|
109
|
+
if (moduleSchema) {
|
|
110
|
+
const schema: Record<string, string> = {};
|
|
111
|
+
for (const [key, property] of Object.entries(moduleSchema.properties as Record<string, any>)) {
|
|
112
|
+
schema[key] = jsonSchemaToCelType(property);
|
|
113
|
+
}
|
|
114
|
+
(env as any).registerVariable({ name: "module", schema });
|
|
115
|
+
} else {
|
|
116
|
+
env.registerVariable("module", "map");
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
if (extraContextSchema?.properties) {
|
|
93
120
|
for (const [name, propSchema] of Object.entries(
|
|
94
121
|
extraContextSchema.properties as Record<string, any>,
|
|
@@ -159,5 +186,15 @@ export function buildImportInputCelEnvironment(
|
|
|
159
186
|
for (const name of ["resources", "ports"]) {
|
|
160
187
|
(env as any).registerVariable({ name, schema: {} });
|
|
161
188
|
}
|
|
189
|
+
// `module` IS part of it: the importer's own identity is config, and passing
|
|
190
|
+
// its version down to a child is the case the binding exists for.
|
|
191
|
+
const metadata = authoredModuleMetadata(mod?.metadata as Record<string, unknown> | undefined);
|
|
192
|
+
if (Object.keys(metadata).length > 0) {
|
|
193
|
+
const schema: Record<string, string> = {};
|
|
194
|
+
for (const key of Object.keys(metadata)) schema[key] = "dyn";
|
|
195
|
+
(env as any).registerVariable({ name: "module", schema });
|
|
196
|
+
} else {
|
|
197
|
+
env.registerVariable("module", "map");
|
|
198
|
+
}
|
|
162
199
|
return env;
|
|
163
200
|
}
|
|
@@ -100,6 +100,10 @@ export interface ModuleGlobals {
|
|
|
100
100
|
variables?: Record<string, unknown>;
|
|
101
101
|
secrets?: Record<string, unknown>;
|
|
102
102
|
ports?: Record<string, unknown>;
|
|
103
|
+
/** The declaring module's own `metadata` — what `module.<field>` reads. Its
|
|
104
|
+
* values, not a schema, because the fields are literals in the manifest and
|
|
105
|
+
* the module a resource belongs to is fixed at analysis time. */
|
|
106
|
+
module?: Record<string, unknown>;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
/** Read the schema-map blocks off a module doc, or `undefined` when it declares
|
|
@@ -120,6 +124,14 @@ function readModuleGlobals(libDoc: ResourceManifest | undefined): ModuleGlobals
|
|
|
120
124
|
const block = doc[key];
|
|
121
125
|
if (block && typeof block === "object" && !Array.isArray(block)) globals[key] = block;
|
|
122
126
|
}
|
|
127
|
+
// `module` is the declaring module's own identity, so it travels with a
|
|
128
|
+
// forwarded manifest for the same reason the config blocks do: a library's
|
|
129
|
+
// `module.version` is the LIBRARY's, and typing it from the consumer's entry
|
|
130
|
+
// doc would report the app's version to the library that asked.
|
|
131
|
+
const metadata = doc.metadata;
|
|
132
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
133
|
+
globals.module = metadata as Record<string, unknown>;
|
|
134
|
+
}
|
|
123
135
|
return Object.keys(globals).length > 0 ? globals : undefined;
|
|
124
136
|
}
|
|
125
137
|
|
package/src/index.ts
CHANGED
|
@@ -222,12 +222,14 @@ export {
|
|
|
222
222
|
} from "./sources/manifest-cache.js";
|
|
223
223
|
export type { ManifestCacheCoords } from "./sources/manifest-cache.js";
|
|
224
224
|
export {
|
|
225
|
+
CODE_LAYER_ROLES,
|
|
225
226
|
LAYER_ROLES,
|
|
226
227
|
PLATFORM_AXES,
|
|
227
228
|
ArtifactSelectorError,
|
|
228
229
|
describeSelector,
|
|
229
230
|
isLayerRole,
|
|
230
231
|
normalizeSelector,
|
|
232
|
+
roleCarriesSelector,
|
|
231
233
|
selectorFromQualifiers,
|
|
232
234
|
selectorKey,
|
|
233
235
|
selectorMatches,
|
|
@@ -240,9 +242,16 @@ export type {
|
|
|
240
242
|
} from "./artifact-selector.js";
|
|
241
243
|
export { collectModuleFileClaims } from "./module-file-claims.js";
|
|
242
244
|
export type { ModuleFileClaim } from "./module-file-claims.js";
|
|
245
|
+
export { readLibraryCandidates } from "./module-library.js";
|
|
246
|
+
export type {
|
|
247
|
+
LibraryCandidate,
|
|
248
|
+
LibraryCandidateProblem,
|
|
249
|
+
LibraryCandidates,
|
|
250
|
+
} from "./module-library.js";
|
|
243
251
|
export {
|
|
244
252
|
LayerIndexError,
|
|
245
|
-
|
|
253
|
+
codeLayerFor,
|
|
254
|
+
matchCodeLayers,
|
|
246
255
|
parseLayerIndex,
|
|
247
256
|
singletonLayer,
|
|
248
257
|
} from "./artifact-layer-index.js";
|
|
@@ -270,4 +279,12 @@ export type {
|
|
|
270
279
|
PositionIndex,
|
|
271
280
|
Range
|
|
272
281
|
} from "./types.js";
|
|
273
|
-
|
|
282
|
+
export * from "./manifest-schemas.js";
|
|
283
|
+
// The release model — module identity, fragments, the ledger, the edge graph and
|
|
284
|
+
// version planning. Pure data in, plan out, so the editor and the CLI release
|
|
285
|
+
// from one model.
|
|
286
|
+
export {
|
|
287
|
+
DERIVED_METADATA_FIELDS,
|
|
288
|
+
authoredModuleMetadata,
|
|
289
|
+
} from "./module-metadata-scope.js";
|
|
290
|
+
export * from "./release/index.js";
|