@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.
Files changed (80) hide show
  1. package/dist/analyzer.d.ts +9 -2
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +38 -10
  4. package/dist/builtins.d.ts.map +1 -1
  5. package/dist/builtins.js +16 -42
  6. package/dist/cel-environment.d.ts.map +1 -1
  7. package/dist/cel-environment.js +35 -0
  8. package/dist/flatten-for-analyzer.d.ts +4 -0
  9. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  10. package/dist/flatten-for-analyzer.js +8 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +6 -0
  14. package/dist/kernel-globals.d.ts +1 -1
  15. package/dist/kernel-globals.d.ts.map +1 -1
  16. package/dist/kernel-globals.js +15 -1
  17. package/dist/manifest-schemas.d.ts +407 -0
  18. package/dist/manifest-schemas.d.ts.map +1 -0
  19. package/dist/manifest-schemas.js +396 -0
  20. package/dist/module-file-claims.d.ts +6 -0
  21. package/dist/module-file-claims.d.ts.map +1 -1
  22. package/dist/module-file-claims.js +4 -0
  23. package/dist/module-metadata-scope.d.ts +44 -0
  24. package/dist/module-metadata-scope.d.ts.map +1 -0
  25. package/dist/module-metadata-scope.js +82 -0
  26. package/dist/parse-loaded-file.d.ts.map +1 -1
  27. package/dist/parse-loaded-file.js +16 -0
  28. package/dist/release/bump-level.d.ts +46 -0
  29. package/dist/release/bump-level.d.ts.map +1 -0
  30. package/dist/release/bump-level.js +80 -0
  31. package/dist/release/changelog.d.ts +36 -0
  32. package/dist/release/changelog.d.ts.map +1 -0
  33. package/dist/release/changelog.js +53 -0
  34. package/dist/release/fragment.d.ts +48 -0
  35. package/dist/release/fragment.d.ts.map +1 -0
  36. package/dist/release/fragment.js +84 -0
  37. package/dist/release/index.d.ts +26 -0
  38. package/dist/release/index.d.ts.map +1 -0
  39. package/dist/release/index.js +18 -0
  40. package/dist/release/ledger.d.ts +53 -0
  41. package/dist/release/ledger.d.ts.map +1 -0
  42. package/dist/release/ledger.js +109 -0
  43. package/dist/release/payload-digest.d.ts +63 -0
  44. package/dist/release/payload-digest.d.ts.map +1 -0
  45. package/dist/release/payload-digest.js +65 -0
  46. package/dist/release/release-plan.d.ts +128 -0
  47. package/dist/release/release-plan.d.ts.map +1 -0
  48. package/dist/release/release-plan.js +277 -0
  49. package/dist/release/version-stamp.d.ts +50 -0
  50. package/dist/release/version-stamp.d.ts.map +1 -0
  51. package/dist/release/version-stamp.js +119 -0
  52. package/dist/release/workspace-config.d.ts +41 -0
  53. package/dist/release/workspace-config.d.ts.map +1 -0
  54. package/dist/release/workspace-config.js +60 -0
  55. package/dist/schema-compat.js +2 -1
  56. package/dist/validate-step-inputs.d.ts +1 -1
  57. package/dist/validate-step-inputs.d.ts.map +1 -1
  58. package/dist/validate-step-inputs.js +99 -8
  59. package/package.json +3 -3
  60. package/src/analyzer.ts +41 -10
  61. package/src/builtins.ts +16 -42
  62. package/src/cel-environment.ts +37 -0
  63. package/src/flatten-for-analyzer.ts +12 -0
  64. package/src/index.ts +9 -1
  65. package/src/kernel-globals.ts +23 -1
  66. package/src/manifest-schemas.ts +408 -0
  67. package/src/module-file-claims.ts +10 -0
  68. package/src/module-metadata-scope.ts +88 -0
  69. package/src/parse-loaded-file.ts +16 -0
  70. package/src/release/bump-level.ts +95 -0
  71. package/src/release/changelog.ts +62 -0
  72. package/src/release/fragment.ts +130 -0
  73. package/src/release/index.ts +66 -0
  74. package/src/release/ledger.ts +142 -0
  75. package/src/release/payload-digest.ts +83 -0
  76. package/src/release/release-plan.ts +392 -0
  77. package/src/release/version-stamp.ts +142 -0
  78. package/src/release/workspace-config.ts +81 -0
  79. package/src/schema-compat.ts +1 -1
  80. 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
