@telorun/analyzer 0.73.0 → 0.74.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 (138) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +26 -7
  3. package/dist/artifact-axes.d.ts +14 -0
  4. package/dist/artifact-axes.d.ts.map +1 -0
  5. package/dist/artifact-axes.js +12 -0
  6. package/dist/artifact-layer-index.d.ts +2 -2
  7. package/dist/artifact-layer-index.d.ts.map +1 -1
  8. package/dist/artifact-layer-index.js +9 -4
  9. package/dist/artifact-selector.d.ts +47 -21
  10. package/dist/artifact-selector.d.ts.map +1 -1
  11. package/dist/artifact-selector.js +85 -16
  12. package/dist/builtins.d.ts.map +1 -1
  13. package/dist/builtins.js +73 -16
  14. package/dist/call-graph.d.ts +12 -1
  15. package/dist/call-graph.d.ts.map +1 -1
  16. package/dist/call-graph.js +22 -0
  17. package/dist/catch-scope.js +1 -1
  18. package/dist/forwarded-declaration.d.ts +26 -0
  19. package/dist/forwarded-declaration.d.ts.map +1 -0
  20. package/dist/forwarded-declaration.js +31 -0
  21. package/dist/index.d.ts +9 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -1
  24. package/dist/kernel-globals.d.ts.map +1 -1
  25. package/dist/kernel-globals.js +21 -5
  26. package/dist/module-alias-scope.d.ts +11 -0
  27. package/dist/module-alias-scope.d.ts.map +1 -1
  28. package/dist/module-alias-scope.js +10 -0
  29. package/dist/module-file-claims.d.ts +17 -1
  30. package/dist/module-file-claims.d.ts.map +1 -1
  31. package/dist/module-file-claims.js +38 -3
  32. package/dist/module-named-files.d.ts +72 -0
  33. package/dist/module-named-files.d.ts.map +1 -0
  34. package/dist/module-named-files.js +115 -0
  35. package/dist/native-entries.d.ts +83 -0
  36. package/dist/native-entries.d.ts.map +1 -0
  37. package/dist/native-entries.js +169 -0
  38. package/dist/normalize-inline-resources.d.ts +41 -13
  39. package/dist/normalize-inline-resources.d.ts.map +1 -1
  40. package/dist/normalize-inline-resources.js +236 -54
  41. package/dist/reference-field-map.d.ts +59 -1
  42. package/dist/reference-field-map.d.ts.map +1 -1
  43. package/dist/reference-field-map.js +200 -34
  44. package/dist/release/payload-digest.d.ts +2 -1
  45. package/dist/release/payload-digest.d.ts.map +1 -1
  46. package/dist/release/payload-digest.js +2 -1
  47. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  48. package/dist/resolve-ref-sentinels.js +8 -5
  49. package/dist/resolve-throws-union.d.ts +25 -4
  50. package/dist/resolve-throws-union.d.ts.map +1 -1
  51. package/dist/resolve-throws-union.js +62 -32
  52. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  53. package/dist/resolve-zone-requirements.js +2 -1
  54. package/dist/rewrite-synthetic-origins.d.ts +6 -1
  55. package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
  56. package/dist/rewrite-synthetic-origins.js +107 -13
  57. package/dist/schema-compat.d.ts.map +1 -1
  58. package/dist/schema-compat.js +18 -5
  59. package/dist/schema-walk.d.ts +47 -13
  60. package/dist/schema-walk.d.ts.map +1 -1
  61. package/dist/schema-walk.js +150 -30
  62. package/dist/scope-declarations.d.ts +59 -0
  63. package/dist/scope-declarations.d.ts.map +1 -0
  64. package/dist/scope-declarations.js +45 -0
  65. package/dist/source-entries.d.ts +119 -0
  66. package/dist/source-entries.d.ts.map +1 -0
  67. package/dist/source-entries.js +349 -0
  68. package/dist/step-bodies.d.ts +24 -0
  69. package/dist/step-bodies.d.ts.map +1 -0
  70. package/dist/step-bodies.js +28 -0
  71. package/dist/telo-version.d.ts +1 -1
  72. package/dist/telo-version.js +1 -1
  73. package/dist/validate-cel-context.d.ts.map +1 -1
  74. package/dist/validate-cel-context.js +3 -2
  75. package/dist/validate-module-artifact.d.ts +4 -2
  76. package/dist/validate-module-artifact.d.ts.map +1 -1
  77. package/dist/validate-module-artifact.js +43 -4
  78. package/dist/validate-native-entries.d.ts +15 -0
  79. package/dist/validate-native-entries.d.ts.map +1 -0
  80. package/dist/validate-native-entries.js +103 -0
  81. package/dist/validate-observed-state.d.ts +7 -1
  82. package/dist/validate-observed-state.d.ts.map +1 -1
  83. package/dist/validate-observed-state.js +24 -6
  84. package/dist/validate-reference-forms.d.ts.map +1 -1
  85. package/dist/validate-reference-forms.js +2 -2
  86. package/dist/validate-references.d.ts.map +1 -1
  87. package/dist/validate-references.js +11 -5
  88. package/dist/validate-scope-reach.d.ts +31 -0
  89. package/dist/validate-scope-reach.d.ts.map +1 -0
  90. package/dist/validate-scope-reach.js +180 -0
  91. package/dist/validate-source-entries.d.ts +16 -0
  92. package/dist/validate-source-entries.d.ts.map +1 -0
  93. package/dist/validate-source-entries.js +90 -0
  94. package/dist/validate-throws-coverage.d.ts.map +1 -1
  95. package/dist/validate-throws-coverage.js +21 -39
  96. package/dist/value-type-keyword.d.ts +22 -5
  97. package/dist/value-type-keyword.d.ts.map +1 -1
  98. package/dist/value-type-keyword.js +16 -7
  99. package/package.json +3 -3
  100. package/src/analyzer.ts +33 -9
  101. package/src/artifact-axes.ts +24 -0
  102. package/src/artifact-layer-index.ts +9 -6
  103. package/src/artifact-selector.ts +110 -35
  104. package/src/builtins.ts +75 -16
  105. package/src/call-graph.ts +30 -0
  106. package/src/catch-scope.ts +1 -1
  107. package/src/forwarded-declaration.ts +32 -0
  108. package/src/index.ts +36 -1
  109. package/src/kernel-globals.ts +28 -5
  110. package/src/migrations/CLAUDE.md +35 -0
  111. package/src/module-alias-scope.ts +17 -0
  112. package/src/module-file-claims.ts +63 -4
  113. package/src/module-named-files.ts +162 -0
  114. package/src/native-entries.ts +227 -0
  115. package/src/normalize-inline-resources.ts +326 -70
  116. package/src/reference-field-map.ts +257 -52
  117. package/src/release/CLAUDE.md +32 -0
  118. package/src/release/payload-digest.ts +2 -1
  119. package/src/resolve-ref-sentinels.ts +7 -6
  120. package/src/resolve-throws-union.ts +87 -32
  121. package/src/resolve-zone-requirements.ts +2 -1
  122. package/src/rewrite-synthetic-origins.ts +107 -16
  123. package/src/schema-compat.ts +18 -4
  124. package/src/schema-walk.ts +162 -34
  125. package/src/scope-declarations.ts +98 -0
  126. package/src/source-entries.ts +502 -0
  127. package/src/step-bodies.ts +55 -0
  128. package/src/telo-version.ts +1 -1
  129. package/src/validate-cel-context.ts +3 -2
  130. package/src/validate-module-artifact.ts +46 -3
  131. package/src/validate-native-entries.ts +147 -0
  132. package/src/validate-observed-state.ts +36 -7
  133. package/src/validate-reference-forms.ts +2 -3
  134. package/src/validate-references.ts +10 -6
  135. package/src/validate-scope-reach.ts +213 -0
  136. package/src/validate-source-entries.ts +120 -0
  137. package/src/validate-throws-coverage.ts +24 -31
  138. package/src/value-type-keyword.ts +35 -9
