@telorun/analyzer 0.65.0 → 0.67.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 +194 -65
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +67 -16
- 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 +8 -0
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +89 -11
- package/dist/definition-registry.d.ts +41 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +76 -0
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +65 -0
- 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 +37 -1
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +104 -5
- package/dist/import-resolution-diagnostics.d.ts +1 -1
- package/dist/import-resolution-diagnostics.js +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/inline-imports.d.ts.map +1 -1
- package/dist/inline-imports.js +1 -0
- package/dist/manifest-visitor.d.ts +4 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +36 -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/precompile.d.ts.map +1 -1
- package/dist/precompile.js +8 -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/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +14 -1
- package/dist/resource-input.d.ts +75 -0
- package/dist/resource-input.d.ts.map +1 -0
- package/dist/resource-input.js +90 -0
- 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-projection.d.ts +119 -4
- package/dist/schema-projection.d.ts.map +1 -1
- package/dist/schema-projection.js +213 -18
- package/dist/system-kinds.d.ts +7 -2
- package/dist/system-kinds.d.ts.map +1 -1
- package/dist/system-kinds.js +7 -2
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts +50 -0
- package/dist/template-body.d.ts.map +1 -0
- package/dist/template-body.js +58 -0
- 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 +73 -78
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +17 -2
- 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 +17 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +77 -18
- 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-inputs.d.ts +35 -0
- package/dist/validate-resource-inputs.d.ts.map +1 -0
- package/dist/validate-resource-inputs.js +319 -0
- 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-template-dispatch.d.ts +27 -0
- package/dist/validate-template-dispatch.d.ts.map +1 -0
- package/dist/validate-template-dispatch.js +95 -0
- 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 +236 -70
- package/src/builtins.ts +69 -16
- package/src/call-graph.ts +7 -1
- package/src/cel-environment.ts +48 -19
- package/src/cel-scope.ts +113 -19
- package/src/definition-registry.ts +78 -0
- package/src/dependency-graph.ts +66 -0
- package/src/eval-paths.ts +152 -0
- package/src/flatten-for-analyzer.ts +117 -4
- package/src/import-resolution-diagnostics.ts +1 -1
- package/src/index.ts +57 -4
- package/src/inline-imports.ts +1 -0
- package/src/manifest-visitor.ts +41 -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/precompile.ts +8 -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/resolve-ref-sentinels.ts +12 -1
- package/src/resource-input.ts +132 -0
- package/src/resource-rule.ts +69 -7
- package/src/rule-condition.ts +15 -0
- package/src/schema-projection.ts +302 -20
- package/src/system-kinds.ts +7 -2
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +104 -0
- package/src/validate-cel-context.ts +73 -78
- package/src/validate-identifier-names.ts +18 -3
- package/src/validate-reference-forms.ts +22 -0
- package/src/validate-references.ts +85 -16
- package/src/validate-referrer-rules.ts +215 -28
- package/src/validate-resource-inputs.ts +367 -0
- package/src/validate-resource-rules.ts +26 -12
- package/src/validate-schema-projection.ts +50 -1
- package/src/validate-template-dispatch.ts +99 -0
- package/src/with-synthetic-positions.ts +1 -1
package/src/ref-slot.ts
CHANGED
|
@@ -97,6 +97,19 @@ export interface RefSlot {
|
|
|
97
97
|
/** `x-telo-inline: true` on the slot or any `anyOf` branch — accepts an inline
|
|
98
98
|
* `{kind, ...config}` definition, not only a `!ref`. */
|
|
99
99
|
inline: boolean;
|
|
100
|
+
/** The slot's VALUE branches — `anyOf` / `oneOf` alternatives that carry no
|
|
101
|
+
* `x-telo-ref`, present only when the reference constraint is itself a
|
|
102
|
+
* *branch* rather than the node's own annotation.
|
|
103
|
+
*
|
|
104
|
+
* That narrowing is the whole point. A node-level `x-telo-ref` with branches
|
|
105
|
+
* beneath it (an Application `targets` entry) uses those branches to describe
|
|
106
|
+
* the POST-RESOLUTION structural forms a reference takes — a bare string
|
|
107
|
+
* there is the removed string-reference spelling, and admitting it as a value
|
|
108
|
+
* would retire `INVALID_REFERENCE_FORM` exactly where it still applies. A
|
|
109
|
+
* branch-level constraint says something different: this slot holds either a
|
|
110
|
+
* value of one shape or a reference, and a scalar is then a value, not a
|
|
111
|
+
* malformed reference. */
|
|
112
|
+
valueBranches: Record<string, any>[];
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
/** Every use a slot can take, flattening a case map. The conservative reading
|
|
@@ -173,6 +186,28 @@ function annotationNodes(node: Record<string, any> | undefined): Record<string,
|
|
|
173
186
|
return out;
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
/** The branches of a value-or-reference union — see {@link RefSlot.valueBranches}.
|
|
190
|
+
* Empty unless the node delegates its reference constraint to a branch. */
|
|
191
|
+
function valueBranchesOf(node: Record<string, any> | undefined): Record<string, any>[] {
|
|
192
|
+
if (!node || typeof node !== "object") return [];
|
|
193
|
+
if (node["x-telo-ref"] !== undefined) return [];
|
|
194
|
+
const out: Record<string, any>[] = [];
|
|
195
|
+
for (const key of ["anyOf", "oneOf"] as const) {
|
|
196
|
+
const branches = node[key];
|
|
197
|
+
if (!Array.isArray(branches)) continue;
|
|
198
|
+
const carriesRef = branches.some(
|
|
199
|
+
(b) => b && typeof b === "object" && b["x-telo-ref"] !== undefined,
|
|
200
|
+
);
|
|
201
|
+
if (!carriesRef) continue;
|
|
202
|
+
for (const branch of branches) {
|
|
203
|
+
if (!branch || typeof branch !== "object") continue;
|
|
204
|
+
if (branch["x-telo-ref"] !== undefined) continue;
|
|
205
|
+
out.push(branch as Record<string, any>);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
176
211
|
/**
|
|
177
212
|
* Reads a schema node as a reference slot, or `undefined` when it declares none.
|
|
178
213
|
*
|
|
@@ -210,6 +245,7 @@ export function readRefSlot(node: Record<string, any> | undefined): RefSlot | un
|
|
|
210
245
|
kinds,
|
|
211
246
|
uses: [...uses],
|
|
212
247
|
inline: node?.["x-telo-inline"] === true || nodes.some((n) => n["x-telo-inline"] === true),
|
|
248
|
+
valueBranches: valueBranchesOf(node),
|
|
213
249
|
};
|
|
214
250
|
if (useCases) slot.useCases = useCases;
|
|
215
251
|
if (inputs !== undefined) slot.inputs = inputs;
|
|
@@ -19,6 +19,10 @@ export interface RefFieldEntry {
|
|
|
19
19
|
inputs?: string;
|
|
20
20
|
/** True when the field path traversed through at least one array (path contains "[]"). */
|
|
21
21
|
isArray: boolean;
|
|
22
|
+
/** The slot's non-reference branches, when the reference constraint is a
|
|
23
|
+
* branch of a union — see {@link RefSlot.valueBranches}. A value satisfying
|
|
24
|
+
* one of these is a value, not a malformed reference. */
|
|
25
|
+
valueBranches?: Record<string, any>[];
|
|
22
26
|
/** x-telo-context schema declared on this ref slot, if any. Describes the CEL invocation
|
|
23
27
|
* context available to resources placed in this slot. */
|
|
24
28
|
context?: Record<string, any>;
|
|
@@ -62,6 +66,43 @@ export function isRefEntry(entry: FieldMapEntry): entry is RefFieldEntry {
|
|
|
62
66
|
return "refs" in entry;
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
/** The half of a definition registry this question needs — structural, so the
|
|
70
|
+
* field map keeps depending on nothing. */
|
|
71
|
+
export interface ValueBranchValidator {
|
|
72
|
+
schemaCompileError(schema: Record<string, any>): string | undefined;
|
|
73
|
+
validateWithRefs(data: unknown, schema: Record<string, any>): string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* True when a value at a ref slot satisfies one of the slot's VALUE branches —
|
|
78
|
+
* a storage class beside a `!ref`, so it is a value and not a malformed
|
|
79
|
+
* reference.
|
|
80
|
+
*
|
|
81
|
+
* One implementation, because BOTH reference passes have to narrow the same way:
|
|
82
|
+
* `validateReferenceForms` would otherwise call it a removed string reference,
|
|
83
|
+
* and `validateReferences` a reference missing `kind` and `name`. Two copies of
|
|
84
|
+
* the rule would eventually disagree about which of the two reported a value.
|
|
85
|
+
*
|
|
86
|
+
* A branch AJV cannot COMPILE is not a branch the value satisfies.
|
|
87
|
+
* `validateWithRefs` returns no issues for one — it swallows the compile failure
|
|
88
|
+
* by design, so one bad schema does not abort the pass — and reading that as
|
|
89
|
+
* "no issues, therefore a value" would switch the reference-form rule off for
|
|
90
|
+
* the slot silently. The uncompilable schema is reported on its own definition
|
|
91
|
+
* by `schemaCompileError`.
|
|
92
|
+
*/
|
|
93
|
+
export function satisfiesValueBranch(
|
|
94
|
+
value: unknown,
|
|
95
|
+
branches: readonly Record<string, any>[] | undefined,
|
|
96
|
+
registry: ValueBranchValidator,
|
|
97
|
+
): boolean {
|
|
98
|
+
if (!branches?.length) return false;
|
|
99
|
+
return branches.some(
|
|
100
|
+
(branch) =>
|
|
101
|
+
registry.schemaCompileError(branch) === undefined &&
|
|
102
|
+
registry.validateWithRefs(value, branch).length === 0,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
65
106
|
export function isScopeEntry(entry: FieldMapEntry): entry is ScopeFieldEntry {
|
|
66
107
|
return "scope" in entry;
|
|
67
108
|
}
|
|
@@ -234,6 +275,7 @@ function traverseNode(
|
|
|
234
275
|
};
|
|
235
276
|
if (slot.useCases) entry.useCases = slot.useCases;
|
|
236
277
|
if (slot.inputs !== undefined) entry.inputs = slot.inputs;
|
|
278
|
+
if (slot.valueBranches.length > 0) entry.valueBranches = slot.valueBranches;
|
|
237
279
|
if (node["x-telo-context"]) entry.context = node["x-telo-context"] as Record<string, any>;
|
|
238
280
|
if (slot.inline) entry.inline = true;
|
|
239
281
|
map.set(path, entry);
|
package/src/referrer-rule.ts
CHANGED
|
@@ -37,6 +37,23 @@ export interface ReferrerRule {
|
|
|
37
37
|
* relation the rule is about, so a kind should write it.
|
|
38
38
|
*/
|
|
39
39
|
readonly referrer?: string;
|
|
40
|
+
/**
|
|
41
|
+
* JSON Pointer to a collection OF THE REFERRER to resolve — the binding that
|
|
42
|
+
* lets a rule state a relation between SIBLING declarations, which neither a
|
|
43
|
+
* resource rule (one resource) nor a plain referrer rule (a pair joined by one
|
|
44
|
+
* reference) can reach.
|
|
45
|
+
*
|
|
46
|
+
* Declaring it binds `peers` (the collection's OTHER entries) and `entry` (my
|
|
47
|
+
* own), and makes the rule evaluate once per entry rather than once per
|
|
48
|
+
* referrer: a rule reading `entry` is about the entry, and a resource listed
|
|
49
|
+
* twice has two entries to answer for.
|
|
50
|
+
*
|
|
51
|
+
* Entries bind AS WRITTEN with the references *inside them* resolved one level
|
|
52
|
+
* — `p` is the declaration where the entry is a bare `!ref`, `p.mount` is the
|
|
53
|
+
* declaration with `p.prefix` beside it where it is not. Nothing is guessed
|
|
54
|
+
* from the item schema and there is no second pointer to write.
|
|
55
|
+
*/
|
|
56
|
+
readonly peers?: string;
|
|
40
57
|
/** CEL source. TRUE when the rule holds. */
|
|
41
58
|
readonly condition: string;
|
|
42
59
|
/** The rule's own name, carried in `data.rule`. Never a diagnostic code —
|
|
@@ -68,15 +85,17 @@ export function readReferrerRules(schema: unknown): ReferrerRule[] {
|
|
|
68
85
|
raw.forEach((entry, index) => {
|
|
69
86
|
if (!isObject(entry)) return;
|
|
70
87
|
const condition = celSourceOf(entry.condition);
|
|
71
|
-
const { code, message, referrer } = entry;
|
|
88
|
+
const { code, message, referrer, peers } = entry;
|
|
72
89
|
if (!condition || typeof code !== "string" || typeof message !== "string") return;
|
|
73
90
|
if (code.length === 0 || message.length === 0) return;
|
|
74
91
|
if (referrer !== undefined && typeof referrer !== "string") return;
|
|
92
|
+
if (peers !== undefined && typeof peers !== "string") return;
|
|
75
93
|
if (entry.severity !== undefined && entry.severity !== "warning" && entry.severity !== "error") {
|
|
76
94
|
return;
|
|
77
95
|
}
|
|
78
96
|
rules.push({
|
|
79
97
|
...(referrer === undefined ? {} : { referrer }),
|
|
98
|
+
...(peers === undefined ? {} : { peers }),
|
|
80
99
|
condition,
|
|
81
100
|
code,
|
|
82
101
|
message,
|
package/src/release/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The release model: module identity, fragments, the ledger, the edge graph,
|
|
3
3
|
* level propagation and version planning.
|
|
4
4
|
*
|
|
5
|
-
* Browser-safe by construction — pure data in, plan out — so
|
|
5
|
+
* Browser-safe by construction — pure data in, plan out — so telo studio can
|
|
6
6
|
* answer "what does changing this library bump?" from the same model the CLI
|
|
7
7
|
* releases from. Everything Node-shaped (finding the workspace, running the
|
|
8
8
|
* controller builder, building payloads, reading git, writing files) is
|
|
@@ -193,7 +193,18 @@ export function resolveRefSentinels(
|
|
|
193
193
|
|
|
194
194
|
for (const r of resources) {
|
|
195
195
|
if (isForeign(r)) continue;
|
|
196
|
-
if (!r.metadata?.name || !r.kind
|
|
196
|
+
if (!r.metadata?.name || !r.kind) continue;
|
|
197
|
+
// A `Telo.Import` is import-time metadata, not a resource instance — except
|
|
198
|
+
// for its `resources:` block, which supplies the instances the target
|
|
199
|
+
// library declared it needs. Those are `!ref`s to the importer's OWN
|
|
200
|
+
// resources and resolve exactly like any other reference; nothing else on
|
|
201
|
+
// the document is a reference slot, so only that subtree is walked.
|
|
202
|
+
if (r.kind === "Telo.Import") {
|
|
203
|
+
const supplied = (r as Record<string, unknown>).resources;
|
|
204
|
+
if (supplied) (r as Record<string, unknown>).resources = walk(supplied);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (SYSTEM_KINDS.has(r.kind)) continue;
|
|
197
208
|
walk(r as Record<string, unknown>);
|
|
198
209
|
}
|
|
199
210
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `resources:` block on a `Telo.Library` — the instances a library requires
|
|
5
|
+
* from whoever imports it, beside the scalar `variables:` / `secrets:` blocks.
|
|
6
|
+
* Instances used to flow up only (`exports.resources`); this is the inward half.
|
|
7
|
+
*
|
|
8
|
+
* THE SINGLE READER of the block, in both halves (the analyzer's passes and the
|
|
9
|
+
* kernel's import controller), on the `ref-slot.ts` precedent: a boundary whose
|
|
10
|
+
* shape two runtimes must agree about is read in one place, so the next shape
|
|
11
|
+
* change is a one-file edit rather than four surfaces recognising a block by
|
|
12
|
+
* pattern-matching it.
|
|
13
|
+
*
|
|
14
|
+
* Browser-safe — no I/O, no Node built-ins.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** One declared input. */
|
|
18
|
+
export interface ResourceInput {
|
|
19
|
+
/** The entry key. Inside the library the instance is named exactly as a
|
|
20
|
+
* locally declared resource: `!ref <name>` at a ref slot, `resources.<name>`
|
|
21
|
+
* in CEL. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** The alias-qualified kind constraint, as written in the DECLARING library's
|
|
24
|
+
* own scope (`Sql.Connection`, `Self.Store`, `Telo.LogSink`). There is no
|
|
25
|
+
* `use:` here: the boundary is a dependency edge for init order whatever the
|
|
26
|
+
* library does with the instance, and the flattened application analysis
|
|
27
|
+
* drops the library doc, so an app-level claim about internal call sites is
|
|
28
|
+
* one nothing could check. */
|
|
29
|
+
kind: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Marker stamped on a synthesized kind-only declaration (see
|
|
34
|
+
* {@link injectedDeclarations}). Read through {@link isInjectedDeclaration} —
|
|
35
|
+
* never by testing the field. */
|
|
36
|
+
const INJECTED = "xTeloInjected";
|
|
37
|
+
|
|
38
|
+
/** Read a module document's `resources:` block. Returns `[]` for an
|
|
39
|
+
* `Telo.Application` (which has no such block), for a library that declares
|
|
40
|
+
* none, and for a malformed entry — the document's own schema validation
|
|
41
|
+
* reports the shape against the precise `resources.<name>` path. */
|
|
42
|
+
export function readResourceInputs(moduleDoc: unknown): ResourceInput[] {
|
|
43
|
+
const raw = (moduleDoc as { resources?: unknown } | undefined)?.resources;
|
|
44
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
|
|
45
|
+
const out: ResourceInput[] = [];
|
|
46
|
+
for (const [name, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
47
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
48
|
+
const kind = (value as { kind?: unknown }).kind;
|
|
49
|
+
if (typeof kind !== "string" || kind.length === 0) continue;
|
|
50
|
+
const description = (value as { description?: unknown }).description;
|
|
51
|
+
out.push({
|
|
52
|
+
name,
|
|
53
|
+
kind,
|
|
54
|
+
...(typeof description === "string" ? { description } : {}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The values an import supplies for a target library's declared inputs, keyed
|
|
61
|
+
* by entry name. Read off a `Telo.Import` (authored or desugared from an
|
|
62
|
+
* `imports:` entry). Values are `!ref` sentinels before Phase 2.5 and
|
|
63
|
+
* `{kind, name}` after it — this reader does not interpret them. */
|
|
64
|
+
export function readSuppliedResources(importDoc: unknown): Record<string, unknown> {
|
|
65
|
+
const raw = (importDoc as { resources?: unknown } | undefined)?.resources;
|
|
66
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
67
|
+
return raw as Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True when `manifest` is a synthesized kind-only declaration standing in for a
|
|
71
|
+
* library's `resources:` entry rather than a resource the author declared. */
|
|
72
|
+
export function isInjectedDeclaration(manifest: ResourceManifest | undefined): boolean {
|
|
73
|
+
return (manifest?.metadata as Record<string, unknown> | undefined)?.[INJECTED] === true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Synthesize a kind-only declaration per `resources:` entry, in the declaring
|
|
78
|
+
* library's own scope.
|
|
79
|
+
*
|
|
80
|
+
* That model has to exist because a library's internals are validated in the
|
|
81
|
+
* library's own pass — the flattened application analysis drops the library doc
|
|
82
|
+
* — so with nothing behind `connection`, `!ref connection` would have nothing to
|
|
83
|
+
* resolve against.
|
|
84
|
+
*
|
|
85
|
+
* Kind-only is enough because it is already what a ref slot gets: a reading
|
|
86
|
+
* types its `status:` half from the kind, closed so a typo below it is
|
|
87
|
+
* `CEL_UNKNOWN_FIELD`, and leaves the flat half open, since no manifest declares
|
|
88
|
+
* what `snapshot()` returned. So `!ref connection` at a ref slot and
|
|
89
|
+
* `resources.connection.<field>` in CEL answer exactly as they do for a locally
|
|
90
|
+
* declared resource.
|
|
91
|
+
*
|
|
92
|
+
* The declaration is a stand-in, never an instantiation: the per-resource
|
|
93
|
+
* validation loop skips it (its kind is routinely an abstract, and its config is
|
|
94
|
+
* the importer's to supply), and the kernel's import controller filters it out
|
|
95
|
+
* of the manifests it registers, binding the borrowed instance under the name
|
|
96
|
+
* instead.
|
|
97
|
+
*/
|
|
98
|
+
export function injectedDeclarations(
|
|
99
|
+
moduleDoc: ResourceManifest,
|
|
100
|
+
ownModule: string | undefined,
|
|
101
|
+
): ResourceManifest[] {
|
|
102
|
+
const inputs = readResourceInputs(moduleDoc);
|
|
103
|
+
if (inputs.length === 0) return [];
|
|
104
|
+
const meta = moduleDoc.metadata as
|
|
105
|
+
| { source?: string; sourceLine?: number }
|
|
106
|
+
| undefined;
|
|
107
|
+
return inputs.map((input) => ({
|
|
108
|
+
kind: input.kind,
|
|
109
|
+
metadata: {
|
|
110
|
+
name: input.name,
|
|
111
|
+
...(ownModule ? { module: ownModule } : {}),
|
|
112
|
+
source: meta?.source ?? "",
|
|
113
|
+
sourceLine: meta?.sourceLine ?? 0,
|
|
114
|
+
[INJECTED]: true,
|
|
115
|
+
},
|
|
116
|
+
})) as unknown as ResourceManifest[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** How many times a library is instantiated in one application. `isolated` —
|
|
120
|
+
* the default, and what every published module was written against — gives
|
|
121
|
+
* each import declaration its own child scope with its own instances;
|
|
122
|
+
* `shared` makes the library a singleton every import resolves to. */
|
|
123
|
+
export type LibraryLifecycle = "isolated" | "shared";
|
|
124
|
+
|
|
125
|
+
/** Read a module document's `lifecycle:`. `Telo.Application` has a field of the
|
|
126
|
+
* same name with a different default and no reader; this is the LIBRARY
|
|
127
|
+
* question only, so it answers `isolated` for anything else. */
|
|
128
|
+
export function readLibraryLifecycle(moduleDoc: unknown): LibraryLifecycle {
|
|
129
|
+
const doc = moduleDoc as { kind?: unknown; lifecycle?: unknown } | undefined;
|
|
130
|
+
if (doc?.kind !== "Telo.Library") return "isolated";
|
|
131
|
+
return doc.lifecycle === "shared" ? "shared" : "isolated";
|
|
132
|
+
}
|
package/src/resource-rule.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*
|
|
22
22
|
* Browser-safe: no Node built-ins.
|
|
23
23
|
*/
|
|
24
|
+
import { CEL_ENGINE, isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
24
25
|
|
|
25
26
|
export const RESOURCE_RULES_ANNOTATION = "x-telo-resource-rules";
|
|
26
27
|
|
|
@@ -58,9 +59,42 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
58
59
|
* Both markers are tested because they are not always both present: a
|
|
59
60
|
* registered definition's schema reaches the analyzer with `call` and
|
|
60
61
|
* `__compiled` dropped, keeping only `__tagged` + `source`. Testing one would
|
|
61
|
-
* make a rule readable on some paths and invisible on others.
|
|
62
|
+
* make a rule readable on some paths and invisible on others.
|
|
63
|
+
*
|
|
64
|
+
* A tagged sentinel of ANOTHER engine is not one. `__tagged` marks every tag
|
|
65
|
+
* the loader parses — `!ref` above all — so testing it alone read a reference
|
|
66
|
+
* as an expression: a column whose `type:` holds a `!ref` skipped every rule
|
|
67
|
+
* that touched `self.columns`, and said "the value holds a CEL expression" about
|
|
68
|
+
* a manifest containing none. A reference names a declaration and is a
|
|
69
|
+
* perfectly comparable value; what a rule cannot compare is a value COMPUTED at
|
|
70
|
+
* create time, which is what this predicate exists to find. */
|
|
62
71
|
function isCelNode(value: unknown): value is { source?: unknown } {
|
|
63
|
-
|
|
72
|
+
if (!isObject(value)) return false;
|
|
73
|
+
if (value.__compiled === true) return true;
|
|
74
|
+
return value.__tagged === true && value.engine === CEL_ENGINE;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The engine of a non-CEL tagged sentinel — a `!ref`, an `!include-*` — or
|
|
78
|
+
* `undefined`. A reference is comparable and never blocks a rule; the other
|
|
79
|
+
* tags hold a value only known once the resource is created, so they do, and
|
|
80
|
+
* the diagnostic has to name the tag rather than claim CEL. */
|
|
81
|
+
export function deferredTagOf(value: unknown): string | undefined {
|
|
82
|
+
if (!isTaggedSentinel(value) || isRefSentinel(value)) return undefined;
|
|
83
|
+
return value.engine === CEL_ENGINE ? undefined : value.engine;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* True when a condition was written with the `!cel` tag.
|
|
88
|
+
*
|
|
89
|
+
* The readers stay lenient and take a bare string — a rule still runs either
|
|
90
|
+
* way. What an untagged condition loses is everything outside evaluation: to the
|
|
91
|
+
* editor's colouring, completion and hover it is a plain string, so a rule author
|
|
92
|
+
* writes CEL with no help and gets none of the checks a `!cel` scalar gets.
|
|
93
|
+
* Losing that silently is exactly what a strict half exists to move earlier, so
|
|
94
|
+
* the tag is reported by the strict halves and never enforced by the readers.
|
|
95
|
+
*/
|
|
96
|
+
export function isTaggedCondition(value: unknown): boolean {
|
|
97
|
+
return isCelNode(value);
|
|
64
98
|
}
|
|
65
99
|
|
|
66
100
|
/** A precompiled `!cel` node keeps its author-written text on `source`; a plain
|
|
@@ -179,17 +213,45 @@ export function resolveRuleSubjects(
|
|
|
179
213
|
return undefined;
|
|
180
214
|
}
|
|
181
215
|
|
|
216
|
+
/** The first leaf a rule cannot compare, and what it is. `what` is a noun
|
|
217
|
+
* phrase the diagnostic quotes verbatim, because "a CEL expression" printed
|
|
218
|
+
* over an `!include-bytes` embed sends its author looking for an expression
|
|
219
|
+
* that is not there. */
|
|
220
|
+
export interface DynamicLeaf {
|
|
221
|
+
readonly path: string;
|
|
222
|
+
readonly what: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Classify ONE node, without descending. Exported because a caller that draws
|
|
226
|
+
* its own bound on how far to look (`peer-binding`'s top-level-scalar scan)
|
|
227
|
+
* must classify by the same rule as the recursive walk, or a `!ref` is a
|
|
228
|
+
* reference to one of them and an expression to the other. */
|
|
229
|
+
export function dynamicNode(value: unknown, path: string): DynamicLeaf | undefined {
|
|
230
|
+
const at = path || "(value)";
|
|
231
|
+
if (isCelNode(value)) return { path: at, what: "a CEL expression" };
|
|
232
|
+
const tag = deferredTagOf(value);
|
|
233
|
+
return tag ? { path: at, what: `an !${tag} embed` } : undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
182
236
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* the subject is skipped — and the
|
|
237
|
+
* The first leaf inside a value whose contents are not known until the resource
|
|
238
|
+
* is created, or `undefined` when every leaf is literal. A rule reading one
|
|
239
|
+
* would be comparing against a placeholder, so the subject is skipped — and the
|
|
240
|
+
* skip is reported, never silent.
|
|
241
|
+
*
|
|
242
|
+
* A `!ref` is NOT one of them. It is a tagged sentinel like `!cel`, and testing
|
|
243
|
+
* `__tagged` alone read every reference as an expression: a column whose `type:`
|
|
244
|
+
* holds a `!ref` switched off every rule touching `self.columns` and reported a
|
|
245
|
+
* CEL expression in a manifest containing none. A reference names a declaration
|
|
246
|
+
* — a value a rule compares perfectly well, and the one peer rules are built on.
|
|
186
247
|
*
|
|
187
248
|
* Stops at nested inline `{ kind }` declarations for the reason every other walk
|
|
188
249
|
* does: that CEL belongs to the nested kind, evaluated in its own scope.
|
|
189
250
|
*/
|
|
190
|
-
export function findDynamicLeaf(value: unknown, base = ""):
|
|
251
|
+
export function findDynamicLeaf(value: unknown, base = ""): DynamicLeaf | undefined {
|
|
191
252
|
if (isObject(value)) {
|
|
192
|
-
|
|
253
|
+
const own = dynamicNode(value, base);
|
|
254
|
+
if (own) return own;
|
|
193
255
|
if (typeof value.kind === "string" && base !== "") return undefined;
|
|
194
256
|
for (const [key, child] of Object.entries(value)) {
|
|
195
257
|
const found = findDynamicLeaf(child, base === "" ? key : `${base}.${key}`);
|
package/src/rule-condition.ts
CHANGED
|
@@ -32,6 +32,21 @@ import {
|
|
|
32
32
|
*/
|
|
33
33
|
export const RULE_BUDGET_MS = 50;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The one message for an untagged `condition:`, shared by every rule family so
|
|
37
|
+
* all of them say the same thing about the same defect.
|
|
38
|
+
*
|
|
39
|
+
* The readers stay lenient and a bare string still runs. What it loses is
|
|
40
|
+
* everything outside evaluation — to the editor's colouring, completion and
|
|
41
|
+
* hover an untagged condition is a plain string, so its author writes CEL with
|
|
42
|
+
* no help and none of the checks a `!cel` scalar gets. Losing that silently is
|
|
43
|
+
* the failure a strict half exists to move earlier.
|
|
44
|
+
*/
|
|
45
|
+
export const UNTAGGED_CONDITION =
|
|
46
|
+
"Write 'condition' with the !cel tag. The reader is lenient and a bare string still " +
|
|
47
|
+
"runs, but untagged the expression is not CEL to the editor's colouring, completion " +
|
|
48
|
+
"or hover, so a rule silently stops being CEL to every surface but this one.";
|
|
49
|
+
|
|
35
50
|
const HOST_BACKED = new Set(CEL_FUNCTIONS.filter((f) => f.hostBacked).map((f) => f.name));
|
|
36
51
|
const NON_DETERMINISTIC = new Set(
|
|
37
52
|
CEL_FUNCTIONS.filter((f) => !f.deterministic).map((f) => f.name),
|