+ }
@@ -1,7 +1,8 @@
1
1
  import AjvModule from "ajv";
2
2
  import addFormats from "ajv-formats";
3
- import { isRefSentinel, isTaggedSentinel, ManifestRootSchema, producedTypeOf, } from "@telorun/templating";
3
+ import { isRefSentinel, isTaggedSentinel, producedTypeOf, } from "@telorun/templating";
4
4
  import { celBaseOfValueType, celTypeOfValueType, readValueTypeSlot, valueBrandBases, valueTypeOf, valueTypePlaceholder, } from "@telorun/sdk";
5
+ import { ManifestRootSchema } from "./manifest-schemas.js";
5
6
  import { registerTeloKeywords } from "./value-type-keyword.js";
6
7
  const Ajv = AjvModule.default ?? AjvModule;
7
8
  /** Creates a configured AJV instance (allErrors, strict: false, with formats).
@@ -6,7 +6,7 @@ export interface StepInputIssue {
6
6
  message: string;
7
7
  /** Set when the issue is a type-argument disagreement rather than a contract
8
8
  * shape violation — the two read differently and deserve their own code. */
9
- code?: "CEL_TYPE_ARGUMENT_MISMATCH";
9
+ code?: "CEL_TYPE_ARGUMENT_MISMATCH" | "LIVE_VALUE_RETRIED";
10
10
  }
11
11
  /**
12
12
  * Validate every step's `inputs:` against the invoked target's declared input
@@ -1 +1 @@
1
- {"version":3,"file":"validate-step-inputs.d.ts","sourceRoot":"","sources":["../src/validate-step-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAoBnE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB;iFAC6E;IAC7E,IAAI,CAAC,EAAE,4BAA4B,CAAC;CACrC;AAGD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY;AACpB;;mEAEmE;AACnE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,cAAc,EAAE,CAiHlB"}
1
+ {"version":3,"file":"validate-step-inputs.d.ts","sourceRoot":"","sources":["../src/validate-step-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAqBnE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB;iFAC6E;IAC7E,IAAI,CAAC,EAAE,4BAA4B,GAAG,oBAAoB,CAAC;CAC5D;AAGD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY;AACpB;;mEAEmE;AACnE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,cAAc,EAAE,CAsJlB"}
@@ -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
- if (stepContext) {
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
- // The step context is rooted at the STEP MAP, so a `steps.` prefix is
86
- // the namespace name and not a property of it. Only that namespace is
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(stepContext, chain.slice("steps.".length));
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.59.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.15.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.74.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
- export function gatherPropertySchemas(schema: Record<string, any>): Array<[string, Record<string, any>]> {
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
- if (schema.properties && typeof schema.properties === "object") {
327
- for (const [k, v] of Object.entries(schema.properties as Record<string, any>)) {
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 = schema[variantKey];
358
+ const arr = base[variantKey];
333
359
  if (!Array.isArray(arr)) continue;
334
- for (const variant of arr) {
335
- if (variant && typeof variant === "object" && variant.properties) {
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: issue.code
2016
- ? `${m.kind}/${stepName}: inputs at '${issue.path}' flow into ${issue.targetLabel} with disagreeing type arguments: ${issue.message}`
2017
- : `${m.kind}/${stepName}: inputs at '${issue.path}' do not satisfy ${issue.targetLabel}'s declared inputType: ${issue.message}`,
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: invoke an Invocable / Runnable on boot
624
- // with an optional `name` (for steps.<name>.result plumbing),
625
- // `when` guard, and `inputs`. Discriminated by the `invoke` key.
626
- // Control flow (if/while/switch/try) is not available here
627
- // reach for Run.Sequence. `invoke` is ref-only: a `!ref` that
628
- // resolves to the `{ kind, name }` shape below. Requiring `name`
629
- // rejects an inline `{ kind }` definition (no name) at analysis
630
- // instead of failing at boot with an undefined resource name. The
631
- // Invocable/Runnable kind set mirrors Run.Sequence invoke steps.
632
- {
633
- type: "object",
634
- required: ["invoke"],
635
- properties: {
636
- name: { type: "string" },
637
- invoke: {
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
  },
@@ -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";
@@ -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 = ["variables", "secrets", "resources", "ports"] as const;
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[],