@@ -0,0 +1,90 @@
1
+ import { moduleDocumentClaims } from "./module-file-claims.js";
2
+ import { crossLayerSourceLinks, stageableFiles, unclaimedSourceEntries, } from "./module-named-files.js";
3
+ import { normalizeNativePath, readNativeEntries } from "./native-entries.js";
4
+ import { readModuleSources } from "./source-entries.js";
5
+ import { DiagnosticSeverity } from "./types.js";
6
+ const SOURCE = "telo-analyzer";
7
+ /**
8
+ * The strict half of `source-entries.ts`: what `telo check` reports about a
9
+ * module doc's `sources:` block.
10
+ *
11
+ * Every rule the block alone decides is a reader problem, reported here under
12
+ * its own code. This adds the rules that need the rest of the module or that
13
+ * only publish enforces: an entry must stage a file the manifest names, a link
14
+ * ships in its target's layer, and every file entry and every build carries its
15
+ * pin. Shape errors are left to the owner doc's JSON Schema.
16
+ *
17
+ * Entry-module-scoped: a dependency's block is not the consumer's to fix.
18
+ */
19
+ export function validateSourceEntries(manifests, entryModules) {
20
+ const out = [];
21
+ for (const manifest of manifests) {
22
+ if (manifest.kind !== "Telo.Application" && manifest.kind !== "Telo.Library")
23
+ continue;
24
+ const metadata = (manifest.metadata ?? {});
25
+ const name = typeof metadata.name === "string" ? metadata.name : undefined;
26
+ const owned = entryModules === undefined ||
27
+ (typeof metadata.module === "string"
28
+ ? entryModules.has(metadata.module)
29
+ : name === undefined || entryModules.has(name));
30
+ if (!owned)
31
+ continue;
32
+ const label = `${manifest.kind}/${name ?? "(unnamed)"}`;
33
+ const report = (code, path, message) => out.push({
34
+ severity: DiagnosticSeverity.Error,
35
+ code,
36
+ source: SOURCE,
37
+ message: `${label}: ${message}`,
38
+ data: {
39
+ resource: { kind: manifest.kind, name },
40
+ filePath: typeof metadata.source === "string" ? metadata.source : undefined,
41
+ path,
42
+ },
43
+ });
44
+ const { sources, problems } = readModuleSources(manifest);
45
+ for (const problem of problems) {
46
+ if (problem.code === "SHAPE")
47
+ continue;
48
+ report(problem.code, problem.path, problem.message);
49
+ }
50
+ if (sources.length === 0)
51
+ continue;
52
+ for (const source of sources) {
53
+ if (source.build && source.build.inputs === undefined) {
54
+ report("SOURCE_BUILD_UNPINNED", `sources.${source.name}.build`, `source '${source.name}' names the crate its files are built from but records no build ` +
55
+ `inputs, so a later edit to that crate cannot be caught — run \`telo release stage ` +
56
+ `--pin\` after building its files. \`telo release check\` and publish refuse it.`);
57
+ }
58
+ for (const entry of source.entries) {
59
+ if (entry.kind !== "file" || entry.pin)
60
+ continue;
61
+ report("SOURCE_ENTRY_UNPINNED", `sources.${source.name}.entries.${entry.key}`, `source '${source.name}' entry '${entry.key}' carries no pin (sha256 and executable), so ` +
62
+ `nothing can verify the file it stages: a kernel refuses to read it and publish refuses ` +
63
+ `to ship it — run \`telo release stage --pin\`.`);
64
+ }
65
+ }
66
+ const moduleName = typeof metadata.module === "string" ? metadata.module : name;
67
+ const stageable = stageableFiles(readNativeEntries(manifest).entries, moduleDocumentClaims(manifests, moduleName));
68
+ for (const { source, entry, layer, targetLayer } of crossLayerSourceLinks(sources, stageable)) {
69
+ report("SOURCE_LINK_TARGET_UNRESOLVED", `sources.${source.name}.entries.${entry.key}.target`, `source '${source.name}' entry '${entry.key}': the link ships in the ${layer} layer, but ` +
70
+ `its target '${entry.resolved}' ships in the ${targetLayer} layer. A runtime extracts ` +
71
+ `only the layers it needs, so the link would dangle — point it at a file of its own layer.`);
72
+ }
73
+ // A `native:` entry with an unrelated problem still names its path; reported
74
+ // once, as that problem, rather than again as an unclaimed source entry.
75
+ const unreadNative = new Set();
76
+ const native = manifest.native;
77
+ for (const entry of Array.isArray(native) ? native : []) {
78
+ const path = entry?.path;
79
+ const verdict = typeof path === "string" ? normalizeNativePath(path.trim()) : undefined;
80
+ if (verdict && "path" in verdict)
81
+ unreadNative.add(verdict.path);
82
+ }
83
+ for (const { source, entry } of unclaimedSourceEntries(sources, stageable, unreadNative)) {
84
+ report("SOURCE_ENTRY_UNCLAIMED", `sources.${source.name}.entries.${entry.key}`, `source '${source.name}' entry '${entry.key}': nothing in the module names '${entry.path}' ` +
85
+ `— no native: entry's path, no platform-qualified controller candidate's path=, and ` +
86
+ `none of the source's notices. Name the file where it is used, or remove the entry.`);
87
+ }
88
+ }
89
+ return out;
90
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAenE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA4kBzE,oDAAoD;AACpD,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,WAAW,EAChB,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAa,EACvD,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa;AACpC;;+BAE+B;AAC/B,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAa,GAC3D,kBAAkB,EAAE,CAsLtB"}
1
+ {"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAkBnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAkkBzE,oDAAoD;AACpD,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,WAAW,EAChB,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAa,EACvD,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa;AACpC;;+BAE+B;AAC/B,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAa,GAC3D,kBAAkB,EAAE,CAsLtB"}
@@ -1,11 +1,11 @@
1
1
  import { isTaggedSentinel } from "@telorun/templating";
2
2
  import { AMBIENT_CONTRACT_ERROR_CODES, isAmbientContractErrorCode, } from "@telorun/sdk";
3
3
  import { scopeResolverForModule } from "./alias-resolver.js";
4
- import { createResolveCtx, resolveScopeUnion, resolveThrowsUnion, } from "./resolve-throws-union.js";
4
+ import { createResolveCtx, handsFailureBack, resolveScopeUnion, resolveThrowsUnion, throwsUses, } from "./resolve-throws-union.js";
5
+ import { forEachDeclaredSlot } from "./schema-walk.js";
5
6
  import { buildEnclosers, collectScopedManifests, enclosingCoverage, } from "./catch-scope.js";
6
7
  import { DiagnosticSeverity } from "./types.js";
7
8
  import { extractAccessChains, validateChainAgainstSchema } from "./validate-cel-context.js";
8
- import { isStepSlot } from "./step-slot.js";
9
9
  const SOURCE = "telo-analyzer";
10
10
  const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
11
11
  /** Walk `definition.schema` and `data` in tandem, invoking `onOutcome` each
@@ -401,12 +401,6 @@ function checkCelChainAgainstDataSchema(entry, dataSchema, resource, filePath, e
401
401
  }
402
402
  return diagnostics;
403
403
  }
404
- /** Rule 8 extension: `inherit: true` only makes sense on a definition whose
405
- * schema declares at least one STEP BODY — an array whose items point at the
406
- * shared grammar, or, for a module published before that fragment existed, one
407
- * carrying the legacy `x-telo-step-context` annotation. That is what drives the
408
- * resolver's generic step traversal; a definition with `inherit: true` and no
409
- * such array has no invocables to inherit from. */
410
404
  /** The capabilities whose lifecycle includes a dispatch a caller can catch. On
411
405
  * every other one a thrown error is a boot-time failure, not a structured
412
406
  * runtime error for a downstream caller — a provider resolves configuration, a
@@ -466,10 +460,14 @@ function validateThrowsDeclarations(manifests) {
466
460
  severity: DiagnosticSeverity.Error,
467
461
  code: "INHERIT_WITHOUT_STEP_CONTEXT",
468
462
  source: SOURCE,
469
- message: `Telo.Definition '${name}' declares throws.inherit: true but its schema declares no step ` +
470
- `body. inherit is only meaningful on a definition that drives invocables through steps ` +
471
- `give an array field 'items: { $ref: "telo://manifest#/$defs/Step" }' (the legacy ` +
472
- `x-telo-step-context annotation is also recognised).`,
463
+ message: `Telo.Definition '${name}' declares throws.inherit: true but its schema dispatches nothing ` +
464
+ `a failure can come back through. inherit makes the kind's union the union of what it ` +
465
+ `dispatches, so the schema needs a step body (an array field with ` +
466
+ `'items: { $ref: "telo://manifest#/$defs/Step" }', or the legacy x-telo-step-context ` +
467
+ `annotation) or a reference slot whose use includes 'call' or 'trigger.consumer' ` +
468
+ `(for a use case map, in any case; a slot declaring no use counts as 'call'). ` +
469
+ `'detached' and 'trigger.inbound' run where no caller awaits them, and ` +
470
+ `'dependency' / 'schema' dispatch nothing.`,
473
471
  data: { resource: { kind: m.kind, name }, filePath, path: "throws.inherit" },
474
472
  });
475
473
  }
@@ -477,35 +475,19 @@ function validateThrowsDeclarations(manifests) {
477
475
  }
478
476
  return diagnostics;
479
477
  }
478
+ /** Rule 8 extension: `inherit: true` needs something to inherit from — a step
479
+ * body, or a reference slot whose use (any case of a case map) hands a failure
480
+ * back. The schema-only mode of the walk the resolver runs per instance, so the
481
+ * check and the union reach the same slots. */
480
482
  function schemaDrivesInvocables(schema) {
481
- if (!schema || typeof schema !== "object")
482
- return false;
483
- if (isStepSlot(schema))
484
- return true;
485
- const props = schema.properties;
486
- if (props && typeof props === "object") {
487
- for (const v of Object.values(props)) {
488
- if (schemaDrivesInvocables(v))
489
- return true;
490
- }
491
- }
492
- if (schema.items && schemaDrivesInvocables(schema.items))
493
- return true;
494
- for (const key of ["oneOf", "anyOf", "allOf"]) {
495
- const arr = schema[key];
496
- if (Array.isArray(arr)) {
497
- for (const sub of arr)
498
- if (schemaDrivesInvocables(sub))
499
- return true;
483
+ let drives = false;
484
+ forEachDeclaredSlot(schema, (declared) => {
485
+ if (declared.kind === "step" ||
486
+ declared.slots.some((slot) => throwsUses(slot).some(handsFailureBack))) {
487
+ drives = true;
500
488
  }
501
- }
502
- if (schema.$defs && typeof schema.$defs === "object") {
503
- for (const v of Object.values(schema.$defs)) {
504
- if (schemaDrivesInvocables(v))
505
- return true;
506
- }
507
- }
508
- return false;
489
+ });
490
+ return drives;
509
491
  }
510
492
  /** Entry point — invoked once per analyze() run. */
511
493
  export function validateThrowsCoverage(manifests, defs, aliases, env, aliasesByModule = new Map(), rootModules = new Set(),
@@ -29,6 +29,20 @@ import type { KeywordDefinition } from "ajv";
29
29
  * `x-telo-type` is deliberately absent — it is the one that emits code.
30
30
  */
31
31
  export declare const ANNOTATION_KEYWORDS: readonly ["x-telo-bindings-from", "x-telo-catches-for", "x-telo-context", "x-telo-context-collection-from", "x-telo-context-element-from", "x-telo-context-from", "x-telo-context-from-ref-kind", "x-telo-context-from-root", "x-telo-context-ref-from", "x-telo-error-context", "x-telo-eval", "x-telo-inline", "x-telo-outcome-list", "x-telo-provides-zone", "x-telo-ref", "x-telo-requires-zone", "x-telo-resource-rules", "x-telo-schema-from", "x-telo-schema-map", "x-telo-schema-projection", "x-telo-schema-projection-from", "x-telo-scope", "x-telo-sensitive", "x-telo-step-context", "x-telo-topology-role", "x-telo-value-schema-from", "x-telo-widget"];
32
+ /** How an AJV instance treats a `live` value type. */
33
+ export interface TeloKeywordOptions {
34
+ /**
35
+ * Assert a `live` type as an instance too, so a literal at such a slot is
36
+ * refused — a value written in a manifest can never be a live instance.
37
+ *
38
+ * STATIC analysis only, where a CEL leaf's stand-in is an instance
39
+ * (`celPlaceholderForSchema`). The kernel's instances leave it off: at
40
+ * dispatch a live value is exempt from validation, and the stage that pulls
41
+ * from it refuses a non-stream itself — so the static half refuses a strict
42
+ * subset of what the runtime refuses, and the two cannot disagree.
43
+ */
44
+ readonly assertLive?: boolean;
45
+ }
32
46
  /**
33
47
  * The `x-telo-type` keyword.
34
48
  *
@@ -37,8 +51,10 @@ export declare const ANNOTATION_KEYWORDS: readonly ["x-telo-bindings-from", "x-t
37
51
  * - a `json` representation validates through its own declared schema, so the
38
52
  * keyword emits nothing — the name carries nominal identity for static wiring
39
53
  * and has no runtime existence at all;
40
- * - a `live` instance is EXEMPT: its value is never traversed, because iterating
41
- * a stream to check it is precisely what the exemption is for;
54
+ * - a `live` instance is EXEMPT at dispatch: its value is never traversed,
55
+ * because iterating a stream to check it is precisely what the exemption is
56
+ * for. Statically ({@link TeloKeywordOptions.assertLive}) it is asserted like
57
+ * any other instance, which checks what the value IS and never iterates it;
42
58
  * - every other instance is ASSERTED against the constructor its binding names.
43
59
  *
44
60
  * An unknown name emits nothing here. It is a hard diagnostic in the analyzer
@@ -46,15 +62,16 @@ export declare const ANNOTATION_KEYWORDS: readonly ["x-telo-bindings-from", "x-t
46
62
  * manifest that wrote it; failing compilation instead would take out every
47
63
  * validator in a module for one typo in one slot.
48
64
  */
49
- export declare function valueTypeKeyword(): KeywordDefinition;
65
+ export declare function valueTypeKeyword(options?: TeloKeywordOptions): KeywordDefinition;
50
66
  /**
51
67
  * Register every Telo keyword on an AJV instance: the annotations as no-ops and
52
68
  * `x-telo-type` as the one that checks.
53
69
  *
54
70
  * Every AJV instance in the runtime and the analyzer goes through this, so a
55
- * schema means the same thing wherever it is validated.
71
+ * schema means the same thing wherever it is validated — apart from the one
72
+ * posture {@link TeloKeywordOptions} names, which only static analysis takes.
56
73
  */
57
74
  export declare function registerTeloKeywords(ajv: {
58
75
  addKeyword: (keyword: any, definition?: any) => unknown;
59
- }): void;
76
+ }, options?: TeloKeywordOptions): void;
60
77
  //# sourceMappingURL=value-type-keyword.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"value-type-keyword.d.ts","sourceRoot":"","sources":["../src/value-type-keyword.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;+EAqB+E;AAG/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,KAAK,CAAC;AAiB7C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,woBA4BtB,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,IAAI,iBAAiB,CA+BpD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACxC,UAAU,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;CACzD,GAAG,IAAI,CAGP"}
1
+ {"version":3,"file":"value-type-keyword.d.ts","sourceRoot":"","sources":["../src/value-type-keyword.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;+EAqB+E;AAG/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,KAAK,CAAC;AAiB7C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,woBA4BtB,CAAC;AAEX,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,kBAAuB,GAAG,iBAAiB,CAsCpF;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE;IAAE,UAAU,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,GAAG,KAAK,OAAO,CAAA;CAAE,EAChE,OAAO,GAAE,kBAAuB,GAC/B,IAAI,CAGN"}
@@ -68,8 +68,10 @@ export const ANNOTATION_KEYWORDS = [
68
68
  * - a `json` representation validates through its own declared schema, so the
69
69
  * keyword emits nothing — the name carries nominal identity for static wiring
70
70
  * and has no runtime existence at all;
71
- * - a `live` instance is EXEMPT: its value is never traversed, because iterating
72
- * a stream to check it is precisely what the exemption is for;
71
+ * - a `live` instance is EXEMPT at dispatch: its value is never traversed,
72
+ * because iterating a stream to check it is precisely what the exemption is
73
+ * for. Statically ({@link TeloKeywordOptions.assertLive}) it is asserted like
74
+ * any other instance, which checks what the value IS and never iterates it;
73
75
  * - every other instance is ASSERTED against the constructor its binding names.
74
76
  *
75
77
  * An unknown name emits nothing here. It is a hard diagnostic in the analyzer
@@ -77,7 +79,7 @@ export const ANNOTATION_KEYWORDS = [
77
79
  * manifest that wrote it; failing compilation instead would take out every
78
80
  * validator in a module for one typo in one slot.
79
81
  */
80
- export function valueTypeKeyword() {
82
+ export function valueTypeKeyword(options = {}) {
81
83
  return {
82
84
  keyword: X_TELO_TYPE,
83
85
  // Both spellings: a bare name, or the object form carrying type arguments.
@@ -86,7 +88,9 @@ export function valueTypeKeyword() {
86
88
  const entry = readValueTypeSlot({
87
89
  [X_TELO_TYPE]: cxt.schema,
88
90
  })?.entry;
89
- if (!entry || entry.representation !== "instance" || entry.live)
91
+ if (!entry || entry.representation !== "instance")
92
+ return;
93
+ if (entry.live && !options.assertLive)
90
94
  return;
91
95
  const binding = VALUE_TYPE_BINDINGS[entry.binding];
92
96
  if (!binding)
@@ -104,6 +108,10 @@ export function valueTypeKeyword() {
104
108
  error: {
105
109
  message: (cxt) => {
106
110
  const entry = readValueTypeSlot({ [X_TELO_TYPE]: cxt.schema })?.entry;
111
+ if (entry?.live) {
112
+ return (`must be a live ${entry.name} — a value written in the manifest can never be one; ` +
113
+ "pass the result of a step that produces it, with a !cel expression");
114
+ }
107
115
  return entry?.binding === "bytes"
108
116
  ? "must be raw bytes (a Uint8Array) — bytes cannot be written inline in a manifest"
109
117
  : `must be a ${entry?.name ?? "declared value type"} — this value is not writable inline in a manifest`;
@@ -116,10 +124,11 @@ export function valueTypeKeyword() {
116
124
  * `x-telo-type` as the one that checks.
117
125
  *
118
126
  * Every AJV instance in the runtime and the analyzer goes through this, so a
119
- * schema means the same thing wherever it is validated.
127
+ * schema means the same thing wherever it is validated — apart from the one
128
+ * posture {@link TeloKeywordOptions} names, which only static analysis takes.
120
129
  */
121
- export function registerTeloKeywords(ajv) {
130
+ export function registerTeloKeywords(ajv, options = {}) {
122
131
  for (const keyword of ANNOTATION_KEYWORDS)
123
132
  ajv.addKeyword(keyword);
124
- ajv.addKeyword(valueTypeKeyword());
133
+ ajv.addKeyword(valueTypeKeyword(options));
125
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -43,14 +43,14 @@
43
43
  "jsonpath-plus": "^10.3.0",
44
44
  "packageurl-js": "^2.0.1",
45
45
  "yaml": "^2.8.3",
46
- "@telorun/templating": "0.20.0"
46
+ "@telorun/templating": "0.20.1"
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
52
  "@telorun/glob": "0.3.0",
53
- "@telorun/sdk": "0.87.0"
53
+ "@telorun/sdk": "0.90.0"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@telorun/sdk": "*"
package/src/analyzer.ts CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  buildObservedStateIndex,
50
50
  buildObservedStateResourcesSchema,
51
51
  collectRunReachableNames,
52
+ forwardedResourceKey,
52
53
  observedStateRead,
53
54
  validateObservedStateDeclarations,
54
55
  } from "./validate-observed-state.js";
@@ -167,6 +168,8 @@ import { validateIdentifierNames } from "./validate-identifier-names.js";
167
168
  import { validateExtends } from "./validate-extends.js";
168
169
  import { validateLogging } from "./validate-logging.js";
169
170
  import { validateModuleArtifact } from "./validate-module-artifact.js";
171
+ import { validateNativeEntries } from "./validate-native-entries.js";
172
+ import { validateSourceEntries } from "./validate-source-entries.js";
170
173
  import { validateIncludePlacement } from "./validate-include-placement.js";
171
174
  import { validateModuleMetadata } from "./validate-module-metadata.js";
172
175
  import { validateRequires } from "./validate-requires.js";
@@ -182,6 +185,8 @@ import { validateResourceInputs } from "./validate-resource-inputs.js";
182
185
  import { validateExports } from "./validate-exports.js";
183
186
  import { validateTemplateBody } from "./validate-template-body.js";
184
187
  import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
188
+ import { validateScopedNameReach } from "./validate-scope-reach.js";
189
+ import { isForwardedDeclaration } from "./forwarded-declaration.js";
185
190
  import { validateThrowsCoverage } from "./validate-throws-coverage.js";
186
191
  import { readStepSlot } from "./step-slot.js";
187
192
 
@@ -406,9 +411,9 @@ function validateStepInvokeReferences(
406
411
  if (typeof m === "string") loadedModules.add(m);
407
412
  continue;
408
413
  }
409
- const meta = r.metadata as { name?: unknown; module?: unknown; forwardedExport?: unknown };
414
+ const meta = r.metadata as { name?: unknown; module?: unknown };
410
415
  if (typeof meta?.name !== "string" || REF_VALIDATION_SKIP_KINDS.has(r.kind)) continue;
411
- if (meta.forwardedExport === true) {
416
+ if (isForwardedDeclaration(r)) {
412
417
  if (typeof meta.module === "string") loadedModules.add(meta.module);
413
418
  continue;
414
419
  }
@@ -476,11 +481,11 @@ function validateStepInvokeReferences(
476
481
  };
477
482
 
478
483
  for (const m of allManifests) {
479
- const meta = m.metadata as { name?: unknown; source?: unknown; forwardedExport?: unknown };
484
+ const meta = m.metadata as { name?: unknown; source?: unknown };
480
485
  if (
481
486
  typeof meta?.name !== "string" ||
482
487
  REF_VALIDATION_SKIP_KINDS.has(m.kind) ||
483
- meta.forwardedExport === true
488
+ isForwardedDeclaration(m)
484
489
  )
485
490
  continue;
486
491
  const def = defs.resolve(aliases.resolveKind(m.kind) ?? m.kind);
@@ -1534,6 +1539,11 @@ export class StaticAnalyzer {
1534
1539
  // would otherwise fail on a consumer's machine — or, for a mistyped platform
1535
1540
  // axis, silently offer one platform's binary to every host.
1536
1541
  diagnostics.push(...validateModuleArtifact(allManifests));
1542
+ // The module doc's `native:` block: each rule decides whether a platform's
1543
+ // file can ever match a host, or whether two layers would write one path.
1544
+ diagnostics.push(...validateNativeEntries(allManifests, rootModules));
1545
+ // The module doc's `sources:` block: where each staged file comes from.
1546
+ diagnostics.push(...validateSourceEntries(allManifests, rootModules));
1537
1547
  // The descriptive `metadata:` surface. Nothing in the kernel branches on
1538
1548
  // these fields, which is precisely why they need a check: a mistyped one
1539
1549
  // has no runtime failure mode that would ever surface it.
@@ -1815,9 +1825,7 @@ export class StaticAnalyzer {
1815
1825
  // `kind`/CEL are authored in that module's scope (e.g. `Self.X` → that module, not the
1816
1826
  // consumer). Re-validating against the consumer's scope yields false UNDEFINED_KIND /
1817
1827
  // scope-mismatch errors, so skip — they participate here only as resolution targets.
1818
- if ((m.metadata as { forwardedExport?: boolean } | undefined)?.forwardedExport === true) {
1819
- continue;
1820
- }
1828
+ if (isForwardedDeclaration(m)) continue;
1821
1829
 
1822
1830
  // A kind-only stand-in for a `resources:` entry is a DECLARATION, not an
1823
1831
  // instantiation: its kind is routinely an abstract and its configuration
@@ -2586,9 +2594,17 @@ export class StaticAnalyzer {
2586
2594
  if (!read) continue;
2587
2595
  // An import's exported instance is indexed under `<Alias>.<name>`,
2588
2596
  // the two-level shape it publishes under, so a cross-module read
2589
- // is checked exactly like a local one.
2597
+ // is checked exactly like a local one. A bare name read inside a
2598
+ // dependency's own manifest names that dependency's resource.
2599
+ const readerModule = isForwardedDeclaration(m)
2600
+ ? (m.metadata?.module as string | undefined)
2601
+ : undefined;
2590
2602
  const reported = observedState.get(
2591
- read.alias ? `${read.alias}.${read.name}` : read.name,
2603
+ read.alias
2604
+ ? `${read.alias}.${read.name}`
2605
+ : readerModule !== undefined
2606
+ ? forwardedResourceKey(readerModule, read.name)
2607
+ : read.name,
2592
2608
  );
2593
2609
 
2594
2610
  if (celRuleApplies && celEvalModeAt(celSites, path) === "compile") {
@@ -2871,6 +2887,14 @@ export class StaticAnalyzer {
2871
2887
  ),
2872
2888
  );
2873
2889
 
2890
+ // An inline declaration referencing a name declared by a scope it was
2891
+ // written inside but is created outside of.
2892
+ diagnostics.push(
2893
+ ...validateScopedNameReach(allManifests, defs, aliases, aliasesByModule, rootModules, (expr) =>
2894
+ celAccessChains(this.celEnv, expr),
2895
+ ),
2896
+ );
2897
+
2874
2898
  // Reroute diagnostics on synthetic (inline-extracted) resources back to
2875
2899
  // the chain root so position-index lookups land on the parent doc.
2876
2900
  return rewriteSyntheticOrigins(
@@ -0,0 +1,24 @@
1
+ // GENERATED by scripts/generate-artifact-axes.mjs from analyzer/artifact-axes/axes.json — do not edit.
2
+
3
+ /** The selector platform axes, in canonical order. Closed as a set of axis
4
+ * names; the set of values stays open. */
5
+ export const PLATFORM_AXES = ["os", "arch", "libc", "abi"] as const;
6
+
7
+ export type PlatformAxis = (typeof PLATFORM_AXES)[number];
8
+
9
+ /** A form an axis value must take beyond the shared token grammar. */
10
+ export interface AxisValueForm {
11
+ readonly pattern: RegExp;
12
+ /** The form as a reader writes it, e.g. `<family>-<version>`. */
13
+ readonly form: string;
14
+ readonly examples: readonly string[];
15
+ }
16
+
17
+ /** The axes whose values carry a form of their own. */
18
+ export const AXIS_VALUE_FORMS: Readonly<Partial<Record<PlatformAxis, AxisValueForm>>> = {
19
+ abi: {
20
+ pattern: new RegExp("^[a-z][a-z0-9_]*-[0-9]+(\\.[0-9]+)*$"),
21
+ form: "<family>-<version>",
22
+ examples: ["node-137", "telo-2"],
23
+ },
24
+ };
@@ -29,7 +29,6 @@
29
29
  */
30
30
 
31
31
  import {
32
- LAYER_ROLES,
33
32
  isLayerRole,
34
33
  normalizeSelector,
35
34
  roleCarriesSelector,
@@ -48,7 +47,7 @@ const CONTENT_DIGEST = /^sha256-[A-Za-z0-9_-]{43}$/;
48
47
 
49
48
  export interface ArtifactLayer {
50
49
  role: LayerRole;
51
- /** Present on the code-bearing roles (`controller`, `library`) only. */
50
+ /** Present on the selector-keyed roles (`controller`, `library`, `native`) only. */
52
51
  selector?: ArtifactSelector;
53
52
  /** OCI blob digest — addresses the layer and verifies the transfer. */
54
53
  blob: string;
@@ -101,10 +100,12 @@ export function parseLayerIndex(value: unknown, describe = "layers"): ArtifactLa
101
100
  const entry = raw as Record<string, unknown>;
102
101
  if (typeof entry.role !== "string" || entry.role === "") {
103
102
  throw new LayerIndexError(
104
- `${where}: role is required and must be one of ${LAYER_ROLES.map((r) => `'${r}'`).join(", ")}; ` +
103
+ `${where}: role is required and must be a non-empty string; ` +
105
104
  `got ${entry.role === undefined ? "nothing" : `'${String(entry.role)}'`}.`,
106
105
  );
107
106
  }
107
+ const blob = digest("blob", entry.blob, where);
108
+ const integrity = digest("integrity", entry.integrity, where);
108
109
  // A role this runtime does not know is SKIPPED, never rejected. Roles are
109
110
  // added over time, and a runtime that cannot name one cannot need it — while
110
111
  // throwing would make the whole manifest unreadable, so a module gaining a
@@ -120,6 +121,8 @@ export function parseLayerIndex(value: unknown, describe = "layers"): ArtifactLa
120
121
  throw new LayerIndexError(`${where}: a ${role} layer must declare a selector.`);
121
122
  }
122
123
  selector = normalizeSelector(entry.selector, where);
124
+ // An unknown axis skips the entry as an unknown role does (spec §3.1).
125
+ if (selector === undefined) return;
123
126
  // Scoped by role: a module's `js` controller layer and its `js` library
124
127
  // layer are different layers with the same selector, and only a collision
125
128
  // *within* one role means two layers claim one address.
@@ -146,8 +149,8 @@ export function parseLayerIndex(value: unknown, describe = "layers"): ArtifactLa
146
149
  layers.push({
147
150
  role,
148
151
  ...(selector ? { selector } : {}),
149
- blob: digest("blob", entry.blob, where),
150
- integrity: digest("integrity", entry.integrity, where),
152
+ blob,
153
+ integrity,
151
154
  });
152
155
  });
153
156
 
@@ -157,7 +160,7 @@ export function parseLayerIndex(value: unknown, describe = "layers"): ArtifactLa
157
160
  /** The singleton layer for a role, or undefined when the artifact has none. */
158
161
  export function singletonLayer(
159
162
  layers: readonly ArtifactLayer[],
160
- role: Exclude<LayerRole, "controller" | "library">,
163
+ role: Exclude<LayerRole, "controller" | "library" | "native">,
161
164
  ): ArtifactLayer | undefined {
162
165
  return layers.find((l) => l.role === role);
163
166
  }