@telorun/analyzer 0.38.0 → 0.39.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.
@@ -2,25 +2,5 @@ import type { ResourceManifest } from "@telorun/sdk";
2
2
  import type { AliasResolver } from "./alias-resolver.js";
3
3
  import type { DefinitionRegistry } from "./definition-registry.js";
4
4
  import { type AnalysisDiagnostic } from "./types.js";
5
- /**
6
- * Phase 3b — Validate `extends` fields on Telo.Definition docs, and flag the legacy
7
- * `capability: <UserAbstract>` overload with CAPABILITY_SHADOWS_EXTENDS so users migrate.
8
- *
9
- * `extends` uses alias form ("<Alias>.<Name>") resolved against the declaring file's
10
- * Telo.Import declarations — same pattern as `kind:` prefixes. The analyzer pre-resolves
11
- * via AliasResolver before register() is called, so by the time this validator runs,
12
- * the definition's effective `extends` is either the canonical form (when the alias was
13
- * known) or the original alias-prefixed string (when it wasn't).
14
- *
15
- * Diagnostics:
16
- * - EXTENDS_MALFORMED: value not in "<Alias>.<Name>" alias form, or not resolvable
17
- * via the declaring file's imports (alias unknown → can't distinguish from a typo).
18
- * - EXTENDS_UNKNOWN_TARGET: alias resolves to a module, but that module has no
19
- * registered definition with the target name.
20
- * - EXTENDS_NON_ABSTRACT: target resolves to a Telo.Definition, not a Telo.Abstract.
21
- * - CAPABILITY_SHADOWS_EXTENDS (warning): `capability` names a user-declared abstract
22
- * (metadata.module !== "Telo"). Builtin lifecycle capabilities (Telo.Invocable, etc.)
23
- * never trigger this — they're lifecycle roles by design.
24
- */
25
5
  export declare function validateExtends(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver): AnalysisDiagnostic[];
26
6
  //# sourceMappingURL=validate-extends.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-extends.d.ts","sourceRoot":"","sources":["../src/validate-extends.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAOzE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,GACrB,kBAAkB,EAAE,CAwJtB"}
