@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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +26 -7
- package/dist/artifact-axes.d.ts +14 -0
- package/dist/artifact-axes.d.ts.map +1 -0
- package/dist/artifact-axes.js +12 -0
- package/dist/artifact-layer-index.d.ts +2 -2
- package/dist/artifact-layer-index.d.ts.map +1 -1
- package/dist/artifact-layer-index.js +9 -4
- package/dist/artifact-selector.d.ts +47 -21
- package/dist/artifact-selector.d.ts.map +1 -1
- package/dist/artifact-selector.js +85 -16
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +73 -16
- package/dist/call-graph.d.ts +12 -1
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +22 -0
- package/dist/catch-scope.js +1 -1
- package/dist/forwarded-declaration.d.ts +26 -0
- package/dist/forwarded-declaration.d.ts.map +1 -0
- package/dist/forwarded-declaration.js +31 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +21 -5
- package/dist/module-alias-scope.d.ts +11 -0
- package/dist/module-alias-scope.d.ts.map +1 -1
- package/dist/module-alias-scope.js +10 -0
- package/dist/module-file-claims.d.ts +17 -1
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +38 -3
- package/dist/module-named-files.d.ts +72 -0
- package/dist/module-named-files.d.ts.map +1 -0
- package/dist/module-named-files.js +115 -0
- package/dist/native-entries.d.ts +83 -0
- package/dist/native-entries.d.ts.map +1 -0
- package/dist/native-entries.js +169 -0
- package/dist/normalize-inline-resources.d.ts +41 -13
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +236 -54
- package/dist/reference-field-map.d.ts +59 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +200 -34
- package/dist/release/payload-digest.d.ts +2 -1
- package/dist/release/payload-digest.d.ts.map +1 -1
- package/dist/release/payload-digest.js +2 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +8 -5
- package/dist/resolve-throws-union.d.ts +25 -4
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +62 -32
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +2 -1
- package/dist/rewrite-synthetic-origins.d.ts +6 -1
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
- package/dist/rewrite-synthetic-origins.js +107 -13
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +18 -5
- package/dist/schema-walk.d.ts +47 -13
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +150 -30
- package/dist/scope-declarations.d.ts +59 -0
- package/dist/scope-declarations.d.ts.map +1 -0
- package/dist/scope-declarations.js +45 -0
- package/dist/source-entries.d.ts +119 -0
- package/dist/source-entries.d.ts.map +1 -0
- package/dist/source-entries.js +349 -0
- package/dist/step-bodies.d.ts +24 -0
- package/dist/step-bodies.d.ts.map +1 -0
- package/dist/step-bodies.js +28 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +3 -2
- package/dist/validate-module-artifact.d.ts +4 -2
- package/dist/validate-module-artifact.d.ts.map +1 -1
- package/dist/validate-module-artifact.js +43 -4
- package/dist/validate-native-entries.d.ts +15 -0
- package/dist/validate-native-entries.d.ts.map +1 -0
- package/dist/validate-native-entries.js +103 -0
- package/dist/validate-observed-state.d.ts +7 -1
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +24 -6
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +2 -2
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +11 -5
- package/dist/validate-scope-reach.d.ts +31 -0
- package/dist/validate-scope-reach.d.ts.map +1 -0
- package/dist/validate-scope-reach.js +180 -0
- package/dist/validate-source-entries.d.ts +16 -0
- package/dist/validate-source-entries.d.ts.map +1 -0
- package/dist/validate-source-entries.js +90 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +21 -39
- package/dist/value-type-keyword.d.ts +22 -5
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +16 -7
- package/package.json +3 -3
- package/src/analyzer.ts +33 -9
- package/src/artifact-axes.ts +24 -0
- package/src/artifact-layer-index.ts +9 -6
- package/src/artifact-selector.ts +110 -35
- package/src/builtins.ts +75 -16
- package/src/call-graph.ts +30 -0
- package/src/catch-scope.ts +1 -1
- package/src/forwarded-declaration.ts +32 -0
- package/src/index.ts +36 -1
- package/src/kernel-globals.ts +28 -5
- package/src/migrations/CLAUDE.md +35 -0
- package/src/module-alias-scope.ts +17 -0
- package/src/module-file-claims.ts +63 -4
- package/src/module-named-files.ts +162 -0
- package/src/native-entries.ts +227 -0
- package/src/normalize-inline-resources.ts +326 -70
- package/src/reference-field-map.ts +257 -52
- package/src/release/CLAUDE.md +32 -0
- package/src/release/payload-digest.ts +2 -1
- package/src/resolve-ref-sentinels.ts +7 -6
- package/src/resolve-throws-union.ts +87 -32
- package/src/resolve-zone-requirements.ts +2 -1
- package/src/rewrite-synthetic-origins.ts +107 -16
- package/src/schema-compat.ts +18 -4
- package/src/schema-walk.ts +162 -34
- package/src/scope-declarations.ts +98 -0
- package/src/source-entries.ts +502 -0
- package/src/step-bodies.ts +55 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +3 -2
- package/src/validate-module-artifact.ts +46 -3
- package/src/validate-native-entries.ts +147 -0
- package/src/validate-observed-state.ts +36 -7
- package/src/validate-reference-forms.ts +2 -3
- package/src/validate-references.ts +10 -6
- package/src/validate-scope-reach.ts +213 -0
- package/src/validate-source-entries.ts +120 -0
- package/src/validate-throws-coverage.ts +24 -31
- 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;
|
|
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
|
|
470
|
-
`
|
|
471
|
-
`
|
|
472
|
-
`x-telo-step-context
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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,
|
|
41
|
-
* a stream to check it is precisely what the exemption is
|
|
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
|
|
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,
|
|
72
|
-
* a stream to check it is precisely what the exemption is
|
|
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"
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
150
|
-
integrity
|
|
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
|
}
|