@telorun/analyzer 0.38.0 → 0.40.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 +17 -0
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +146 -0
- package/dist/import-resolution-diagnostics.d.ts +20 -0
- package/dist/import-resolution-diagnostics.d.ts.map +1 -0
- package/dist/import-resolution-diagnostics.js +59 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/inline-imports.d.ts.map +1 -1
- package/dist/inline-imports.js +1 -0
- package/dist/loaded-types.d.ts +12 -1
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +11 -1
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +20 -1
- package/dist/redaction-path.d.ts +49 -0
- package/dist/redaction-path.d.ts.map +1 -0
- package/dist/redaction-path.js +133 -0
- package/dist/reference-field-map.d.ts +12 -0
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +9 -0
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +8 -0
- package/dist/sources/local-path-ref.d.ts +4 -0
- package/dist/sources/local-path-ref.d.ts.map +1 -0
- package/dist/sources/local-path-ref.js +5 -0
- package/dist/validate-extends.d.ts +0 -20
- package/dist/validate-extends.d.ts.map +1 -1
- package/dist/validate-extends.js +7 -1
- package/dist/validate-logging.d.ts +22 -0
- package/dist/validate-logging.d.ts.map +1 -0
- package/dist/validate-logging.js +116 -0
- package/package.json +2 -2
- package/src/analyzer.ts +18 -0
- package/src/builtins.ts +152 -0
- package/src/import-resolution-diagnostics.ts +66 -0
- package/src/index.ts +8 -0
- package/src/inline-imports.ts +1 -0
- package/src/loaded-types.ts +12 -1
- package/src/manifest-loader.ts +11 -1
- package/src/normalize-inline-resources.ts +20 -1
- package/src/redaction-path.ts +142 -0
- package/src/reference-field-map.ts +20 -0
- package/src/schema-compat.ts +7 -0
- package/src/sources/local-path-ref.ts +5 -0
- package/src/validate-extends.ts +8 -1
- package/src/validate-logging.ts +136 -0
package/src/builtins.ts
CHANGED
|
@@ -22,6 +22,89 @@ const PROVENANCE_METADATA = {
|
|
|
22
22
|
documentation: { type: "string" },
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/** The six named levels of `kernel/specs/logging.md` §5.1. The full 1–24 OTel
|
|
26
|
+
* range stays valid on the wire; only these are nameable in a manifest. */
|
|
27
|
+
const LOG_LEVEL_ENUM = ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
28
|
+
|
|
29
|
+
const DURATION_PATTERN = "^\\s*\\d+(\\.\\d+)?\\s*(ms|s|m|h)\\s*$";
|
|
30
|
+
|
|
31
|
+
/** Fields every sink kind inherits from `Telo.LogSink` (§12.1). A concrete sink
|
|
32
|
+
* kind may narrow a default but must not remove a field — a buffering policy
|
|
33
|
+
* that cannot be configured from the only permitted configuration source is
|
|
34
|
+
* not a policy. */
|
|
35
|
+
const LOG_SINK_COMMON_PROPERTIES = {
|
|
36
|
+
level: { type: "string", enum: LOG_LEVEL_ENUM },
|
|
37
|
+
buffer: { type: "integer", minimum: 1 },
|
|
38
|
+
on_full: { type: "string", enum: ["block", "drop_new", "drop_old"] },
|
|
39
|
+
flush_interval: { type: "string", pattern: DURATION_PATTERN },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Threshold / redaction / sampling — the fields an `imports:` entry may
|
|
43
|
+
* override for its subtree (§12.2). Deliberately excludes `sinks`: sinks are
|
|
44
|
+
* process-level I/O and belong to the root Application that owns the process,
|
|
45
|
+
* so an imported library can never open a log file on its importer's behalf. */
|
|
46
|
+
const LOGGING_SCOPE_PROPERTIES = {
|
|
47
|
+
level: { type: "string", enum: LOG_LEVEL_ENUM },
|
|
48
|
+
attributes: { type: "object" },
|
|
49
|
+
redact: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
paths: { type: "array", items: { type: "string" } },
|
|
53
|
+
censor: { type: "string" },
|
|
54
|
+
// Deletion destroys schema stability and hides that a field was present
|
|
55
|
+
// at all, so §14 offers this but never as the default.
|
|
56
|
+
remove: { type: "boolean" },
|
|
57
|
+
},
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
},
|
|
60
|
+
sampling: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
first: { type: "integer", minimum: 0 },
|
|
64
|
+
thereafter: { type: "integer", minimum: 0 },
|
|
65
|
+
tick: { type: "string", pattern: DURATION_PATTERN },
|
|
66
|
+
sampleErrors: { type: "boolean" },
|
|
67
|
+
},
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The per-import `logging:` override. */
|
|
73
|
+
const IMPORT_LOGGING_SCHEMA = {
|
|
74
|
+
type: "object",
|
|
75
|
+
"x-telo-eval": "compile",
|
|
76
|
+
properties: LOGGING_SCOPE_PROPERTIES,
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** The root Application's `logging:` block — the scope fields plus `sinks`.
|
|
81
|
+
*
|
|
82
|
+
* `x-telo-eval: compile` covers the whole block: every value resolves once at
|
|
83
|
+
* load, which is what lets a level come from the host environment through a
|
|
84
|
+
* `variables:` entry read with `!cel` rather than through a parallel
|
|
85
|
+
* `TELO_LOG_*` path that would be invisible to the analyzer and the editor
|
|
86
|
+
* (§12.3, D6). */
|
|
87
|
+
const ROOT_LOGGING_SCHEMA = {
|
|
88
|
+
type: "object",
|
|
89
|
+
"x-telo-eval": "compile",
|
|
90
|
+
properties: {
|
|
91
|
+
...LOGGING_SCOPE_PROPERTIES,
|
|
92
|
+
// A list rather than a keyed map because sinks are root-only and therefore
|
|
93
|
+
// never merged; with no merge to disambiguate, a list matches how Telo
|
|
94
|
+
// spells every other ref-or-inline collection. `x-telo-inline` opts this one
|
|
95
|
+
// slot into inline-resource extraction — see normalize-inline-resources.ts.
|
|
96
|
+
sinks: {
|
|
97
|
+
type: "array",
|
|
98
|
+
items: {
|
|
99
|
+
type: "object",
|
|
100
|
+
"x-telo-ref": "telo#LogSink",
|
|
101
|
+
"x-telo-inline": true,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
};
|
|
107
|
+
|
|
25
108
|
export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
26
109
|
{ kind: "Telo.Abstract", metadata: { name: "Template", module: "Telo" } },
|
|
27
110
|
{ kind: "Telo.Abstract", metadata: { name: "Runnable", module: "Telo" } },
|
|
@@ -34,6 +117,59 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
34
117
|
metadata: { name: "Provider", module: "Telo" },
|
|
35
118
|
schema: { "x-telo-eval": "compile" },
|
|
36
119
|
},
|
|
120
|
+
// The sink lifecycle role: attach, write a record, flush, detach. Deliberately
|
|
121
|
+
// payload-opaque — it carries no filtering and no encoding — so a future
|
|
122
|
+
// `Telo.TraceSink` reuses the same capability with a different record type.
|
|
123
|
+
// Scoped to record-stream sinks; metrics aggregate rather than stream and are
|
|
124
|
+
// not covered. See kernel/specs/logging.md §10.
|
|
125
|
+
{ kind: "Telo.Abstract", metadata: { name: "Sink", module: "Telo" } },
|
|
126
|
+
// The abstract every *log* sink kind extends, carrying the log-specific
|
|
127
|
+
// configuration. A kernel built-in resolvable without an import, so a sink
|
|
128
|
+
// author depends on the kernel contract rather than on a standard-library
|
|
129
|
+
// module version and kernel↔module skew never becomes a compatibility surface
|
|
130
|
+
// for "where do logs go".
|
|
131
|
+
{
|
|
132
|
+
kind: "Telo.Abstract",
|
|
133
|
+
metadata: { name: "LogSink", module: "Telo" },
|
|
134
|
+
capability: "Telo.Sink",
|
|
135
|
+
schema: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: LOG_SINK_COMMON_PROPERTIES,
|
|
138
|
+
additionalProperties: true,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
kind: "Telo.Definition",
|
|
143
|
+
metadata: { name: "ConsoleSink", module: "Telo" },
|
|
144
|
+
capability: "Telo.Sink",
|
|
145
|
+
extends: "Telo.LogSink",
|
|
146
|
+
schema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
...LOG_SINK_COMMON_PROPERTIES,
|
|
150
|
+
destination: { type: "string", enum: ["stderr", "stdout"] },
|
|
151
|
+
encoding: { type: "string", enum: ["auto", "pretty", "json"] },
|
|
152
|
+
color: { type: "string", enum: ["auto", "always", "never"] },
|
|
153
|
+
},
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
kind: "Telo.Definition",
|
|
159
|
+
metadata: { name: "FileSink", module: "Telo" },
|
|
160
|
+
capability: "Telo.Sink",
|
|
161
|
+
extends: "Telo.LogSink",
|
|
162
|
+
schema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
...LOG_SINK_COMMON_PROPERTIES,
|
|
166
|
+
destination: { type: "string" },
|
|
167
|
+
encoding: { type: "string", enum: ["json", "pretty"] },
|
|
168
|
+
},
|
|
169
|
+
required: ["destination"],
|
|
170
|
+
additionalProperties: false,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
37
173
|
{
|
|
38
174
|
kind: "Telo.Definition",
|
|
39
175
|
metadata: { name: "Abstract", module: "Telo" },
|
|
@@ -273,6 +409,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
273
409
|
{ type: "array", items: { type: "string" } },
|
|
274
410
|
],
|
|
275
411
|
},
|
|
412
|
+
logging: IMPORT_LOGGING_SCHEMA,
|
|
276
413
|
},
|
|
277
414
|
required: ["metadata", "source"],
|
|
278
415
|
additionalProperties: false,
|
|
@@ -431,6 +568,11 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
431
568
|
{ type: "array", items: { type: "string" } },
|
|
432
569
|
],
|
|
433
570
|
},
|
|
571
|
+
// Threshold / redaction / sampling override for this import's
|
|
572
|
+
// subtree. Attached to the import rather than to a map keyed
|
|
573
|
+
// by module name because an alias is already uniqueness-
|
|
574
|
+
// enforced, while module names collide (§12.2, D9).
|
|
575
|
+
logging: IMPORT_LOGGING_SCHEMA,
|
|
434
576
|
},
|
|
435
577
|
additionalProperties: false,
|
|
436
578
|
},
|
|
@@ -500,6 +642,11 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
500
642
|
additionalProperties: false,
|
|
501
643
|
},
|
|
502
644
|
},
|
|
645
|
+
// Structured logging configuration. The manifest is the only
|
|
646
|
+
// configuration source — there is no TELO_LOG_* variable and no logging
|
|
647
|
+
// CLI flag — so a level derived from the host environment goes through a
|
|
648
|
+
// `variables:` entry read with `!cel`. See kernel/specs/logging.md §12.
|
|
649
|
+
logging: ROOT_LOGGING_SCHEMA,
|
|
503
650
|
},
|
|
504
651
|
required: ["metadata"],
|
|
505
652
|
additionalProperties: false,
|
|
@@ -565,6 +712,11 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
565
712
|
{ type: "array", items: { type: "string" } },
|
|
566
713
|
],
|
|
567
714
|
},
|
|
715
|
+
// Threshold / redaction / sampling override for this import's
|
|
716
|
+
// subtree. Attached to the import rather than to a map keyed
|
|
717
|
+
// by module name because an alias is already uniqueness-
|
|
718
|
+
// enforced, while module names collide (§12.2, D9).
|
|
719
|
+
logging: IMPORT_LOGGING_SCHEMA,
|
|
568
720
|
},
|
|
569
721
|
additionalProperties: false,
|
|
570
722
|
},
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { GraphLoadError, LoadedGraph } from "./loaded-types.js";
|
|
2
|
+
import { isLocalPathSource } from "./sources/local-path-ref.js";
|
|
3
|
+
import { isRegistryRef } from "./sources/module-ref.js";
|
|
4
|
+
import { isOciRef } from "./sources/oci-ref.js";
|
|
5
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const SOURCE = "telo-analyzer";
|
|
8
|
+
|
|
9
|
+
/** True when `source` is a shape some transport claims — a registry ref, an OCI
|
|
10
|
+
* ref, an HTTP(S) URL, or a relative/absolute path. A source matching none of
|
|
11
|
+
* these is malformed (no transport can ever resolve it), which we report
|
|
12
|
+
* differently from a well-formed ref that simply failed to fetch. */
|
|
13
|
+
function isRecognizedSourceShape(source: string): boolean {
|
|
14
|
+
return (
|
|
15
|
+
isRegistryRef(source) ||
|
|
16
|
+
isOciRef(source) ||
|
|
17
|
+
source.startsWith("http://") ||
|
|
18
|
+
source.startsWith("https://") ||
|
|
19
|
+
isLocalPathSource(source)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function messageFor(e: GraphLoadError, malformed: boolean): string {
|
|
24
|
+
const authored = e.source ?? e.url;
|
|
25
|
+
const via = e.alias ? `import '${e.alias}' → '${authored}'` : `'${authored}'`;
|
|
26
|
+
if (malformed) {
|
|
27
|
+
return (
|
|
28
|
+
`Cannot resolve ${via}: not a recognized module reference. Expected ` +
|
|
29
|
+
`'namespace/name@version', 'oci://host/repo@tag', 'https://…', or a relative path.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return `Cannot resolve ${via}: ${e.error.message}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Convert a graph's import-resolution failures (`graph.errors`) into structured,
|
|
37
|
+
* coded diagnostics. This is the single source of truth for surfacing a broken
|
|
38
|
+
* import — every host (CLI, VS Code, telo-editor) routes these instead of each
|
|
39
|
+
* re-deriving the channel and drifting (the VS Code extension used to drop it
|
|
40
|
+
* entirely, showing nothing for a broken import).
|
|
41
|
+
*
|
|
42
|
+
* The analyzer owns only this raw channel conversion; the *presentation* policy
|
|
43
|
+
* — which analysis cascade to hold back for a compromised file — lives in
|
|
44
|
+
* `@telorun/ide-support`'s `assembleGraphDiagnostics`.
|
|
45
|
+
*
|
|
46
|
+
* Each diagnostic adopts the same `data` shape as version-reconciliation
|
|
47
|
+
* diagnostics — `{ filePath, path: "imports.<alias>" }` — so the shared
|
|
48
|
+
* `findPositions` / `resolveRange` routing anchors it on the offending import
|
|
49
|
+
* line with no host-specific code.
|
|
50
|
+
*/
|
|
51
|
+
export function importResolutionDiagnostics(graph: LoadedGraph): AnalysisDiagnostic[] {
|
|
52
|
+
return graph.errors.map((e) => {
|
|
53
|
+
const filePath = e.fromSource ?? graph.entry.owner.source;
|
|
54
|
+
const malformed = !isRecognizedSourceShape(e.source ?? e.url);
|
|
55
|
+
const data: { filePath: string; path?: string; sourceLine?: number } = { filePath };
|
|
56
|
+
if (e.alias) data.path = `imports.${e.alias}`;
|
|
57
|
+
if (e.sourceLine !== undefined) data.sourceLine = e.sourceLine;
|
|
58
|
+
return {
|
|
59
|
+
severity: DiagnosticSeverity.Error,
|
|
60
|
+
code: malformed ? "INVALID_IMPORT_SOURCE" : "IMPORT_UNRESOLVED",
|
|
61
|
+
source: SOURCE,
|
|
62
|
+
message: messageFor(e, malformed),
|
|
63
|
+
data,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { AnalysisRegistry } from "./analysis-registry.js";
|
|
2
2
|
export type { RefFieldInfo } from "./analysis-registry.js";
|
|
3
3
|
export { StaticAnalyzer } from "./analyzer.js";
|
|
4
|
+
export { importResolutionDiagnostics } from "./import-resolution-diagnostics.js";
|
|
4
5
|
export type {
|
|
5
6
|
GraphLoadError,
|
|
6
7
|
ImportEdge,
|
|
@@ -33,6 +34,12 @@ export {
|
|
|
33
34
|
resolveParent,
|
|
34
35
|
} from "./extends-resolution.js";
|
|
35
36
|
export type { DefResolver } from "./extends-resolution.js";
|
|
37
|
+
export {
|
|
38
|
+
hasIntermediateWildcard,
|
|
39
|
+
parseRedactionPath,
|
|
40
|
+
RedactionPathError,
|
|
41
|
+
} from "./redaction-path.js";
|
|
42
|
+
export type { RedactionSegment } from "./redaction-path.js";
|
|
36
43
|
export { buildReferenceFieldMap, isRefEntry, isScopeEntry } from "./reference-field-map.js";
|
|
37
44
|
export type { ReferenceFieldMap, RefFieldEntry } from "./reference-field-map.js";
|
|
38
45
|
export { visitManifest } from "./manifest-visitor.js";
|
|
@@ -78,6 +85,7 @@ export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
|
|
|
78
85
|
export type { ParsedModuleRef } from "./sources/module-ref.js";
|
|
79
86
|
export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
|
|
80
87
|
export type { ParsedOciRef } from "./sources/oci-ref.js";
|
|
88
|
+
export { isLocalPathSource } from "./sources/local-path-ref.js";
|
|
81
89
|
export {
|
|
82
90
|
MANIFEST_CACHE_BASE_URL,
|
|
83
91
|
ManifestCacheSource,
|
package/src/inline-imports.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function inlineImportManifests(
|
|
|
55
55
|
...(entry.variables !== undefined ? { variables: entry.variables } : {}),
|
|
56
56
|
...(entry.secrets !== undefined ? { secrets: entry.secrets } : {}),
|
|
57
57
|
...(entry.runtime !== undefined ? { runtime: entry.runtime } : {}),
|
|
58
|
+
...(entry.logging !== undefined ? { logging: entry.logging } : {}),
|
|
58
59
|
} as unknown as ResourceManifest;
|
|
59
60
|
|
|
60
61
|
out.push({ manifest, position: synthPosition(modulePosition, alias, scalar) });
|
package/src/loaded-types.ts
CHANGED
|
@@ -96,9 +96,20 @@ export interface LoadedGraph {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export interface GraphLoadError {
|
|
99
|
-
/** URL of the file that failed to load
|
|
99
|
+
/** URL of the file that failed to load (resolved — may be a `file://` URL for
|
|
100
|
+
* a relative import). */
|
|
100
101
|
url: string;
|
|
102
|
+
/** The import source string exactly as authored (`./lib`, `std/x@1.0.0`),
|
|
103
|
+
* before relative-path resolution. Preferred over `url` for classification
|
|
104
|
+
* and display, so a diagnostic quotes what the author wrote. */
|
|
105
|
+
source?: string;
|
|
101
106
|
/** Source of the import that triggered the load, or null for the entry. */
|
|
102
107
|
fromSource: string | null;
|
|
108
|
+
/** Import alias the failed source was bound to in `fromSource`'s `imports:`
|
|
109
|
+
* map, when the failure is a transitive import (absent for an entry-load
|
|
110
|
+
* failure). Lets a consumer anchor the diagnostic at `imports.<alias>`. */
|
|
111
|
+
alias?: string;
|
|
112
|
+
/** Line of the `Telo.Import` doc in `fromSource`, for position fallback. */
|
|
113
|
+
sourceLine?: number;
|
|
103
114
|
error: Error;
|
|
104
115
|
}
|
package/src/manifest-loader.ts
CHANGED
|
@@ -258,7 +258,10 @@ export class Loader {
|
|
|
258
258
|
} catch (err) {
|
|
259
259
|
errors.push({
|
|
260
260
|
url: importSource,
|
|
261
|
+
source: importSource,
|
|
261
262
|
fromSource: file.source,
|
|
263
|
+
alias,
|
|
264
|
+
sourceLine,
|
|
262
265
|
error: err instanceof Error ? err : new Error(String(err)),
|
|
263
266
|
});
|
|
264
267
|
continue;
|
|
@@ -284,7 +287,14 @@ export class Loader {
|
|
|
284
287
|
} catch (err) {
|
|
285
288
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
286
289
|
(e as { sourceLine?: number }).sourceLine = sourceLine;
|
|
287
|
-
errors.push({
|
|
290
|
+
errors.push({
|
|
291
|
+
url: resolvedTarget,
|
|
292
|
+
source: importSource,
|
|
293
|
+
fromSource: file.source,
|
|
294
|
+
alias,
|
|
295
|
+
sourceLine,
|
|
296
|
+
error: e,
|
|
297
|
+
});
|
|
288
298
|
continue;
|
|
289
299
|
}
|
|
290
300
|
}
|
|
@@ -10,6 +10,21 @@ const SYSTEM_KINDS = new Set([
|
|
|
10
10
|
"Telo.Import",
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* System kinds are excluded from inline extraction by default, but a single slot
|
|
15
|
+
* may opt back in with `x-telo-inline: true` — `Telo.Application.logging.sinks`
|
|
16
|
+
* is the case this exists for.
|
|
17
|
+
*
|
|
18
|
+
* The opt-in is per slot rather than per kind because this pass runs *upstream*
|
|
19
|
+
* of schema validation on both the analyzer and runtime paths. Admitting the
|
|
20
|
+
* whole Application document would rewrite an inline `{kind, ...}` in `targets`
|
|
21
|
+
* into a valid `{kind, name}` before AJV ever saw it, silently converting a
|
|
22
|
+
* deliberate rejection into a working feature.
|
|
23
|
+
*/
|
|
24
|
+
function acceptsInline(resourceKind: string, entry: { inline?: boolean }): boolean {
|
|
25
|
+
return !SYSTEM_KINDS.has(resourceKind) || entry.inline === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
/** Replaces characters outside [a-zA-Z0-9_] with underscores. */
|
|
14
29
|
function sanitizeName(raw: string): string {
|
|
15
30
|
return raw.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
@@ -65,9 +80,12 @@ export function normalizeInlineResources(
|
|
|
65
80
|
|
|
66
81
|
// Queue: all non-system resources with a name. Extracted resources are appended.
|
|
67
82
|
// Filter the CLONES (not the originals) so traversal mutates copies.
|
|
83
|
+
// System kinds join the queue too: their inline-accepting slots are filtered
|
|
84
|
+
// per entry below, so a system document is walked but only its opted-in slots
|
|
85
|
+
// are extracted from.
|
|
68
86
|
const queue = result.filter(
|
|
69
87
|
(r): r is ResourceManifest & { metadata: { name: string } } =>
|
|
70
|
-
typeof r.metadata?.name === "string" && !!r.kind
|
|
88
|
+
typeof r.metadata?.name === "string" && !!r.kind,
|
|
71
89
|
);
|
|
72
90
|
|
|
73
91
|
let i = 0;
|
|
@@ -97,6 +115,7 @@ export function normalizeInlineResources(
|
|
|
97
115
|
|
|
98
116
|
for (const [fieldPath, entry] of fieldMap) {
|
|
99
117
|
if (!isRefEntry(entry)) continue;
|
|
118
|
+
if (!acceptsInline(resource.kind, entry)) continue;
|
|
100
119
|
|
|
101
120
|
const inScope = scopePrefixes.some(
|
|
102
121
|
(prefix) =>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The redaction path grammar of `kernel/specs/logging.md` §14 — a hand-written
|
|
3
|
+
* parser over a closed grammar.
|
|
4
|
+
*
|
|
5
|
+
* §14.1 makes this a security requirement rather than a style preference. The
|
|
6
|
+
* implementation this syntax is borrowed from compiles paths through the
|
|
7
|
+
* `Function` constructor and validates them by "evaluate it and see whether it
|
|
8
|
+
* parses", which is exactly why that implementation must forbid user input. A
|
|
9
|
+
* real parser removes the injection surface entirely and, as a bonus, makes
|
|
10
|
+
* paths statically checkable by `telo check`.
|
|
11
|
+
*
|
|
12
|
+
* Browser-safe by construction: this module is imported by both the analyzer's
|
|
13
|
+
* static check and the kernel's runtime redaction pass, so the grammar has one
|
|
14
|
+
* definition rather than two that can drift.
|
|
15
|
+
*
|
|
16
|
+
* Grammar:
|
|
17
|
+
*
|
|
18
|
+
* path := segment ( "." segment | bracket )*
|
|
19
|
+
* segment := bareKey | "*"
|
|
20
|
+
* bracket := "[" ( quoted | integer | "*" ) "]"
|
|
21
|
+
* quoted := '"' ... '"' | "'" ... "'"
|
|
22
|
+
*
|
|
23
|
+
* More than one wildcard per path is supported — `items[*].tokens[*].value` is
|
|
24
|
+
* valid. The one-wildcard limit in the best-known implementation is an artifact
|
|
25
|
+
* of how it compiles accessors, not a property of the grammar.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type RedactionSegment = { kind: "key"; name: string } | { kind: "wildcard" };
|
|
29
|
+
|
|
30
|
+
export class RedactionPathError extends Error {
|
|
31
|
+
readonly code = "INVALID_REDACTION_PATH";
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly offset: number;
|
|
34
|
+
|
|
35
|
+
constructor(path: string, offset: number, detail: string) {
|
|
36
|
+
super(`Invalid redaction path "${path}" at position ${offset}: ${detail}`);
|
|
37
|
+
this.name = "RedactionPathError";
|
|
38
|
+
this.path = path;
|
|
39
|
+
this.offset = offset;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const BARE_KEY_TERMINATORS = new Set([".", "[", "]"]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse a redaction path into its segments. Throws {@link RedactionPathError}
|
|
47
|
+
* with the offending offset so `telo check` can point at the character rather
|
|
48
|
+
* than the whole path.
|
|
49
|
+
*/
|
|
50
|
+
export function parseRedactionPath(path: string): RedactionSegment[] {
|
|
51
|
+
if (path.length === 0) throw new RedactionPathError(path, 0, "path is empty");
|
|
52
|
+
|
|
53
|
+
const segments: RedactionSegment[] = [];
|
|
54
|
+
let index = 0;
|
|
55
|
+
let expectSegment = true;
|
|
56
|
+
|
|
57
|
+
while (index < path.length) {
|
|
58
|
+
const char = path[index]!;
|
|
59
|
+
|
|
60
|
+
if (char === "[") {
|
|
61
|
+
index = parseBracket(path, index, segments);
|
|
62
|
+
expectSegment = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (char === ".") {
|
|
67
|
+
if (expectSegment) {
|
|
68
|
+
throw new RedactionPathError(path, index, "expected a key before '.'");
|
|
69
|
+
}
|
|
70
|
+
index += 1;
|
|
71
|
+
expectSegment = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (char === "]") {
|
|
76
|
+
throw new RedactionPathError(path, index, "unmatched ']'");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const start = index;
|
|
80
|
+
while (index < path.length && !BARE_KEY_TERMINATORS.has(path[index]!)) index += 1;
|
|
81
|
+
const raw = path.slice(start, index);
|
|
82
|
+
if (raw.length === 0) throw new RedactionPathError(path, start, "empty key");
|
|
83
|
+
segments.push(raw === "*" ? { kind: "wildcard" } : { kind: "key", name: raw });
|
|
84
|
+
expectSegment = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (expectSegment) {
|
|
88
|
+
throw new RedactionPathError(path, path.length, "path ends with a trailing '.'");
|
|
89
|
+
}
|
|
90
|
+
return segments;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseBracket(path: string, open: number, segments: RedactionSegment[]): number {
|
|
94
|
+
let index = open + 1;
|
|
95
|
+
if (index >= path.length) throw new RedactionPathError(path, open, "unterminated '['");
|
|
96
|
+
|
|
97
|
+
const quote = path[index];
|
|
98
|
+
if (quote === '"' || quote === "'") {
|
|
99
|
+
index += 1;
|
|
100
|
+
const start = index;
|
|
101
|
+
while (index < path.length && path[index] !== quote) index += 1;
|
|
102
|
+
if (index >= path.length) {
|
|
103
|
+
throw new RedactionPathError(path, start, `unterminated ${quote} quoted key`);
|
|
104
|
+
}
|
|
105
|
+
const name = path.slice(start, index);
|
|
106
|
+
if (name.length === 0) throw new RedactionPathError(path, start, "empty quoted key");
|
|
107
|
+
index += 1;
|
|
108
|
+
if (path[index] !== "]") {
|
|
109
|
+
throw new RedactionPathError(path, index, "expected ']' after quoted key");
|
|
110
|
+
}
|
|
111
|
+
segments.push({ kind: "key", name });
|
|
112
|
+
return index + 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const start = index;
|
|
116
|
+
while (index < path.length && path[index] !== "]") index += 1;
|
|
117
|
+
if (index >= path.length) throw new RedactionPathError(path, open, "unterminated '['");
|
|
118
|
+
const raw = path.slice(start, index);
|
|
119
|
+
if (raw.length === 0) throw new RedactionPathError(path, start, "empty '[]'");
|
|
120
|
+
if (raw === "*") {
|
|
121
|
+
segments.push({ kind: "wildcard" });
|
|
122
|
+
} else if (/^\d+$/.test(raw)) {
|
|
123
|
+
segments.push({ kind: "key", name: raw });
|
|
124
|
+
} else {
|
|
125
|
+
throw new RedactionPathError(
|
|
126
|
+
path,
|
|
127
|
+
start,
|
|
128
|
+
`expected a quoted key, an integer index, or '*', got "${raw}" — quote it as ["${raw}"]`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return index + 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `true` when the path contains a wildcard anywhere but its last segment.
|
|
135
|
+
* §14.2 measures intermediate wildcards at 25–55% over plain serialization,
|
|
136
|
+
* against 1–2% for explicit paths, so a runtime may warn when one is used. */
|
|
137
|
+
export function hasIntermediateWildcard(segments: readonly RedactionSegment[]): boolean {
|
|
138
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
139
|
+
if (segments[i]!.kind === "wildcard") return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
@@ -8,6 +8,18 @@ export interface RefFieldEntry {
|
|
|
8
8
|
/** x-telo-context schema declared on this ref slot, if any. Describes the CEL invocation
|
|
9
9
|
* context available to resources placed in this slot. */
|
|
10
10
|
context?: Record<string, any>;
|
|
11
|
+
/** `x-telo-inline: true` — this slot accepts an inline `{kind, ...config}`
|
|
12
|
+
* definition, not only a `!ref`.
|
|
13
|
+
*
|
|
14
|
+
* Only meaningful on the *system* kinds (`Telo.Application` and friends),
|
|
15
|
+
* which are otherwise excluded from inline-resource normalization wholesale.
|
|
16
|
+
* Ordinary resource kinds accept inline definitions at every ref slot and
|
|
17
|
+
* need no annotation. The flag exists so `logging.sinks` can opt in without
|
|
18
|
+
* also legalizing an inline definition in `targets`, where the Application
|
|
19
|
+
* schema rejects one deliberately — normalization runs upstream of AJV, so
|
|
20
|
+
* an unconditional opt-in would rewrite the value into a valid shape before
|
|
21
|
+
* the schema ever saw it. */
|
|
22
|
+
inline?: boolean;
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
/** An entry for a field that declares an execution scope (x-telo-scope). */
|
|
@@ -148,6 +160,13 @@ export function buildReferenceFieldMap(schema: Record<string, any>): ReferenceFi
|
|
|
148
160
|
return map;
|
|
149
161
|
}
|
|
150
162
|
|
|
163
|
+
/** `x-telo-inline` declared on any `anyOf` branch marks the whole slot as
|
|
164
|
+
* inline-accepting, matching how {@link collectRefs} unions branch refs. */
|
|
165
|
+
function collectInlineFlag(node: Record<string, any>): boolean {
|
|
166
|
+
if (!Array.isArray(node.anyOf)) return false;
|
|
167
|
+
return node.anyOf.some((branch: Record<string, any>) => branch?.["x-telo-inline"] === true);
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
export function collectRefs(node: Record<string, any>): string[] {
|
|
152
171
|
const refs: string[] = [];
|
|
153
172
|
if (typeof node["x-telo-ref"] === "string") {
|
|
@@ -212,6 +231,7 @@ function traverseNode(
|
|
|
212
231
|
if (refs.length > 0) {
|
|
213
232
|
const entry: RefFieldEntry = { refs, isArray: path.includes("[]") };
|
|
214
233
|
if (node["x-telo-context"]) entry.context = node["x-telo-context"] as Record<string, any>;
|
|
234
|
+
if (node["x-telo-inline"] === true || collectInlineFlag(node)) entry.inline = true;
|
|
215
235
|
map.set(path, entry);
|
|
216
236
|
// A node can mix item-level ref branches (a bare string / `{kind, name}`)
|
|
217
237
|
// with object branches that carry their OWN nested refs — e.g. Application
|
package/src/schema-compat.ts
CHANGED
|
@@ -308,6 +308,13 @@ export function celTypeSatisfiesJsonSchema(celType: string, schema: Record<strin
|
|
|
308
308
|
/** Return a literal placeholder value of the correct schema type for AJV. */
|
|
309
309
|
export function celPlaceholderForSchema(schema: Record<string, any>): unknown {
|
|
310
310
|
if (schema.default !== undefined) return schema.default;
|
|
311
|
+
// An enum-constrained field needs a placeholder drawn from the enum: the
|
|
312
|
+
// type-based fallbacks below ("" for a string, 0 for a number) satisfy `type`
|
|
313
|
+
// but violate `enum`, so a CEL expression feeding any enum field would report
|
|
314
|
+
// a spurious SCHEMA_VIOLATION against a value the author never wrote. The
|
|
315
|
+
// member chosen is irrelevant — only its acceptability to AJV matters, since
|
|
316
|
+
// the real value is checked at runtime once the expression resolves.
|
|
317
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
|
|
311
318
|
switch (schema.type) {
|
|
312
319
|
case "integer":
|
|
313
320
|
case "number":
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** True when `source` names an on-disk sibling manifest — a relative (`./`,
|
|
2
|
+
* `../`) or absolute (`/`) path — rather than a transport-owned remote ref. */
|
|
3
|
+
export function isLocalPathSource(source: string): boolean {
|
|
4
|
+
return source.startsWith(".") || source.startsWith("/");
|
|
5
|
+
}
|
package/src/validate-extends.ts
CHANGED
|
@@ -29,6 +29,9 @@ const EXTENDS_ALIAS_RE = /^[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*$/;
|
|
|
29
29
|
* (metadata.module !== "Telo"). Builtin lifecycle capabilities (Telo.Invocable, etc.)
|
|
30
30
|
* never trigger this — they're lifecycle roles by design.
|
|
31
31
|
*/
|
|
32
|
+
/** The built-in namespace, resolvable without a `Telo.Import`. */
|
|
33
|
+
const TELO_BUILTIN_ALIAS = "Telo";
|
|
34
|
+
|
|
32
35
|
export function validateExtends(
|
|
33
36
|
manifests: ResourceManifest[],
|
|
34
37
|
registry: DefinitionRegistry,
|
|
@@ -88,7 +91,11 @@ export function validateExtends(
|
|
|
88
91
|
});
|
|
89
92
|
} else {
|
|
90
93
|
const prefix = extendsValue.slice(0, extendsValue.indexOf("."));
|
|
91
|
-
|
|
94
|
+
// `Telo` needs no import: the kernel built-ins are globally resolvable
|
|
95
|
+
// by design, which is what lets a sink author depend on the kernel
|
|
96
|
+
// contract (`extends: Telo.LogSink`) rather than on a standard-library
|
|
97
|
+
// module version — see kernel/specs/logging.md §10.2.
|
|
98
|
+
if (prefix !== TELO_BUILTIN_ALIAS && !aliases.hasAlias(prefix)) {
|
|
92
99
|
diagnostics.push({
|
|
93
100
|
severity: DiagnosticSeverity.Error,
|
|
94
101
|
code: "EXTENDS_MALFORMED",
|