1
+ {"version":3,"file":"validate-extends.d.ts","sourceRoot":"","sources":["../src/validate-extends.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA8BzE,wBAAgB,eAAe,CAC7B,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,GACrB,kBAAkB,EAAE,CA4JtB"}
@@ -23,6 +23,8 @@ const EXTENDS_ALIAS_RE = /^[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*$/;
23
23
  * (metadata.module !== "Telo"). Builtin lifecycle capabilities (Telo.Invocable, etc.)
24
24
  * never trigger this — they're lifecycle roles by design.
25
25
  */
26
+ /** The built-in namespace, resolvable without a `Telo.Import`. */
27
+ const TELO_BUILTIN_ALIAS = "Telo";
26
28
  export function validateExtends(manifests, registry, aliases) {
27
29
  const diagnostics = [];
28
30
  // Defs forwarded from imported libraries carry `metadata.module` set to that
@@ -81,7 +83,11 @@ export function validateExtends(manifests, registry, aliases) {
81
83
  }
82
84
  else {
83
85
  const prefix = extendsValue.slice(0, extendsValue.indexOf("."));
84
- if (!aliases.hasAlias(prefix)) {
86
+ // `Telo` needs no import: the kernel built-ins are globally resolvable
87
+ // by design, which is what lets a sink author depend on the kernel
88
+ // contract (`extends: Telo.LogSink`) rather than on a standard-library
89
+ // module version — see kernel/specs/logging.md §10.2.
90
+ if (prefix !== TELO_BUILTIN_ALIAS && !aliases.hasAlias(prefix)) {
85
91
  diagnostics.push({
86
92
  severity: DiagnosticSeverity.Error,
87
93
  code: "EXTENDS_MALFORMED",
@@ -0,0 +1,22 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import type { AliasResolver } from "./alias-resolver.js";
3
+ import type { DefinitionRegistry } from "./definition-registry.js";
4
+ import { type AnalysisDiagnostic } from "./types.js";
5
+ /**
6
+ * Static validation of the `logging:` block — `kernel/specs/logging.md` §14.1
7
+ * and §10.3.
8
+ *
9
+ * Two things the spec explicitly says are statically detectable, so leaving them
10
+ * to a runtime failure would contradict Telo's "manifests must remain statically
11
+ * analyzable" goal:
12
+ *
13
+ * 1. **Redaction paths** parse against the closed §14 grammar. §14.1's whole
14
+ * argument for a hand-written parser is that it makes paths checkable by
15
+ * `telo check`; a bad path must fail here, not silently fail to redact at
16
+ * runtime.
17
+ * 2. **`on_full: block`** is unimplementable on a single-threaded runtime and
18
+ * §10.3 calls it "statically detectable by `telo check`". Catching it here —
19
+ * rather than only at boot — is what lets an operator fix it before shipping.
20
+ */
21
+ export declare function validateLogging(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver, aliasesByModule?: Map<string, AliasResolver>): AnalysisDiagnostic[];
22
+ //# sourceMappingURL=validate-logging.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-logging.d.ts","sourceRoot":"","sources":["../src/validate-logging.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,EACtB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,kBAAkB,EAAE,CAqBtB"}
@@ -0,0 +1,116 @@
1
+ import { parseRedactionPath, RedactionPathError } from "./redaction-path.js";
2
+ import { DiagnosticSeverity } from "./types.js";
3
+ const SOURCE = "telo-analyzer";
4
+ /**
5
+ * Static validation of the `logging:` block — `kernel/specs/logging.md` §14.1
6
+ * and §10.3.
7
+ *
8
+ * Two things the spec explicitly says are statically detectable, so leaving them
9
+ * to a runtime failure would contradict Telo's "manifests must remain statically
10
+ * analyzable" goal:
11
+ *
12
+ * 1. **Redaction paths** parse against the closed §14 grammar. §14.1's whole
13
+ * argument for a hand-written parser is that it makes paths checkable by
14
+ * `telo check`; a bad path must fail here, not silently fail to redact at
15
+ * runtime.
16
+ * 2. **`on_full: block`** is unimplementable on a single-threaded runtime and
17
+ * §10.3 calls it "statically detectable by `telo check`". Catching it here —
18
+ * rather than only at boot — is what lets an operator fix it before shipping.
19
+ */
20
+ export function validateLogging(manifests, registry, aliases, aliasesByModule) {
21
+ const diagnostics = [];
22
+ for (const manifest of manifests) {
23
+ const kind = manifest.kind;
24
+ // Redaction paths live on the `logging:` block of the root Application and on
25
+ // any `Telo.Import` / `Telo.Library` doc carrying a per-import override.
26
+ if (kind === "Telo.Application" || kind === "Telo.Import" || kind === "Telo.Library") {
27
+ validateRedactPaths(manifest, diagnostics);
28
+ }
29
+ // `on_full` lives on any sink instance — an inline `logging.sinks[]` entry
30
+ // (extracted by Phase 2 into a first-class manifest by now) or a standalone
31
+ // sink resource declared elsewhere and reached via `!ref`.
32
+ if (isSinkKind(manifest, registry, aliases, aliasesByModule)) {
33
+ validateOnFull(manifest, diagnostics);
34
+ }
35
+ }
36
+ return diagnostics;
37
+ }
38
+ function validateRedactPaths(manifest, out) {
39
+ const name = manifest.metadata?.name;
40
+ const filePath = manifest.metadata?.source;
41
+ const resource = { kind: manifest.kind, name };
42
+ for (const { block, prefix } of loggingBlocks(manifest)) {
43
+ const paths = block.redact?.paths;
44
+ if (!Array.isArray(paths))
45
+ continue;
46
+ paths.forEach((path, index) => {
47
+ // A `!cel` path is a compiled/sentinel node by now, not a string — its
48
+ // value isn't known statically, so there is nothing to parse.
49
+ if (typeof path !== "string")
50
+ return;
51
+ try {
52
+ parseRedactionPath(path);
53
+ }
54
+ catch (err) {
55
+ if (!(err instanceof RedactionPathError))
56
+ throw err;
57
+ out.push({
58
+ severity: DiagnosticSeverity.Error,
59
+ code: "INVALID_REDACTION_PATH",
60
+ source: SOURCE,
61
+ message: `${manifest.kind}/${name ?? "(unnamed)"}: ${err.message}`,
62
+ data: { resource, filePath, path: `${prefix}redact.paths[${index}]` },
63
+ });
64
+ }
65
+ });
66
+ }
67
+ }
68
+ function validateOnFull(manifest, out) {
69
+ const onFull = manifest.on_full;
70
+ if (onFull !== "block")
71
+ return;
72
+ const name = manifest.metadata?.name;
73
+ const filePath = manifest.metadata?.source;
74
+ out.push({
75
+ severity: DiagnosticSeverity.Error,
76
+ code: "LOG_SINK_ON_FULL_UNSUPPORTED",
77
+ source: SOURCE,
78
+ message: `${manifest.kind}/${name ?? "(unnamed)"}: on_full: block is not supported on a ` +
79
+ `single-threaded runtime — blocking the producer would stall the writer. ` +
80
+ `Use drop_new or drop_old.`,
81
+ data: { resource: { kind: manifest.kind, name }, filePath, path: "on_full" },
82
+ });
83
+ }
84
+ /** The root `logging:` block plus every per-import `logging:` override, each
85
+ * with the dotted path prefix a diagnostic anchors against. */
86
+ function loggingBlocks(manifest) {
87
+ const blocks = [];
88
+ const root = manifest.logging;
89
+ if (isObject(root))
90
+ blocks.push({ block: root, prefix: "logging." });
91
+ // Inline imports map: `imports.<Alias>.logging`.
92
+ const imports = manifest.imports;
93
+ if (isObject(imports)) {
94
+ for (const [alias, entry] of Object.entries(imports)) {
95
+ if (!isObject(entry))
96
+ continue;
97
+ const block = entry.logging;
98
+ if (isObject(block))
99
+ blocks.push({ block, prefix: `imports.${alias}.logging.` });
100
+ }
101
+ }
102
+ return blocks;
103
+ }
104
+ function isSinkKind(manifest, registry, aliases, aliasesByModule) {
105
+ if (typeof manifest.kind !== "string")
106
+ return false;
107
+ if (manifest.kind === "Telo.ConsoleSink" || manifest.kind === "Telo.FileSink")
108
+ return true;
109
+ const ownModule = manifest.metadata?.module;
110
+ const resolver = (ownModule ? aliasesByModule?.get(ownModule) : undefined) ?? aliases;
111
+ const canonical = resolver.resolveKind(manifest.kind) ?? manifest.kind;
112
+ return registry.resolve(canonical)?.capability === "Telo.Sink";
113
+ }
114
+ function isObject(value) {
115
+ return typeof value === "object" && value !== null && !Array.isArray(value);
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -48,7 +48,7 @@
48
48
  "@types/node": "^20.0.0",
49
49
  "typescript": "^5.0.0",
50
50
  "vitest": "^2.1.8",
51
- "@telorun/sdk": "0.48.0"
51
+ "@telorun/sdk": "0.50.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
package/src/analyzer.ts CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  } from "./validate-cel-context.js";
41
41
  import { buildEvalPaths, evalPathsCover } from "./eval-paths.js";
42
42
  import { validateExtends } from "./validate-extends.js";
43
+ import { validateLogging } from "./validate-logging.js";
43
44
  import { validateBaseMapping } from "./validate-base-mapping.js";
44
45
  import { validateNestedInlineResources } from "./validate-nested-inline.js";
45
46
  import { validateProviderCoherence } from "./validate-provider-coherence.js";
@@ -432,11 +433,17 @@ function buildStepContextSchema(
432
433
  * handler dispatched directly, e.g. `Lambda.Function`), so it can't be rejected
433
434
  * statically without false positives. This is the sound subset of the runtime rule.
434
435
  */
436
+ /** The built-in namespace: globally resolvable, crossing no import boundary. */
437
+ const TELO_BUILTIN_MODULE = "Telo";
438
+
435
439
  const NON_INVOKABLE_CAPABILITIES = new Set([
436
440
  "Telo.Provider",
437
441
  "Telo.Mount",
438
442
  "Telo.Type",
439
443
  "Telo.Template",
444
+ // A sink is written to through a direct contract on the controller instance,
445
+ // never dispatched — so invoking one is statically wrong.
446
+ "Telo.Sink",
440
447
  ]);
441
448
 
442
449
  /**
@@ -859,6 +866,12 @@ export class StaticAnalyzer {
859
866
  // so state builds up across successive calls (e.g. incremental editor validation).
860
867
  const ctx = registry?._context();
861
868
  const aliases = ctx?.aliases ?? new AliasResolver();
869
+ // `Telo` crosses no import boundary — the kernel built-ins are globally
870
+ // resolvable, which is what lets `kind: Telo.ConsoleSink` and
871
+ // `extends: Telo.LogSink` work with no `imports:` entry (§10.2). The kernel
872
+ // registers the same ungated alias at boot; registering it here keeps the
873
+ // static and runtime halves agreeing.
874
+ aliases.registerUngatedAlias(TELO_BUILTIN_MODULE, TELO_BUILTIN_MODULE);
862
875
  const defs = ctx?.definitions ?? new DefinitionRegistry();
863
876
 
864
877
  // Register module identities and aliases.
@@ -908,6 +921,7 @@ export class StaticAnalyzer {
908
921
  let libResolver = aliasesByModule.get(moduleName);
909
922
  if (!libResolver) {
910
923
  libResolver = new AliasResolver();
924
+ libResolver.registerUngatedAlias(TELO_BUILTIN_MODULE, TELO_BUILTIN_MODULE);
911
925
  aliasesByModule.set(moduleName, libResolver);
912
926
  }
913
927
  libResolver.registerUngatedAlias("Self", moduleName);
@@ -1002,6 +1016,7 @@ export class StaticAnalyzer {
1002
1016
  let libResolver = aliasesByModule.get(ownModule);
1003
1017
  if (!libResolver) {
1004
1018
  libResolver = new AliasResolver();
1019
+ libResolver.registerUngatedAlias(TELO_BUILTIN_MODULE, TELO_BUILTIN_MODULE);
1005
1020
  aliasesByModule.set(ownModule, libResolver);
1006
1021
  }
1007
1022
  if (!libResolver.hasAlias("Self")) {
@@ -1082,6 +1097,9 @@ export class StaticAnalyzer {
1082
1097
  diagnostics.push(
1083
1098
  ...validateSchemaTypeRefs(allManifests, defs, aliases, aliasesByModule, rootModules),
1084
1099
  );
1100
+ // §14.1 / §10.3: redaction paths and `on_full: block` are statically
1101
+ // detectable, so they fail `telo check` rather than only at boot.
1102
+ diagnostics.push(...validateLogging(allManifests, defs, aliases, aliasesByModule));
1085
1103
  }
1086
1104
  resolveSchemaTypeRefs(allManifests, aliases, aliasesByModule);
1087
1105
 
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
  },
package/src/index.ts CHANGED
@@ -33,6 +33,12 @@ export {
33
33
  resolveParent,
34
34
  } from "./extends-resolution.js";
35
35
  export type { DefResolver } from "./extends-resolution.js";
36
+ export {
37
+ hasIntermediateWildcard,
38
+ parseRedactionPath,
39
+ RedactionPathError,
40
+ } from "./redaction-path.js";
41
+ export type { RedactionSegment } from "./redaction-path.js";
36
42
  export { buildReferenceFieldMap, isRefEntry, isScopeEntry } from "./reference-field-map.js";
37
43
  export type { ReferenceFieldMap, RefFieldEntry } from "./reference-field-map.js";
38
44
  export { visitManifest } from "./manifest-visitor.js";
@@ -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) });
@@ -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 && !SYSTEM_KINDS.has(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) =>