@telorun/analyzer 0.59.0 → 0.60.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/builtins.d.ts.map +1 -1
- package/dist/builtins.js +16 -42
- 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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- 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 +6 -0
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +4 -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-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/builtins.ts +16 -42
- package/src/cel-environment.ts +37 -0
- package/src/flatten-for-analyzer.ts +12 -0
- package/src/index.ts +9 -1
- package/src/kernel-globals.ts +23 -1
- package/src/manifest-schemas.ts +408 -0
- package/src/module-file-claims.ts +10 -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-step-inputs.ts +109 -9
|
@@ -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
|
+
}
|
package/dist/schema-compat.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import AjvModule from "ajv";
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
|
-
import { isRefSentinel, isTaggedSentinel,
|
|
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).
|
|
@@ -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;
|
|
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"}
|
|
@@ -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.60.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,
|
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
|
|
|
@@ -620,48 +621,21 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
620
621
|
},
|
|
621
622
|
additionalProperties: false,
|
|
622
623
|
},
|
|
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
|
-
},
|
|
624
|
+
// Inline flat invoke step, discriminated by the `invoke` key —
|
|
625
|
+
// THE dispatch site, shared with every `Run` step array rather
|
|
626
|
+
// than restated here. Restating it is what made `retry:` a schema
|
|
627
|
+
// error at boot while working one line away in a sequence: not a
|
|
628
|
+
// decision about boot, just a copy that never grew the field.
|
|
629
|
+
// Control flow (if/while/switch/try) is still not available here —
|
|
630
|
+
// reach for Run.Sequence.
|
|
631
|
+
// An expanded, stamped COPY. `builtins.ts` is not a manifest, so it
|
|
632
|
+
// never passes through the loader's expansion — embedding the
|
|
633
|
+
// fragment object itself would leave the nested `retry` as an
|
|
634
|
+
// unresolved `$ref` with no `x-telo-fragment` stamp, which is
|
|
635
|
+
// exactly what made LIVE_VALUE_RETRIED silently skip every boot
|
|
636
|
+
// target. A copy, because this is a module-level singleton and
|
|
637
|
+
// `resolveSchemaRefKinds` rewrites the `x-telo-ref` inside it.
|
|
638
|
+
manifestFragment("InvokeStep"),
|
|
665
639
|
],
|
|
666
640
|
},
|
|
667
641
|
},
|
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
|
@@ -270,4 +270,12 @@ export type {
|
|
|
270
270
|
PositionIndex,
|
|
271
271
|
Range
|
|
272
272
|
} from "./types.js";
|
|
273
|
-
|
|
273
|
+
export * from "./manifest-schemas.js";
|
|
274
|
+
// The release model — module identity, fragments, the ledger, the edge graph and
|
|
275
|
+
// version planning. Pure data in, plan out, so the editor and the CLI release
|
|
276
|
+
// from one model.
|
|
277
|
+
export {
|
|
278
|
+
DERIVED_METADATA_FIELDS,
|
|
279
|
+
authoredModuleMetadata,
|
|
280
|
+
} from "./module-metadata-scope.js";
|
|
281
|
+
export * from "./release/index.js";
|
package/src/kernel-globals.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { moduleMetadataSchema } from "./module-metadata-scope.js";
|
|
2
3
|
import { residualEntrySchemaMap } from "./residual-schema.js";
|
|
3
4
|
import { applyObservedStateNode } from "./validate-observed-state.js";
|
|
4
5
|
|
|
@@ -13,7 +14,13 @@ import { applyObservedStateNode } from "./validate-observed-state.js";
|
|
|
13
14
|
* by declaring a typed `variables:`/`secrets:` entry with an `env:` binding
|
|
14
15
|
* and referencing `variables.X` / `secrets.X`.
|
|
15
16
|
*/
|
|
16
|
-
export const KERNEL_GLOBAL_NAMES = [
|
|
17
|
+
export const KERNEL_GLOBAL_NAMES = [
|
|
18
|
+
"variables",
|
|
19
|
+
"secrets",
|
|
20
|
+
"resources",
|
|
21
|
+
"ports",
|
|
22
|
+
"module",
|
|
23
|
+
] as const;
|
|
17
24
|
|
|
18
25
|
const SYSTEM_KINDS = new Set([
|
|
19
26
|
"Telo.Definition",
|
|
@@ -90,6 +97,7 @@ interface ModuleGlobals {
|
|
|
90
97
|
variables?: Record<string, unknown>;
|
|
91
98
|
secrets?: Record<string, unknown>;
|
|
92
99
|
ports?: Record<string, unknown>;
|
|
100
|
+
module?: Record<string, unknown>;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
function globalsSchema(
|
|
@@ -103,10 +111,24 @@ function globalsSchema(
|
|
|
103
111
|
secrets: buildSchemaMapSchema(doc?.secrets as Record<string, any> | undefined),
|
|
104
112
|
resources: resourcesSchema,
|
|
105
113
|
ports: buildPortsSchema(doc?.ports as Record<string, any> | undefined),
|
|
114
|
+
module: buildModuleSchema(doc),
|
|
106
115
|
},
|
|
107
116
|
};
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
/** The `module` namespace. Derived by `moduleMetadataSchema`, the same call the
|
|
120
|
+
* CEL environment types from, so the two cannot disagree about which fields
|
|
121
|
+
* exist or whether the namespace is closed. */
|
|
122
|
+
function buildModuleSchema(doc: ModuleGlobals | Record<string, any> | undefined): Record<string, any> {
|
|
123
|
+
return (
|
|
124
|
+
moduleMetadataSchema(
|
|
125
|
+
((doc as ModuleGlobals | undefined)?.module ?? (doc as any)?.metadata) as
|
|
126
|
+
| Record<string, unknown>
|
|
127
|
+
| undefined,
|
|
128
|
+
) ?? { type: "object", additionalProperties: true }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
/** Every non-system resource name in the set, plus the scope-declared ones. */
|
|
111
133
|
function buildResourcesSchema(
|
|
112
134
|
manifests: ResourceManifest[],
|