@telorun/analyzer 0.64.0 → 0.66.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 +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +175 -65
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +7 -1
- package/dist/cel-environment.d.ts +12 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +35 -19
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +24 -4
- package/dist/definition-registry.d.ts +47 -6
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +82 -13
- package/dist/eval-paths.d.ts +68 -0
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +138 -0
- package/dist/flatten-for-analyzer.d.ts +1 -1
- package/dist/flatten-for-analyzer.js +1 -1
- package/dist/import-resolution-diagnostics.d.ts +1 -1
- package/dist/import-resolution-diagnostics.js +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +8 -1
- package/dist/migrations/entries/index.d.ts.map +1 -1
- package/dist/migrations/entries/index.js +2 -0
- package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
- package/dist/peer-binding.d.ts +232 -0
- package/dist/peer-binding.d.ts.map +1 -0
- package/dist/peer-binding.js +418 -0
- package/dist/ref-slot.d.ts +13 -0
- package/dist/ref-slot.d.ts.map +1 -1
- package/dist/ref-slot.js +26 -0
- package/dist/reference-field-map.d.ts +28 -0
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +25 -0
- package/dist/referrer-rule.d.ts +17 -0
- package/dist/referrer-rule.d.ts.map +1 -1
- package/dist/referrer-rule.js +4 -1
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.js +1 -1
- package/dist/resource-rule.d.ts +40 -27
- package/dist/resource-rule.d.ts.map +1 -1
- package/dist/resource-rule.js +61 -7
- package/dist/rule-condition.d.ts +11 -0
- package/dist/rule-condition.d.ts.map +1 -1
- package/dist/rule-condition.js +13 -0
- package/dist/schema-compat.d.ts +59 -22
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +60 -75
- package/dist/schema-error-report.d.ts +68 -0
- package/dist/schema-error-report.d.ts.map +1 -0
- package/dist/schema-error-report.js +356 -0
- package/dist/schema-projection.d.ts +106 -4
- package/dist/schema-projection.d.ts.map +1 -1
- package/dist/schema-projection.js +206 -18
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-cel-context.d.ts +1 -17
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +5 -70
- package/dist/validate-nested-inline.d.ts +22 -1
- package/dist/validate-nested-inline.d.ts.map +1 -1
- package/dist/validate-nested-inline.js +17 -9
- package/dist/validate-reference-forms.d.ts +10 -0
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +23 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +9 -2
- package/dist/validate-referrer-rules.d.ts +38 -4
- package/dist/validate-referrer-rules.d.ts.map +1 -1
- package/dist/validate-referrer-rules.js +139 -31
- package/dist/validate-resource-rules.d.ts +11 -2
- package/dist/validate-resource-rules.d.ts.map +1 -1
- package/dist/validate-resource-rules.js +18 -12
- package/dist/validate-schema-projection.d.ts.map +1 -1
- package/dist/validate-schema-projection.js +33 -2
- package/dist/validate-step-inputs.js +11 -3
- package/dist/with-synthetic-positions.d.ts +1 -1
- package/dist/with-synthetic-positions.js +1 -1
- package/package.json +3 -3
- package/src/analyzer.ts +211 -68
- package/src/call-graph.ts +7 -1
- package/src/cel-environment.ts +48 -19
- package/src/cel-scope.ts +24 -6
- package/src/definition-registry.ts +90 -11
- package/src/eval-paths.ts +152 -0
- package/src/flatten-for-analyzer.ts +1 -1
- package/src/import-resolution-diagnostics.ts +1 -1
- package/src/index.ts +55 -6
- package/src/manifest-visitor.ts +8 -1
- package/src/migrations/entries/index.ts +2 -0
- package/src/migrations/entries/schema-prepare-bucket.json +27 -0
- package/src/peer-binding.ts +641 -0
- package/src/ref-slot.ts +36 -0
- package/src/reference-field-map.ts +42 -0
- package/src/referrer-rule.ts +20 -1
- package/src/release/index.ts +1 -1
- package/src/resource-rule.ts +69 -7
- package/src/rule-condition.ts +15 -0
- package/src/schema-compat.ts +92 -79
- package/src/schema-error-report.ts +417 -0
- package/src/schema-projection.ts +283 -20
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +6 -71
- package/src/validate-nested-inline.ts +35 -14
- package/src/validate-reference-forms.ts +22 -0
- package/src/validate-references.ts +15 -2
- package/src/validate-referrer-rules.ts +215 -28
- package/src/validate-resource-rules.ts +26 -12
- package/src/validate-schema-projection.ts +50 -1
- package/src/validate-step-inputs.ts +10 -4
- package/src/with-synthetic-positions.ts +1 -1
package/src/analyzer.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
buildCelEnvironment,
|
|
21
21
|
buildImportInputCelEnvironment,
|
|
22
22
|
buildTypedCelEnvironment,
|
|
23
|
+
isKindDocument,
|
|
23
24
|
type CelHandlers,
|
|
24
25
|
} from "./cel-environment.js";
|
|
25
26
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
@@ -78,7 +79,7 @@ import {
|
|
|
78
79
|
type ResourceRuleDiagnostic,
|
|
79
80
|
type ResourceRuleIssue,
|
|
80
81
|
} from "./validate-resource-rules.js";
|
|
81
|
-
import { readResourceRules, type ResourceRule } from "./resource-rule.js";
|
|
82
|
+
import { pointerToPath, readResourceRules, type ResourceRule } from "./resource-rule.js";
|
|
82
83
|
import { readReferrerRules, type ReferrerRule } from "./referrer-rule.js";
|
|
83
84
|
import {
|
|
84
85
|
evaluateReferrerRules,
|
|
@@ -87,11 +88,15 @@ import {
|
|
|
87
88
|
reportUnexercisedReferrerRule,
|
|
88
89
|
validateReferrerRuleDeclarations,
|
|
89
90
|
type Referrer,
|
|
91
|
+
type ReferrerRuleContext,
|
|
90
92
|
type ReferrerRuleDiagnostic,
|
|
91
93
|
type ReferrerRuleIssue,
|
|
92
94
|
} from "./validate-referrer-rules.js";
|
|
95
|
+
import { analyzerPeerBinder, analyzerPeersTarget } from "./peer-binding.js";
|
|
93
96
|
import {
|
|
94
97
|
describeProjectionFailure,
|
|
98
|
+
manifestListScope,
|
|
99
|
+
resolveSchemaProjections,
|
|
95
100
|
type ProjectionFailure,
|
|
96
101
|
} from "./schema-projection.js";
|
|
97
102
|
import {
|
|
@@ -118,14 +123,18 @@ import { collectValueSchemaIssues } from "./validate-value-schema.js";
|
|
|
118
123
|
import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisOptions } from "./types.js";
|
|
119
124
|
import {
|
|
120
125
|
extractAccessChains,
|
|
121
|
-
extractCelRegionScopes,
|
|
122
126
|
extractContextsFromSchema,
|
|
123
127
|
getManifestItem,
|
|
124
|
-
pathMatchesScope,
|
|
125
128
|
resolveContextAnnotations,
|
|
126
129
|
resolveTypeFieldToSchema,
|
|
127
130
|
} from "./validate-cel-context.js";
|
|
128
|
-
import {
|
|
131
|
+
import {
|
|
132
|
+
celEvalModeAt,
|
|
133
|
+
celEvalSites,
|
|
134
|
+
mergeCelEvalSites,
|
|
135
|
+
NO_CEL_EVAL_SITES,
|
|
136
|
+
type CelEvalSites,
|
|
137
|
+
} from "./eval-paths.js";
|
|
129
138
|
import {
|
|
130
139
|
BINDINGS_ANNOTATION,
|
|
131
140
|
bindingContextProperties,
|
|
@@ -706,7 +715,7 @@ export class StaticAnalyzer {
|
|
|
706
715
|
* `DUPLICATE_RESOURCE_NAME` reads those fields to tell a pipeline echo
|
|
707
716
|
* apart from a genuine collision, and downstream diagnostic positioning
|
|
708
717
|
* depends on them too. Real callers stamp positions already (the `Loader`,
|
|
709
|
-
* `flattenForAnalyzer`,
|
|
718
|
+
* `flattenForAnalyzer`, telo studio's `emitDocsFor`, the VSCode
|
|
710
719
|
* extension). Programmatic callers — tests, ad-hoc scripts — should pass
|
|
711
720
|
* their inputs through `withSyntheticPositions(...)` before calling
|
|
712
721
|
* `analyze()`. A missing position throws a clear error rather than
|
|
@@ -941,6 +950,11 @@ export class StaticAnalyzer {
|
|
|
941
950
|
const projectionIssues: SchemaProjectionIssue[] = [];
|
|
942
951
|
const resourceRuleIssues: ResourceRuleIssue[] = [];
|
|
943
952
|
const referrerRuleIssues: ReferrerRuleIssue[] = [];
|
|
953
|
+
/** Definition docs of the entry's own modules that may declare rules. Their
|
|
954
|
+
* referrer-rule declarations are checked after every kind is registered,
|
|
955
|
+
* because a `peers:` pointer is checked against the REFERRER kind's schema
|
|
956
|
+
* and that kind may be declared later in the same file. */
|
|
957
|
+
const ownRuleDeclarers: ResourceManifest[] = [];
|
|
944
958
|
// A rule that never had anything to iterate is never proven — the second way
|
|
945
959
|
// coverage varies invisibly, beside the dynamic-leaf skip. Tracked across the
|
|
946
960
|
// whole run and reported once, since "empty on every resource" is not a fact
|
|
@@ -998,9 +1012,10 @@ export class StaticAnalyzer {
|
|
|
998
1012
|
effectiveAuthorSchema(m as any, (k) => defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k)),
|
|
999
1013
|
),
|
|
1000
1014
|
);
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1015
|
+
// Deferred to after the registration loop: the `peers:` half is checked
|
|
1016
|
+
// against the REFERRER kind's schema, and a rule may name a kind
|
|
1017
|
+
// declared later in the same file.
|
|
1018
|
+
ownRuleDeclarers.push(m as unknown as ResourceManifest);
|
|
1004
1019
|
for (const rule of readReferrerRules((m as Record<string, unknown>).schema)) {
|
|
1005
1020
|
referrerRuleExercise.set(`${m.metadata?.module}.${m.metadata?.name}#${rule.code}`, {
|
|
1006
1021
|
manifest: m as unknown as ResourceManifest,
|
|
@@ -1056,6 +1071,25 @@ export class StaticAnalyzer {
|
|
|
1056
1071
|
defs.register(normalized);
|
|
1057
1072
|
}
|
|
1058
1073
|
|
|
1074
|
+
/**
|
|
1075
|
+
* What a peer rule's `peers:` pointer names in the kind its `referrer:`
|
|
1076
|
+
* filters to.
|
|
1077
|
+
*
|
|
1078
|
+
* Liskov in BOTH directions, which is what the check has to be: the filter
|
|
1079
|
+
* is usually an abstract (`Sql.Schema`, so one rule serves every backend)
|
|
1080
|
+
* while the collection is declared by the backends that implement it, so a
|
|
1081
|
+
* pointer resolving on any candidate resolves the rule. Reported only when
|
|
1082
|
+
* NO candidate declares it, or when every candidate that does holds plain
|
|
1083
|
+
* data — the two shapes where the rule would see no declaration at all.
|
|
1084
|
+
*/
|
|
1085
|
+
const peersTarget = analyzerPeersTarget(defs);
|
|
1086
|
+
|
|
1087
|
+
if (!options?.skipValidation) {
|
|
1088
|
+
for (const declarer of ownRuleDeclarers) {
|
|
1089
|
+
referrerRuleIssues.push(...validateReferrerRuleDeclarations(declarer, { peersTarget }));
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1059
1093
|
// Reference-form validation — enforce `!ref` as the only reference shape.
|
|
1060
1094
|
// Runs on the RAW manifests, BEFORE inline extraction and sentinel
|
|
1061
1095
|
// resolution, while an author-written `{kind, name}` is still
|
|
@@ -1476,6 +1510,17 @@ export class StaticAnalyzer {
|
|
|
1476
1510
|
diagnostics.push(...validateIncludePlacement(allManifests));
|
|
1477
1511
|
}
|
|
1478
1512
|
resolveSchemaTypeRefs(allManifests, aliases, aliasesByModule);
|
|
1513
|
+
// ...and over the manifests the DEFINITION REGISTRY holds, which are not
|
|
1514
|
+
// these. `normalizeInlineResources` deep-clones — that clone is the
|
|
1515
|
+
// analyzer's immutability boundary — while `defs.register` ran before it, on
|
|
1516
|
+
// the originals. So a kind whose `schema:` references a named shape kept the
|
|
1517
|
+
// authored `telo://Self/<Type>` spelling everywhere the registry is read,
|
|
1518
|
+
// which is where a resource's configuration is validated: the schema failed
|
|
1519
|
+
// to compile, the failure was swallowed, and `telo check` reported nothing
|
|
1520
|
+
// about a resource the kernel then rejected at boot. The pass is idempotent
|
|
1521
|
+
// — a canonical id parses as no authority and is left alone — so running it
|
|
1522
|
+
// over both is the whole repair.
|
|
1523
|
+
resolveSchemaTypeRefs(manifests, aliases, aliasesByModule);
|
|
1479
1524
|
|
|
1480
1525
|
// Trusted-input fast path: when the caller has already attested that
|
|
1481
1526
|
// this exact manifest set passes analysis (e.g. via the kernel's
|
|
@@ -1487,13 +1532,12 @@ export class StaticAnalyzer {
|
|
|
1487
1532
|
return suppressUnreadableModuleDiagnostics(diagnostics, unreadableFiles);
|
|
1488
1533
|
}
|
|
1489
1534
|
|
|
1490
|
-
//
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1535
|
+
// ONE binder for the whole run, built beside the binding it serves: it caches
|
|
1536
|
+
// each referrer's resolved collection, which is what keeps a rule over an
|
|
1537
|
+
// n-entry collection from re-resolving that collection once per entry.
|
|
1538
|
+
const referrerRuleContext: ReferrerRuleContext = {
|
|
1539
|
+
peerBinder: analyzerPeerBinder(defs, aliases, allManifests as ResourceManifest[]),
|
|
1540
|
+
};
|
|
1497
1541
|
|
|
1498
1542
|
// Fail loud on definition schemas AJV cannot compile. `validateAgainstSchema`
|
|
1499
1543
|
// and `validateWithRefs` swallow compile failures (returning no issues),
|
|
@@ -1640,6 +1684,54 @@ export class StaticAnalyzer {
|
|
|
1640
1684
|
observedStateContext,
|
|
1641
1685
|
});
|
|
1642
1686
|
|
|
1687
|
+
/**
|
|
1688
|
+
* The schema a resource of this kind is validated against, per DEFINITION.
|
|
1689
|
+
*
|
|
1690
|
+
* Both halves derive a fresh object — `effectiveAuthorSchema` merges along
|
|
1691
|
+
* `extends`, and the closed-schema branch spreads `kind` / `metadata` in —
|
|
1692
|
+
* so asking per resource handed `validateResourceConfig` a schema object it
|
|
1693
|
+
* had never seen. That registry memoizes its compiled AJV validator by
|
|
1694
|
+
* object IDENTITY, precisely because every resource of a kind is checked
|
|
1695
|
+
* against the same one at keystroke time, so a fresh object per resource
|
|
1696
|
+
* recompiled the whole kind schema per resource. Keyed on the definition,
|
|
1697
|
+
* which is stable for the run.
|
|
1698
|
+
*/
|
|
1699
|
+
const authorSchemaCache = new WeakMap<object, Record<string, any>>();
|
|
1700
|
+
const validationSchemaFor = (def: ResourceDefinition): Record<string, any> => {
|
|
1701
|
+
const cached = authorSchemaCache.get(def as unknown as object);
|
|
1702
|
+
if (cached) return cached;
|
|
1703
|
+
const authorSchema = effectiveAuthorSchema(def, (k) =>
|
|
1704
|
+
defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k),
|
|
1705
|
+
);
|
|
1706
|
+
// `kind` and `metadata` are implicit on every resource — inject them so
|
|
1707
|
+
// module authors don't have to repeat them under `additionalProperties:
|
|
1708
|
+
// false`.
|
|
1709
|
+
const schema =
|
|
1710
|
+
authorSchema.additionalProperties === false
|
|
1711
|
+
? {
|
|
1712
|
+
...authorSchema,
|
|
1713
|
+
properties: {
|
|
1714
|
+
kind: { type: "string" },
|
|
1715
|
+
metadata: { type: "object" },
|
|
1716
|
+
...authorSchema.properties,
|
|
1717
|
+
},
|
|
1718
|
+
}
|
|
1719
|
+
: authorSchema;
|
|
1720
|
+
authorSchemaCache.set(def as unknown as object, schema);
|
|
1721
|
+
return schema;
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
// One scope for the whole run: it closes over the manifest list and the
|
|
1725
|
+
// registry, neither of which changes per resource, and it is asked once per
|
|
1726
|
+
// schema-valued slot that carries a projection.
|
|
1727
|
+
const resourceProjectionScope = manifestListScope(
|
|
1728
|
+
allManifests as Record<string, any>[],
|
|
1729
|
+
(kind) =>
|
|
1730
|
+
(defs.resolve(kind) ?? defs.resolve(aliases.resolveKind(kind) ?? kind)) as
|
|
1731
|
+
| Record<string, any>
|
|
1732
|
+
| undefined,
|
|
1733
|
+
);
|
|
1734
|
+
|
|
1643
1735
|
// Validate each non-definition, non-system resource
|
|
1644
1736
|
for (const m of allManifests) {
|
|
1645
1737
|
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
@@ -1738,35 +1830,55 @@ export class StaticAnalyzer {
|
|
|
1738
1830
|
}
|
|
1739
1831
|
|
|
1740
1832
|
// Validate resource config against the definition's AUTHOR-FACING schema —
|
|
1741
|
-
// inheritance-resolved
|
|
1742
|
-
//
|
|
1743
|
-
//
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
// authors don't have to repeat them when using additionalProperties: false.
|
|
1747
|
-
const authorSchema = effectiveAuthorSchema(definition, (k) =>
|
|
1748
|
-
defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k),
|
|
1749
|
-
);
|
|
1750
|
-
if (authorSchema && Object.keys(authorSchema).length > 0) {
|
|
1751
|
-
const schema =
|
|
1752
|
-
authorSchema.additionalProperties === false
|
|
1753
|
-
? {
|
|
1754
|
-
...authorSchema,
|
|
1755
|
-
properties: {
|
|
1756
|
-
kind: { type: "string" },
|
|
1757
|
-
metadata: { type: "object" },
|
|
1758
|
-
...authorSchema.properties,
|
|
1759
|
-
},
|
|
1760
|
-
}
|
|
1761
|
-
: authorSchema;
|
|
1833
|
+
// inheritance-resolved, with `kind` / `metadata` injected. See
|
|
1834
|
+
// `validationSchemaFor`, which is where both derivations and the reason
|
|
1835
|
+
// they are memoized per definition live.
|
|
1836
|
+
const schema = validationSchemaFor(definition);
|
|
1837
|
+
if (Object.keys(schema).length > 0) {
|
|
1762
1838
|
// Phase 1: collect the pure-CEL leaves and the schema of the slot each
|
|
1763
1839
|
// flows into. The expression's own type is resolved later, by the
|
|
1764
1840
|
// engine walk that owns type-checking; this half only knows the target.
|
|
1765
1841
|
for (const slot of collectCelValueSlots(m, schema, "")) {
|
|
1766
1842
|
celReturnSlots.push({ manifest: m, resource, filePath, ...slot });
|
|
1767
1843
|
}
|
|
1768
|
-
//
|
|
1769
|
-
|
|
1844
|
+
// A kind's own schema may point a slot at a PROJECTION — of a resource it
|
|
1845
|
+
// references, or (the empty pointer) of this very declaration, which is
|
|
1846
|
+
// what types a table's seed rows against its own `columns:`. Resolved
|
|
1847
|
+
// PER RESOURCE, because a projection is declaration-derived: the same
|
|
1848
|
+
// kind schema yields a different row shape for every table.
|
|
1849
|
+
//
|
|
1850
|
+
// A failure here is REPORTED, never dropped. An entry that could not be
|
|
1851
|
+
// read vanishes from the projected shape, so the slot typed from it then
|
|
1852
|
+
// rejects a field the author did declare ("'status' is not allowed") with
|
|
1853
|
+
// nothing saying why — the projection's own failure mode, blaming the
|
|
1854
|
+
// wrong line. Entry-module-scoped, like every other schema issue.
|
|
1855
|
+
const projectionFailures: ProjectionFailure[] = [];
|
|
1856
|
+
const projected = resolveSchemaProjections(
|
|
1857
|
+
schema,
|
|
1858
|
+
m as Record<string, any>,
|
|
1859
|
+
resourceProjectionScope,
|
|
1860
|
+
projectionFailures,
|
|
1861
|
+
) as Record<string, any>;
|
|
1862
|
+
if (!ownModule || rootModules.has(ownModule)) {
|
|
1863
|
+
for (const failure of projectionFailures) {
|
|
1864
|
+
diagnostics.push({
|
|
1865
|
+
severity: DiagnosticSeverity.Error,
|
|
1866
|
+
code: "SCHEMA_PROJECTION_FROM_UNRESOLVED",
|
|
1867
|
+
source: SOURCE,
|
|
1868
|
+
message: `${m.kind}/${resource.name}: ${describeProjectionFailure(failure)}`,
|
|
1869
|
+
data: { resource, filePath, path: pointerToPath(failure.pointer) },
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
// Phase 2+3: AJV on substituted data — CEL fields replaced with typed
|
|
1874
|
+
// placeholders. Through the REGISTRY, so a kind whose schema references
|
|
1875
|
+
// a shape declared elsewhere is checked on the instance that holds it.
|
|
1876
|
+
const ajvIssues = defs.validateResourceConfig(
|
|
1877
|
+
substituteCelFields(m, projected, undefined, {
|
|
1878
|
+
external: (ref) => defs.schemaForId(ref),
|
|
1879
|
+
}),
|
|
1880
|
+
projected,
|
|
1881
|
+
);
|
|
1770
1882
|
// Phase 4: value slots that must satisfy a type declared elsewhere on
|
|
1771
1883
|
// the resource (`x-telo-value-schema-from`) — e.g. every row of a
|
|
1772
1884
|
// decision table against its declared `outputType`, so a mistyped branch
|
|
@@ -1800,12 +1912,12 @@ export class StaticAnalyzer {
|
|
|
1800
1912
|
for (const report of reportResourceRules(
|
|
1801
1913
|
m as unknown as ResourceManifest,
|
|
1802
1914
|
definition as unknown as ResourceManifest,
|
|
1803
|
-
evaluateResourceRules(m as unknown as ResourceManifest,
|
|
1915
|
+
evaluateResourceRules(m as unknown as ResourceManifest, schema),
|
|
1804
1916
|
!ruleDeclarer || rootModules.has(ruleDeclarer),
|
|
1805
1917
|
)) {
|
|
1806
1918
|
diagnostics.push(resourceRuleDiagnostic(report));
|
|
1807
1919
|
}
|
|
1808
|
-
for (const rule of readResourceRules(
|
|
1920
|
+
for (const rule of readResourceRules(schema)) {
|
|
1809
1921
|
const key = `${definition.metadata?.module}.${definition.metadata?.name}#${rule.code}`;
|
|
1810
1922
|
const tracked = ruleExercise.get(key);
|
|
1811
1923
|
if (!tracked) continue;
|
|
@@ -1819,7 +1931,7 @@ export class StaticAnalyzer {
|
|
|
1819
1931
|
// literal appears on the referring side, where the spelling would be the
|
|
1820
1932
|
// consumer's import alias rather than anything the rule's author controls.
|
|
1821
1933
|
// A consumer of the shared call graph, never a second traversal.
|
|
1822
|
-
const referrerRules = readReferrerRules(
|
|
1934
|
+
const referrerRules = readReferrerRules(schema);
|
|
1823
1935
|
if (referrerRules.length > 0) {
|
|
1824
1936
|
const referrers = referrersOf(m as unknown as ResourceManifest, getCallGraph());
|
|
1825
1937
|
for (const report of reportReferrerRules(
|
|
@@ -1827,9 +1939,10 @@ export class StaticAnalyzer {
|
|
|
1827
1939
|
definition as unknown as ResourceManifest,
|
|
1828
1940
|
evaluateReferrerRules(
|
|
1829
1941
|
m as unknown as ResourceManifest,
|
|
1830
|
-
|
|
1942
|
+
schema,
|
|
1831
1943
|
referrers,
|
|
1832
1944
|
kindMatches,
|
|
1945
|
+
referrerRuleContext,
|
|
1833
1946
|
),
|
|
1834
1947
|
!ruleDeclarer || rootModules.has(ruleDeclarer),
|
|
1835
1948
|
)) {
|
|
@@ -1841,13 +1954,25 @@ export class StaticAnalyzer {
|
|
|
1841
1954
|
continue;
|
|
1842
1955
|
}
|
|
1843
1956
|
diagnostics.push(referrerRuleDiagnostic(report));
|
|
1957
|
+
// A rule that VIOLATED plainly ran, whatever its peer set held. The
|
|
1958
|
+
// unexercised report exists to catch silence — a `peers:` naming
|
|
1959
|
+
// nothing makes every `!peers.exists(…)` pass vacuously — so a finding
|
|
1960
|
+
// is exactly the evidence it asks for.
|
|
1961
|
+
if (report.code === "REFERRER_RULE_VIOLATED") {
|
|
1962
|
+
const violated = referrerRuleExercise.get(
|
|
1963
|
+
`${definition.metadata?.module}.${definition.metadata?.name}#${report.rule}`,
|
|
1964
|
+
);
|
|
1965
|
+
if (violated) violated.exercised = true;
|
|
1966
|
+
}
|
|
1844
1967
|
}
|
|
1845
1968
|
for (const rule of referrerRules) {
|
|
1846
1969
|
const key = `${definition.metadata?.module}.${definition.metadata?.name}#${rule.code}`;
|
|
1847
1970
|
const tracked = referrerRuleExercise.get(key);
|
|
1848
1971
|
if (!tracked) continue;
|
|
1849
1972
|
tracked.seen = true;
|
|
1850
|
-
if (referrerRuleExercised(rule, referrers, kindMatches))
|
|
1973
|
+
if (referrerRuleExercised(rule, referrers, kindMatches, referrerRuleContext)) {
|
|
1974
|
+
tracked.exercised = true;
|
|
1975
|
+
}
|
|
1851
1976
|
}
|
|
1852
1977
|
}
|
|
1853
1978
|
|
|
@@ -1879,6 +2004,10 @@ export class StaticAnalyzer {
|
|
|
1879
2004
|
return viaRoot ? defs.resolve(viaRoot) : undefined;
|
|
1880
2005
|
},
|
|
1881
2006
|
allManifests as Record<string, any>[],
|
|
2007
|
+
{
|
|
2008
|
+
validate: (data, target) => defs.validateResourceConfig(data, target),
|
|
2009
|
+
external: (ref) => defs.schemaForId(ref),
|
|
2010
|
+
},
|
|
1882
2011
|
),
|
|
1883
2012
|
);
|
|
1884
2013
|
}
|
|
@@ -2045,18 +2174,15 @@ export class StaticAnalyzer {
|
|
|
2045
2174
|
// same schema an expression is typed against.
|
|
2046
2175
|
let celStepContextSchema: Record<string, any> | undefined;
|
|
2047
2176
|
let celErrorScopes: ReadonlyMap<string, Record<string, any>> = new Map();
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2050
|
-
//
|
|
2051
|
-
//
|
|
2052
|
-
|
|
2177
|
+
// Where this kind says its values are evaluated — `x-telo-eval` paths (own +
|
|
2178
|
+
// capability) and the regions covering their contents. A `!cel` outside every
|
|
2179
|
+
// one of them is read as a literal, and one under a compile path resolves at
|
|
2180
|
+
// startup, where observed state cannot exist yet. Both questions are
|
|
2181
|
+
// `celEvalModeAt`, which the editor asks of the same sites.
|
|
2182
|
+
let celSites: CelEvalSites = NO_CEL_EVAL_SITES;
|
|
2053
2183
|
// The bindings field this kind declares (if any), read by the CEL sites that
|
|
2054
2184
|
// see the names it introduces.
|
|
2055
2185
|
let celBindingSites: BindingSites | undefined;
|
|
2056
|
-
// The compile half alone: a field that resolves at startup, where observed
|
|
2057
|
-
// state cannot exist yet.
|
|
2058
|
-
let celCompilePaths: string[] = [];
|
|
2059
|
-
let celRegionScopes: string[] = [];
|
|
2060
2186
|
let celRuleApplies = false;
|
|
2061
2187
|
|
|
2062
2188
|
visitManifest(
|
|
@@ -2211,22 +2337,28 @@ export class StaticAnalyzer {
|
|
|
2211
2337
|
celRuleApplies =
|
|
2212
2338
|
!!e.definition?.schema && capability !== undefined && capability !== "Telo.Template";
|
|
2213
2339
|
if (celRuleApplies) {
|
|
2214
|
-
|
|
2215
|
-
|
|
2340
|
+
// The INHERITANCE-RESOLVED schema, not the kind's own: without
|
|
2341
|
+
// `base:`, an `extends` child is authored against merge(parent, own),
|
|
2342
|
+
// so a CEL-bearing field the parent declares is a legal field on the
|
|
2343
|
+
// child and the kernel expands it — it stamps the merged schema at
|
|
2344
|
+
// definition registration and builds eval paths from that. Reading
|
|
2345
|
+
// the own schema here reported `CEL_IN_NON_EVAL_FIELD` for an
|
|
2346
|
+
// expression the runtime evaluates correctly, which is the two halves
|
|
2347
|
+
// disagreeing about what the manifest means.
|
|
2348
|
+
const ownSchema = effectiveAuthorSchema(
|
|
2349
|
+
e.definition as unknown as ResourceDefinition,
|
|
2350
|
+
(k) => defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k),
|
|
2351
|
+
) as Record<string, any>;
|
|
2216
2352
|
const capabilityDef = capability ? defs.resolve(capability) : undefined;
|
|
2217
|
-
const parent = capabilityDef?.schema
|
|
2218
|
-
? buildEvalPaths(capabilityDef.schema as Record<string, any>)
|
|
2219
|
-
: { compile: [], runtime: [] };
|
|
2220
|
-
celEvalPaths = [...own.compile, ...own.runtime, ...parent.compile, ...parent.runtime];
|
|
2221
2353
|
// A `Telo.Provider`'s fields are implicitly compile-eval — the
|
|
2222
2354
|
// capability abstract carries the root annotation — so its reads are
|
|
2223
2355
|
// covered here without the provider restating anything.
|
|
2224
|
-
|
|
2225
|
-
|
|
2356
|
+
celSites = mergeCelEvalSites(
|
|
2357
|
+
celEvalSites(ownSchema),
|
|
2358
|
+
celEvalSites(capabilityDef?.schema as Record<string, any> | undefined),
|
|
2359
|
+
);
|
|
2226
2360
|
} else {
|
|
2227
|
-
|
|
2228
|
-
celCompilePaths = [];
|
|
2229
|
-
celRegionScopes = [];
|
|
2361
|
+
celSites = NO_CEL_EVAL_SITES;
|
|
2230
2362
|
}
|
|
2231
2363
|
},
|
|
2232
2364
|
onCel: (e) => {
|
|
@@ -2244,8 +2376,7 @@ export class StaticAnalyzer {
|
|
|
2244
2376
|
celRuleApplies &&
|
|
2245
2377
|
engineName === "cel" &&
|
|
2246
2378
|
celScope.invocationContextSchema === undefined &&
|
|
2247
|
-
|
|
2248
|
-
!celRegionScopes.some((scope) => pathMatchesScope(path, scope)) &&
|
|
2379
|
+
celEvalModeAt(celSites, path) === null &&
|
|
2249
2380
|
!pathCrossesNestedResource(m, path)
|
|
2250
2381
|
) {
|
|
2251
2382
|
diagnostics.push({
|
|
@@ -2273,7 +2404,7 @@ export class StaticAnalyzer {
|
|
|
2273
2404
|
read.alias ? `${read.alias}.${read.name}` : read.name,
|
|
2274
2405
|
);
|
|
2275
2406
|
|
|
2276
|
-
if (celRuleApplies &&
|
|
2407
|
+
if (celRuleApplies && celEvalModeAt(celSites, path) === "compile") {
|
|
2277
2408
|
diagnostics.push({
|
|
2278
2409
|
severity: DiagnosticSeverity.Error,
|
|
2279
2410
|
code: "OBSERVED_STATE_IN_STARTUP_FIELD",
|
|
@@ -2320,7 +2451,19 @@ export class StaticAnalyzer {
|
|
|
2320
2451
|
matchedScope,
|
|
2321
2452
|
});
|
|
2322
2453
|
|
|
2323
|
-
const result = engine.analyze(expr, {
|
|
2454
|
+
const result = engine.analyze(expr, {
|
|
2455
|
+
celEnv: typedEnv,
|
|
2456
|
+
contextSchema: effectiveContext,
|
|
2457
|
+
// `scopeFor` registers every kernel global and every name the site's
|
|
2458
|
+
// context declares, so a root this environment does not know is one
|
|
2459
|
+
// nothing puts in scope. Two places where the CEL belongs to another
|
|
2460
|
+
// scope, both already recognised by the non-eval-field check: a kind
|
|
2461
|
+
// document, whose CEL is written for whoever instantiates the kind,
|
|
2462
|
+
// and anything below a nested inline `{ kind }`, whose CEL the
|
|
2463
|
+
// nested kind evaluates — and which is analyzed again, in its own
|
|
2464
|
+
// scope, as the resource it was extracted into.
|
|
2465
|
+
rootsDeclared: !isKindDocument(m) && !pathCrossesNestedResource(m, path),
|
|
2466
|
+
});
|
|
2324
2467
|
|
|
2325
2468
|
if (result.type !== undefined) {
|
|
2326
2469
|
let byPath = celTypeByPath.get(m);
|
|
@@ -2357,7 +2500,7 @@ export class StaticAnalyzer {
|
|
|
2357
2500
|
// intent (a boot timestamp, a run id), so it warns rather than
|
|
2358
2501
|
// blocking. The engine reports which calls re-evaluate; the eval mode
|
|
2359
2502
|
// is manifest policy and stays here.
|
|
2360
|
-
if (celRuleApplies &&
|
|
2503
|
+
if (celRuleApplies && celEvalModeAt(celSites, path) === "compile") {
|
|
2361
2504
|
const volatile = [
|
|
2362
2505
|
...new Set(result.calls.filter((c) => c.deterministic === false).map((c) => c.name)),
|
|
2363
2506
|
].sort();
|
package/src/call-graph.ts
CHANGED
|
@@ -284,7 +284,13 @@ function resolveUseAtSite(
|
|
|
284
284
|
const resolved = entry.useCases.cases[String(selector)];
|
|
285
285
|
if (resolved) return { use: resolved };
|
|
286
286
|
}
|
|
287
|
-
const slot = {
|
|
287
|
+
const slot = {
|
|
288
|
+
kinds: entry.refs,
|
|
289
|
+
uses: entry.uses,
|
|
290
|
+
useCases: entry.useCases,
|
|
291
|
+
inline: false,
|
|
292
|
+
valueBranches: [],
|
|
293
|
+
};
|
|
288
294
|
const unresolvedReason = isTaggedSentinel(selector)
|
|
289
295
|
? "dynamic"
|
|
290
296
|
: selector === undefined
|
package/src/cel-environment.ts
CHANGED
|
@@ -47,24 +47,34 @@ export function buildTypedCelEnvironment(
|
|
|
47
47
|
(env as any).registerType(brand, { fields: {} });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
// `variables` / `secrets`: the DECLARING module's blocks, which is the
|
|
51
|
+
// contract the resource's CEL is evaluated against at runtime. Read the
|
|
52
|
+
// same three ways `ports` and `module` are, and for the same reason — a
|
|
53
|
+
// resource doc does not carry them, so typing from `manifest` alone left
|
|
54
|
+
// every ordinary resource with an open `variables` and no check at all,
|
|
55
|
+
// while `ports.<typo>` one line away was an error.
|
|
56
|
+
//
|
|
57
|
+
// Order matters: a module-identity doc analyzing itself carries its own
|
|
58
|
+
// block; a resource forwarded from an imported library carries its
|
|
59
|
+
// library's as `metadata.moduleGlobals`, which must win over the consuming
|
|
60
|
+
// application's; everything else is the entry module's own doc.
|
|
61
|
+
const moduleGlobals = (manifest.metadata as Record<string, any> | undefined)?.moduleGlobals as
|
|
62
|
+
| Record<string, unknown>
|
|
63
|
+
| undefined;
|
|
64
|
+
// A KIND document is the exception, and it is not a detail: the CEL inside
|
|
65
|
+
// a `Telo.Definition`'s `schema:` — an `examples:` entry, a `description`
|
|
66
|
+
// showing `${{ secrets.API_KEY }}` — illustrates what a CONSUMER writes, in
|
|
67
|
+
// the consumer's scope. Closing those over the declaring module's blocks
|
|
68
|
+
// reported an error against a name the module never meant to declare, and
|
|
69
|
+
// one nobody could fix without deleting the example.
|
|
70
|
+
const root = (
|
|
71
|
+
isKindDocument(manifest) ? undefined : (rootModuleManifest as Record<string, unknown>)
|
|
72
|
+
) as Record<string, unknown> | undefined;
|
|
73
|
+
registerConfigNamespace(
|
|
74
|
+
env,
|
|
75
|
+
(manifest as Record<string, unknown>).variables ?? moduleGlobals?.variables ?? root?.variables,
|
|
76
|
+
"variables",
|
|
77
|
+
);
|
|
68
78
|
|
|
69
79
|
// `ports` namespace: each entry types as the brand its `protocol` selects
|
|
70
80
|
// (tcp → TcpPort, udp → UdpPort), so `${{ ports.http }}` carries a nominal
|
|
@@ -87,7 +97,11 @@ export function buildTypedCelEnvironment(
|
|
|
87
97
|
env.registerVariable("ports", "map");
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
registerConfigNamespace(
|
|
101
|
+
env,
|
|
102
|
+
(manifest as Record<string, unknown>).secrets ?? moduleGlobals?.secrets ?? root?.secrets,
|
|
103
|
+
"secrets",
|
|
104
|
+
);
|
|
91
105
|
env.registerVariable("resources", "map");
|
|
92
106
|
|
|
93
107
|
// `module` — the declaring module's own `metadata`, so a manifest reads its
|
|
@@ -132,6 +146,21 @@ export function buildTypedCelEnvironment(
|
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
148
|
|
|
149
|
+
/**
|
|
150
|
+
* A kind document — whose CEL is written for whoever instantiates the kind, not
|
|
151
|
+
* evaluated in the declaring module's own scope.
|
|
152
|
+
*
|
|
153
|
+
* Its `examples:` show a consumer's route reading `request` and `result`, its
|
|
154
|
+
* `description`s show `${{ secrets.API_KEY }}`, and a rule condition reads the
|
|
155
|
+
* `self` / `referrer` its own evaluator binds. None of those names are in scope
|
|
156
|
+
* where they are WRITTEN, and all of them are correct where they are READ — so
|
|
157
|
+
* every check that asks "is this name in scope here" has to stand down on these
|
|
158
|
+
* documents, or it reports errors nobody can fix without deleting the example.
|
|
159
|
+
*/
|
|
160
|
+
export function isKindDocument(manifest: ResourceManifest): boolean {
|
|
161
|
+
return manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract";
|
|
162
|
+
}
|
|
163
|
+
|
|
135
164
|
/** Register a `variables`/`secrets` namespace typed from a module doc's schema map
|
|
136
165
|
* (`{ name: <schema>, … }`), falling back to dyn `map` when absent or untyped. */
|
|
137
166
|
function registerConfigNamespace(
|
package/src/cel-scope.ts
CHANGED
|
@@ -445,19 +445,23 @@ export class CelScopeResolver {
|
|
|
445
445
|
this.invocationContext = (m.metadata as any)?.xTeloInvocationContext as
|
|
446
446
|
| Record<string, any>
|
|
447
447
|
| undefined;
|
|
448
|
-
|
|
448
|
+
// The INHERITANCE-RESOLVED schema at both sites, for the reason the CEL
|
|
449
|
+
// context regions use it: an `extends` child is authored against
|
|
450
|
+
// merge(parent, own), so a step body or a `catch:` region the parent
|
|
451
|
+
// declares is in force on the child. All three consumers share one memo so
|
|
452
|
+
// they cannot come to disagree about which schema a kind means.
|
|
453
|
+
const authorSchema = defs.effectiveSchemaOf(definition) as Record<string, any> | undefined;
|
|
454
|
+
this.stepContext = authorSchema
|
|
449
455
|
? buildStepContextSchema(
|
|
450
456
|
m as Record<string, any>,
|
|
451
|
-
|
|
457
|
+
authorSchema,
|
|
452
458
|
allManifests as Record<string, any>[],
|
|
453
459
|
defs,
|
|
454
460
|
aliases,
|
|
455
461
|
scopes,
|
|
456
462
|
)
|
|
457
463
|
: undefined;
|
|
458
|
-
this.errorScopes = collectErrorContextScopes(
|
|
459
|
-
definition?.schema as Record<string, any> | undefined,
|
|
460
|
-
);
|
|
464
|
+
this.errorScopes = collectErrorContextScopes(authorSchema);
|
|
461
465
|
}
|
|
462
466
|
|
|
463
467
|
/**
|
|
@@ -518,7 +522,21 @@ export class CelScopeResolver {
|
|
|
518
522
|
const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
|
|
519
523
|
matched = {
|
|
520
524
|
...base,
|
|
521
|
-
properties: {
|
|
525
|
+
properties: {
|
|
526
|
+
// `inputs` is in scope beside `steps` wherever a step body runs — the
|
|
527
|
+
// step engine evaluates a step against the enclosing kind's own
|
|
528
|
+
// arguments, which is what `steps[0].inputs.x` reads. Modelled here
|
|
529
|
+
// because it was modelled NOWHERE: the step context is open, so a
|
|
530
|
+
// chain through it was never checked and the name's absence went
|
|
531
|
+
// unnoticed until something asked whether the root existed.
|
|
532
|
+
//
|
|
533
|
+
// OPEN, not typed from the kind's `inputType`: closing it would newly
|
|
534
|
+
// reject every read of an argument the contract does not spell out,
|
|
535
|
+
// which is a separate decision from knowing the name is legal.
|
|
536
|
+
inputs: { type: "object", additionalProperties: true },
|
|
537
|
+
...(base.properties ?? {}),
|
|
538
|
+
steps: this.stepContext,
|
|
539
|
+
},
|
|
522
540
|
};
|
|
523
541
|
}
|
|
524
542
|
|