@telorun/analyzer 0.65.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.
Files changed (100) hide show
  1. package/dist/analyzer.d.ts +1 -1
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +157 -63
  4. package/dist/call-graph.d.ts.map +1 -1
  5. package/dist/call-graph.js +7 -1
  6. package/dist/cel-environment.d.ts +12 -0
  7. package/dist/cel-environment.d.ts.map +1 -1
  8. package/dist/cel-environment.js +35 -19
  9. package/dist/cel-scope.d.ts.map +1 -1
  10. package/dist/cel-scope.js +24 -4
  11. package/dist/definition-registry.d.ts +24 -0
  12. package/dist/definition-registry.d.ts.map +1 -1
  13. package/dist/definition-registry.js +41 -0
  14. package/dist/eval-paths.d.ts +68 -0
  15. package/dist/eval-paths.d.ts.map +1 -1
  16. package/dist/eval-paths.js +138 -0
  17. package/dist/flatten-for-analyzer.d.ts +1 -1
  18. package/dist/flatten-for-analyzer.js +1 -1
  19. package/dist/import-resolution-diagnostics.d.ts +1 -1
  20. package/dist/import-resolution-diagnostics.js +1 -1
  21. package/dist/index.d.ts +9 -7
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -5
  24. package/dist/manifest-visitor.d.ts.map +1 -1
  25. package/dist/manifest-visitor.js +8 -1
  26. package/dist/migrations/entries/index.d.ts.map +1 -1
  27. package/dist/migrations/entries/index.js +2 -0
  28. package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
  29. package/dist/peer-binding.d.ts +232 -0
  30. package/dist/peer-binding.d.ts.map +1 -0
  31. package/dist/peer-binding.js +418 -0
  32. package/dist/ref-slot.d.ts +13 -0
  33. package/dist/ref-slot.d.ts.map +1 -1
  34. package/dist/ref-slot.js +26 -0
  35. package/dist/reference-field-map.d.ts +28 -0
  36. package/dist/reference-field-map.d.ts.map +1 -1
  37. package/dist/reference-field-map.js +25 -0
  38. package/dist/referrer-rule.d.ts +17 -0
  39. package/dist/referrer-rule.d.ts.map +1 -1
  40. package/dist/referrer-rule.js +4 -1
  41. package/dist/release/index.d.ts +1 -1
  42. package/dist/release/index.js +1 -1
  43. package/dist/resource-rule.d.ts +40 -27
  44. package/dist/resource-rule.d.ts.map +1 -1
  45. package/dist/resource-rule.js +61 -7
  46. package/dist/rule-condition.d.ts +11 -0
  47. package/dist/rule-condition.d.ts.map +1 -1
  48. package/dist/rule-condition.js +13 -0
  49. package/dist/schema-projection.d.ts +106 -4
  50. package/dist/schema-projection.d.ts.map +1 -1
  51. package/dist/schema-projection.js +206 -18
  52. package/dist/telo-version.d.ts +1 -1
  53. package/dist/telo-version.js +1 -1
  54. package/dist/validate-cel-context.d.ts +1 -17
  55. package/dist/validate-cel-context.d.ts.map +1 -1
  56. package/dist/validate-cel-context.js +5 -70
  57. package/dist/validate-reference-forms.d.ts +10 -0
  58. package/dist/validate-reference-forms.d.ts.map +1 -1
  59. package/dist/validate-reference-forms.js +23 -0
  60. package/dist/validate-references.d.ts.map +1 -1
  61. package/dist/validate-references.js +9 -2
  62. package/dist/validate-referrer-rules.d.ts +38 -4
  63. package/dist/validate-referrer-rules.d.ts.map +1 -1
  64. package/dist/validate-referrer-rules.js +139 -31
  65. package/dist/validate-resource-rules.d.ts +11 -2
  66. package/dist/validate-resource-rules.d.ts.map +1 -1
  67. package/dist/validate-resource-rules.js +18 -12
  68. package/dist/validate-schema-projection.d.ts.map +1 -1
  69. package/dist/validate-schema-projection.js +33 -2
  70. package/dist/with-synthetic-positions.d.ts +1 -1
  71. package/dist/with-synthetic-positions.js +1 -1
  72. package/package.json +3 -3
  73. package/src/analyzer.ts +191 -68
  74. package/src/call-graph.ts +7 -1
  75. package/src/cel-environment.ts +48 -19
  76. package/src/cel-scope.ts +24 -6
  77. package/src/definition-registry.ts +42 -0
  78. package/src/eval-paths.ts +152 -0
  79. package/src/flatten-for-analyzer.ts +1 -1
  80. package/src/import-resolution-diagnostics.ts +1 -1
  81. package/src/index.ts +45 -4
  82. package/src/manifest-visitor.ts +8 -1
  83. package/src/migrations/entries/index.ts +2 -0
  84. package/src/migrations/entries/schema-prepare-bucket.json +27 -0
  85. package/src/peer-binding.ts +641 -0
  86. package/src/ref-slot.ts +36 -0
  87. package/src/reference-field-map.ts +42 -0
  88. package/src/referrer-rule.ts +20 -1
  89. package/src/release/index.ts +1 -1
  90. package/src/resource-rule.ts +69 -7
  91. package/src/rule-condition.ts +15 -0
  92. package/src/schema-projection.ts +283 -20
  93. package/src/telo-version.ts +1 -1
  94. package/src/validate-cel-context.ts +6 -71
  95. package/src/validate-reference-forms.ts +22 -0
  96. package/src/validate-references.ts +15 -2
  97. package/src/validate-referrer-rules.ts +215 -28
  98. package/src/validate-resource-rules.ts +26 -12
  99. package/src/validate-schema-projection.ts +50 -1
  100. package/src/with-synthetic-positions.ts +1 -1
@@ -24,6 +24,8 @@
24
24
  */
25
25
  import type { ResourceManifest } from "@telorun/sdk";
26
26
  import { type ReferrerRule } from "./referrer-rule.js";
27
+ import type { PeerBinder, PeerBindingFailure, PeersTarget } from "./peer-binding.js";
28
+ import { type DynamicLeaf } from "./resource-rule.js";
27
29
  export interface ReferrerRuleIssue {
28
30
  code: "REFERRER_RULE_INVALID";
29
31
  manifest: ResourceManifest;
@@ -48,7 +50,12 @@ export type ReferrerRuleFinding = {
48
50
  kind: "skipped";
49
51
  rule: ReferrerRule;
50
52
  referrer: Referrer;
51
- dynamicAt: string;
53
+ dynamic: DynamicLeaf;
54
+ } | {
55
+ kind: "unbound";
56
+ rule: ReferrerRule;
57
+ referrer: Referrer;
58
+ failure: PeerBindingFailure;
52
59
  } | {
53
60
  kind: "failed";
54
61
  rule: ReferrerRule;
@@ -60,6 +67,28 @@ export type ReferrerRuleFinding = {
60
67
  referrer: Referrer;
61
68
  elapsedMs: number;
62
69
  };
70
+ /** What a peer rule needs beyond the referrers themselves: the binder that
71
+ * resolves a referrer's collection into declarations. Absent for a host that
72
+ * cannot resolve one — a rule declaring `peers:` then reports as unbound rather
73
+ * than running with the binding missing, which would evaluate a condition
74
+ * against names that are simply not there.
75
+ *
76
+ * ONE binder per analysis, because it caches each referrer's resolved
77
+ * collection and both the evaluation and the exercised check ask for it. */
78
+ export interface ReferrerRuleContext {
79
+ readonly peerBinder?: PeerBinder;
80
+ }
81
+ /** What a `peers:` pointer names in the referrer kind it is filtered to. Defined
82
+ * with the binding vocabulary; re-exported here because this is the surface
83
+ * that consumes it. The caller resolves it — only it holds the definition
84
+ * registry, and only after every kind is registered, since a rule may name a
85
+ * kind declared later in the same file. */
86
+ export type { PeersTarget };
87
+ export interface ReferrerRuleDeclarationContext {
88
+ /** Resolves a rule's `peers:` against its `referrer:` kind. Omit to skip the
89
+ * check — the safe direction for a host with no registry. */
90
+ readonly peersTarget?: (referrerKind: string, pointer: string) => PeersTarget;
91
+ }
63
92
  /**
64
93
  * Report every way a kind's referrer-rule declarations are malformed. Runs on
65
94
  * the `Telo.Definition` / `Telo.Abstract` doc, so a defect lands on the line the
@@ -69,7 +98,7 @@ export type ReferrerRuleFinding = {
69
98
  * declaring module's scope by `resolveSchemaRefKinds`, the only pass holding
70
99
  * that scope, and a name resolving to nothing is reported from there.
71
100
  */
72
- export declare function validateReferrerRuleDeclarations(manifest: ResourceManifest): ReferrerRuleIssue[];
101
+ export declare function validateReferrerRuleDeclarations(manifest: ResourceManifest, context?: ReferrerRuleDeclarationContext): ReferrerRuleIssue[];
73
102
  /**
74
103
  * Run a kind's referrer rules against every resource that references one of its
75
104
  * resources.
@@ -79,16 +108,21 @@ export declare function validateReferrerRuleDeclarations(manifest: ResourceManif
79
108
  * manifest identity, since a name alone is module-scoped — because the condition
80
109
  * reads the two manifests and nothing about the site, so a second site could only
81
110
  * produce the identical verdict at a different path.
111
+ *
112
+ * **A rule declaring `peers:` is the exception, and evaluates once per ENTRY.**
113
+ * That is a deliberate departure from judge-once rather than an inconsistency: a
114
+ * rule that binds `entry` is about the entry, so a resource listed twice has two
115
+ * entries to answer for, and the two sites can genuinely disagree.
82
116
  */
83
117
  export declare function evaluateReferrerRules(manifest: ResourceManifest, definitionSchema: unknown, referrers: readonly Referrer[],
84
118
  /** Whether a referrer of `kind` satisfies a rule's `referrer:` filter —
85
119
  * Liskov-substitutable, so a child of the named kind matches. Supplied by the
86
120
  * caller, which holds the definition registry. */
87
- kindMatches: (filter: string, kind: string) => boolean): ReferrerRuleFinding[];
121
+ kindMatches: (filter: string, kind: string) => boolean, context?: ReferrerRuleContext): ReferrerRuleFinding[];
88
122
  /** Whether a rule found any referrer to judge — the input to the never-exercised
89
123
  * report, which is how a mistyped `referrer:` filter would otherwise disable a
90
124
  * check in silence. */
91
- export declare function referrerRuleExercised(rule: ReferrerRule, referrers: readonly Referrer[], kindMatches: (filter: string, kind: string) => boolean): boolean;
125
+ export declare function referrerRuleExercised(rule: ReferrerRule, referrers: readonly Referrer[], kindMatches: (filter: string, kind: string) => boolean, context?: ReferrerRuleContext): boolean;
92
126
  /** Where a referrer-rule finding is reported, and how loudly. Plain data, so the
93
127
  * caller pushes it exactly as it does for the resource-rule reports. */
94
128
  export interface ReferrerRuleDiagnostic {
@@ -1 +1 @@
1
- {"version":3,"file":"validate-referrer-rules.d.ts","sourceRoot":"","sources":["../src/validate-referrer-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,+EAA+E;AAC/E,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,0CAA0C;AAC1C,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3E;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAMvF;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,gBAAgB,GAAG,iBAAiB,EAAE,CA2EhG;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,gBAAgB,EAC1B,gBAAgB,EAAE,OAAO,EACzB,SAAS,EAAE,SAAS,QAAQ,EAAE;AAC9B;;mDAEmD;AACnD,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,GACrD,mBAAmB,EAAE,CAmEvB;AAED;;wBAEwB;AACxB,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,SAAS,QAAQ,EAAE,EAC9B,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,GACrD,OAAO,CAIT;AAED;yEACyE;AACzE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EACA,wBAAwB,GACxB,uBAAuB,GACvB,uBAAuB,GACvB,2BAA2B,CAAC;IAChC,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB;oEACgE;IAChE,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAKD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,gBAAgB,GAAG,SAAS,EACxC,QAAQ,EAAE,SAAS,mBAAmB,EAAE;AACxC,0EAA0E;AAC1E,iBAAiB,EAAE,OAAO,GACzB,sBAAsB,EAAE,CAmE1B;AAED;;kDAEkD;AAClD,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,gBAAgB,EAC5B,IAAI,EAAE,YAAY,GACjB,sBAAsB,CAexB"}
1
+ {"version":3,"file":"validate-referrer-rules.d.ts","sourceRoot":"","sources":["../src/validate-referrer-rules.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { REFERRER_RULES_ANNOTATION, readRawReferrerRules, readReferrerRules, } from "./referrer-rule.js";
2
- import { celSourceOf, findDynamicLeaf, readNodes } from "./resource-rule.js";
3
- import { RULE_BUDGET_MS, compileRuleCondition, conditionRefusals } from "./rule-condition.js";
2
+ import { celSourceOf, findDynamicLeaf, isTaggedCondition, pointerSegments, pointerToPath, readNodes, } from "./resource-rule.js";
3
+ import { RULE_BUDGET_MS, UNTAGGED_CONDITION, compileRuleCondition, conditionRefusals, } from "./rule-condition.js";
4
4
  function isObject(value) {
5
5
  return !!value && typeof value === "object" && !Array.isArray(value);
6
6
  }
@@ -13,7 +13,7 @@ function isObject(value) {
13
13
  * declaring module's scope by `resolveSchemaRefKinds`, the only pass holding
14
14
  * that scope, and a name resolving to nothing is reported from there.
15
15
  */
16
- export function validateReferrerRuleDeclarations(manifest) {
16
+ export function validateReferrerRuleDeclarations(manifest, context = {}) {
17
17
  const own = manifest.schema;
18
18
  const raw = readRawReferrerRules(own);
19
19
  if (raw === undefined)
@@ -65,6 +65,10 @@ export function validateReferrerRuleDeclarations(manifest) {
65
65
  if (entry.severity !== undefined && entry.severity !== "error" && entry.severity !== "warning") {
66
66
  issue(`${at}.severity`, "'severity' must be 'error' or 'warning'.");
67
67
  }
68
+ if (condition !== undefined && condition.length > 0 && !isTaggedCondition(entry.condition)) {
69
+ issue(`${at}.condition`, UNTAGGED_CONDITION);
70
+ }
71
+ validatePeersDeclaration(entry, at, issue, context);
68
72
  if (condition) {
69
73
  for (const refusal of conditionRefusals(condition))
70
74
  issue(`${at}.condition`, refusal);
@@ -72,6 +76,40 @@ export function validateReferrerRuleDeclarations(manifest) {
72
76
  });
73
77
  return issues;
74
78
  }
79
+ /** The strict half of `peers:` — the binding that lets a rule reach a referrer's
80
+ * OTHER entries, which is also the one that fails invisibly: a pointer naming a
81
+ * collection nothing in resolves would leave the rule seeing no declaration at
82
+ * all, and a check that never sees its subject reads as passing. */
83
+ function validatePeersDeclaration(entry, at, issue, context) {
84
+ const { peers } = entry;
85
+ if (peers === undefined)
86
+ return;
87
+ if (typeof peers !== "string") {
88
+ issue(`${at}.peers`, "'peers' is a JSON Pointer naming a collection OF THE REFERRER (e.g. /tables), whose " +
89
+ "other entries bind as 'peers' and whose own entry binds as 'entry'.");
90
+ return;
91
+ }
92
+ if (pointerSegments(peers) === undefined || peers === "" || peers === "/") {
93
+ issue(`${at}.peers`, `'peers' must be a JSON Pointer to a collection, e.g. /tables — '${peers}' is not one.`);
94
+ return;
95
+ }
96
+ if (typeof entry.referrer !== "string" || entry.referrer.length === 0) {
97
+ issue(`${at}.peers`, "'peers' names a collection of the REFERRER, so the rule must also declare 'referrer:' — " +
98
+ "without a kind there is nothing to check the pointer against, and a pointer that " +
99
+ "resolves to nothing disables the rule in silence.");
100
+ return;
101
+ }
102
+ const target = context.peersTarget?.(entry.referrer, peers);
103
+ if (target === "absent") {
104
+ issue(`${at}.peers`, `'${entry.referrer}' declares no collection at '${peers}'. A pointer that resolves to ` +
105
+ "nothing binds no peers, so the rule would never see a sibling declaration.");
106
+ }
107
+ else if (target === "plain") {
108
+ issue(`${at}.peers`, `'${peers}' on '${entry.referrer}' holds plain data — nothing in it is a reference, so ` +
109
+ "no entry resolves to a declaration and the rule would run against values it cannot " +
110
+ "compare. Name a collection whose items are, or contain, a reference.");
111
+ }
112
+ }
75
113
  /**
76
114
  * Run a kind's referrer rules against every resource that references one of its
77
115
  * resources.
@@ -81,19 +119,25 @@ export function validateReferrerRuleDeclarations(manifest) {
81
119
  * manifest identity, since a name alone is module-scoped — because the condition
82
120
  * reads the two manifests and nothing about the site, so a second site could only
83
121
  * produce the identical verdict at a different path.
122
+ *
123
+ * **A rule declaring `peers:` is the exception, and evaluates once per ENTRY.**
124
+ * That is a deliberate departure from judge-once rather than an inconsistency: a
125
+ * rule that binds `entry` is about the entry, so a resource listed twice has two
126
+ * entries to answer for, and the two sites can genuinely disagree.
84
127
  */
85
128
  export function evaluateReferrerRules(manifest, definitionSchema, referrers,
86
129
  /** Whether a referrer of `kind` satisfies a rule's `referrer:` filter —
87
130
  * Liskov-substitutable, so a child of the named kind matches. Supplied by the
88
131
  * caller, which holds the definition registry. */
89
- kindMatches) {
132
+ kindMatches, context = {}) {
90
133
  const rules = readReferrerRules(definitionSchema);
91
134
  if (rules.length === 0)
92
135
  return [];
93
136
  const self = manifest;
94
137
  const findings = [];
95
138
  for (const rule of rules) {
96
- const compiled = compileRuleCondition(rule.condition, ["self", "referrer"]);
139
+ const perEntry = rule.peers !== undefined;
140
+ const compiled = compileRuleCondition(rule.condition, perEntry ? ["self", "referrer", "entry", "peers"] : ["self", "referrer"]);
97
141
  if ("reason" in compiled) {
98
142
  findings.push({ kind: "failed", rule, reason: compiled.reason });
99
143
  continue;
@@ -108,29 +152,46 @@ kindMatches) {
108
152
  for (const referrer of referrers) {
109
153
  if (rule.referrer !== undefined && !kindMatches(rule.referrer, referrer.kind))
110
154
  continue;
111
- if (seen.has(referrer.manifest))
112
- continue;
113
- seen.add(referrer.manifest);
155
+ if (!perEntry) {
156
+ if (seen.has(referrer.manifest))
157
+ continue;
158
+ seen.add(referrer.manifest);
159
+ }
160
+ const scope = {
161
+ self,
162
+ referrer: referrer.manifest,
163
+ };
164
+ if (perEntry) {
165
+ const bound = context.peerBinder
166
+ ? context.peerBinder.bind(referrer.manifest, referrer.kind, rule.peers, referrer.path)
167
+ : {
168
+ ok: false,
169
+ failure: { reason: "unknown-shape", at: pointerToPath(rule.peers) },
170
+ };
171
+ if (!bound.ok) {
172
+ findings.push({ kind: "unbound", rule, referrer, failure: bound.failure });
173
+ continue;
174
+ }
175
+ scope.peers = bound.binding.peers;
176
+ scope.entry = bound.binding.entry;
177
+ }
114
178
  // Only the nodes this condition READS decide whether it can run — the
115
179
  // resource-rule reasoning, and it bites harder here: the referrer is a
116
180
  // whole manifest, so scanning all of it would disable the rule for any
117
181
  // server carrying one unrelated expression.
118
- let dynamicAt;
119
- for (const node of readNodes(chains, {
120
- self,
121
- referrer: referrer.manifest,
122
- })) {
123
- dynamicAt = findDynamicLeaf(node);
124
- if (dynamicAt !== undefined)
182
+ let dynamic;
183
+ for (const node of readNodes(chains, scope)) {
184
+ dynamic = findDynamicLeaf(node);
185
+ if (dynamic !== undefined)
125
186
  break;
126
187
  }
127
- if (dynamicAt !== undefined) {
128
- findings.push({ kind: "skipped", rule, referrer, dynamicAt });
188
+ if (dynamic !== undefined) {
189
+ findings.push({ kind: "skipped", rule, referrer, dynamic });
129
190
  continue;
130
191
  }
131
192
  let held;
132
193
  try {
133
- held = parsed({ self, referrer: referrer.manifest });
194
+ held = parsed(scope);
134
195
  }
135
196
  catch (err) {
136
197
  findings.push({
@@ -156,11 +217,38 @@ kindMatches) {
156
217
  /** Whether a rule found any referrer to judge — the input to the never-exercised
157
218
  * report, which is how a mistyped `referrer:` filter would otherwise disable a
158
219
  * check in silence. */
159
- export function referrerRuleExercised(rule, referrers, kindMatches) {
160
- if (rule.referrer === undefined)
161
- return referrers.length > 0;
162
- const filter = rule.referrer;
163
- return referrers.some((referrer) => kindMatches(filter, referrer.kind));
220
+ export function referrerRuleExercised(rule, referrers, kindMatches, context = {}) {
221
+ const matching = rule.referrer === undefined
222
+ ? referrers
223
+ : referrers.filter((referrer) => kindMatches(rule.referrer, referrer.kind));
224
+ if (matching.length === 0)
225
+ return false;
226
+ if (rule.peers === undefined)
227
+ return true;
228
+ // A peer rule that ran against an EMPTY peer set proved nothing — and an empty
229
+ // set everywhere is exactly what a typo in `peers:` looks like from outside,
230
+ // which is silence that reads as passing. Asked through the binder, so it
231
+ // shares the resolution the evaluation already paid for.
232
+ const binder = context.peerBinder;
233
+ if (!binder)
234
+ return false;
235
+ return matching.some((referrer) => binder.hasPeers(referrer.manifest, referrer.kind, rule.peers, referrer.path));
236
+ }
237
+ /** Why a peer binding could not be produced, as the sentence a reader acts on. */
238
+ function peerBindingReason(failure) {
239
+ switch (failure.reason) {
240
+ case "no-collection":
241
+ return `'${failure.at}' holds no collection to bind 'peers' from.`;
242
+ case "unresolved":
243
+ return (`a reference at '${failure.at}' names a declaration this analysis does not hold, ` +
244
+ "so a peer would bind to nothing.");
245
+ case "dynamic":
246
+ return (`a value at '${failure.at}' holds ${failure.what ?? "a value"}, which is not known ` +
247
+ "until the resource is created, so the comparison would run against a placeholder.");
248
+ case "unknown-shape":
249
+ return (`which paths under '${failure.at}' hold references is not known here, so nothing ` +
250
+ "could be resolved into a declaration.");
251
+ }
164
252
  }
165
253
  const nameOf = (manifest) => manifest.metadata?.name ?? "<unnamed>";
166
254
  /**
@@ -196,8 +284,8 @@ declarationIsOurs) {
196
284
  severity: "information",
197
285
  message: `${finding.referrer.kind}/${nameOf(finding.referrer.manifest)}: rule ` +
198
286
  `'${finding.rule.code}' from ${declaringKind} did not run at ` +
199
- `'${finding.referrer.path}' — the value holds a CEL expression at ` +
200
- `'${finding.dynamicAt}', which is not known until the resource is created. ` +
287
+ `'${finding.referrer.path}' — the value holds ${finding.dynamic.what} at ` +
288
+ `'${finding.dynamic.path}', which is not known until the resource is created. ` +
201
289
  "Reported rather than dropped: a check whose coverage varies invisibly reads as passing.",
202
290
  manifest: finding.referrer.manifest,
203
291
  path: finding.referrer.path,
@@ -205,6 +293,20 @@ declarationIsOurs) {
205
293
  });
206
294
  continue;
207
295
  }
296
+ if (finding.kind === "unbound") {
297
+ out.push({
298
+ code: "REFERRER_RULE_SKIPPED",
299
+ severity: "information",
300
+ message: `${finding.referrer.kind}/${nameOf(finding.referrer.manifest)}: rule ` +
301
+ `'${finding.rule.code}' from ${declaringKind} did not run at ` +
302
+ `'${finding.referrer.path}' — ${peerBindingReason(finding.failure)}` +
303
+ " Reported rather than dropped: a check whose coverage varies invisibly reads as passing.",
304
+ manifest: finding.referrer.manifest,
305
+ path: finding.referrer.path,
306
+ rule: finding.rule.code,
307
+ });
308
+ continue;
309
+ }
208
310
  const because = finding.kind === "failed"
209
311
  ? `failed to evaluate: ${finding.reason}. Guard an optional field with 'in' or '.?'.`
210
312
  : `exceeded its evaluation budget (${finding.elapsedMs}ms) and was stopped, so ` +
@@ -242,12 +344,18 @@ export function reportUnexercisedReferrerRule(definition, rule) {
242
344
  return {
243
345
  code: "REFERRER_RULE_UNEXERCISED",
244
346
  severity: "information",
245
- message: `Referrer rule '${rule.code}' never ran: nothing` +
246
- (rule.referrer === undefined ? "" : ` of kind '${rule.referrer}'`) +
247
- " references a resource of this kind, so nothing has proven the condition." +
248
- (rule.referrer === undefined
249
- ? ""
250
- : " A 'referrer' naming a kind no manifest uses disables the rule in silence."),
347
+ message: rule.peers !== undefined
348
+ ? `Referrer rule '${rule.code}' never ran with peers to compare: nothing` +
349
+ (rule.referrer === undefined ? "" : ` of kind '${rule.referrer}'`) +
350
+ ` references a resource of this kind while '${rule.peers}' held another ` +
351
+ "declaration, so nothing has proven the condition. An empty peer set " +
352
+ "everywhere is what a typo in 'peers:' looks like from outside."
353
+ : `Referrer rule '${rule.code}' never ran: nothing` +
354
+ (rule.referrer === undefined ? "" : ` of kind '${rule.referrer}'`) +
355
+ " references a resource of this kind, so nothing has proven the condition." +
356
+ (rule.referrer === undefined
357
+ ? ""
358
+ : " A 'referrer' naming a kind no manifest uses disables the rule in silence."),
251
359
  manifest: definition,
252
360
  path: `schema.${REFERRER_RULES_ANNOTATION}[${rule.index}]`,
253
361
  rule: rule.code,
@@ -22,7 +22,7 @@
22
22
  * Browser-safe: no Node built-ins.
23
23
  */
24
24
  import type { ResourceManifest } from "@telorun/sdk";
25
- import { type ResourceRule } from "./resource-rule.js";
25
+ import { type DynamicLeaf, type ResourceRule } from "./resource-rule.js";
26
26
  /** Published name for the shared budget — see `rule-condition.ts`. */
27
27
  export declare const RESOURCE_RULE_BUDGET_MS = 50;
28
28
  export interface ResourceRuleIssue {
@@ -42,7 +42,7 @@ export type ResourceRuleFinding = {
42
42
  kind: "skipped";
43
43
  rule: ResourceRule;
44
44
  path: string;
45
- dynamicAt: string;
45
+ dynamic: DynamicLeaf;
46
46
  } | {
47
47
  kind: "failed";
48
48
  rule: ResourceRule;
@@ -54,6 +54,15 @@ export type ResourceRuleFinding = {
54
54
  path: string;
55
55
  elapsedMs: number;
56
56
  };
57
+ /** Navigate a kind's own schema to the node describing what a pointer names, so
58
+ * an `in:` naming a field the kind does not declare is caught at the kind.
59
+ * Exported because the peer-rule strict half asks the same question of the
60
+ * REFERRER's schema, and two navigators would eventually disagree about what a
61
+ * pointer names. */
62
+ export declare function schemaAtPointer(schema: unknown, pointer: string): unknown;
63
+ /** True when a schema node describes something a rule can iterate. Unknown or
64
+ * absent `type` passes: an open schema is not evidence of a defect. */
65
+ export declare function isIterableSchema(node: unknown): boolean;
57
66
  /**
58
67
  * Report every way a kind's rule declarations are malformed. Runs on the
59
68
  * `Telo.Definition` / `Telo.Abstract` doc, so a defect lands on the line the
@@ -1 +1 @@
1
- {"version":3,"file":"validate-resource-rules.d.ts","sourceRoot":"","sources":["../src/validate-resource-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EASL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAE5B,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,KAAiB,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,0CAA0C;AAC1C,MAAM,MAAM,mBAAmB,GAC3B;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,YAAY,CAAC;IACnB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAmCjF;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,gBAAgB;AAC1B;;;;;;;;;GASG;AACH,eAAe,CAAC,EAAE,OAAO,GACxB,iBAAiB,EAAE,CA+FrB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,gBAAgB,EAC1B,gBAAgB,EAAE,OAAO,GACxB,mBAAmB,EAAE,CA4DvB;AAED;iFACiF;AACjF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAIrF;AAED;;gFAEgF;AAChF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EACA,wBAAwB,GACxB,uBAAuB,GACvB,uBAAuB,GACvB,2BAA2B,CAAC;IAChC,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB;+EAC2E;IAC3E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,gBAAgB,GAAG,SAAS,EACxC,QAAQ,EAAE,SAAS,mBAAmB,EAAE;AACxC,0EAA0E;AAC1E,iBAAiB,EAAE,OAAO,GACzB,sBAAsB,EAAE,CA2D1B;AAED;+CAC+C;AAC/C,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,gBAAgB,EAC5B,IAAI,EAAE,YAAY,GACjB,sBAAsB,CAYxB"}
1
+ {"version":3,"file":"validate-resource-rules.d.ts","sourceRoot":"","sources":["../src/validate-resource-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAOrD,OAAO,EAIL,KAAK,WAAW,EAOhB,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAE5B,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,KAAiB,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,0CAA0C;AAC1C,MAAM,MAAM,mBAAmB,GAC3B;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,YAAY,CAAC;IACnB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAC3E;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAMjF;;;;qBAIqB;AACrB,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAezE;AAED;wEACwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAMvD;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,gBAAgB;AAC1B;;;;;;;;;GASG;AACH,eAAe,CAAC,EAAE,OAAO,GACxB,iBAAiB,EAAE,CAmGrB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,gBAAgB,EAC1B,gBAAgB,EAAE,OAAO,GACxB,mBAAmB,EAAE,CA4DvB;AAED;iFACiF;AACjF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAIrF;AAED;;gFAEgF;AAChF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EACA,wBAAwB,GACxB,uBAAuB,GACvB,uBAAuB,GACvB,2BAA2B,CAAC;IAChC,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB;+EAC2E;IAC3E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,gBAAgB,GAAG,SAAS,EACxC,QAAQ,EAAE,SAAS,mBAAmB,EAAE;AACxC,0EAA0E;AAC1E,iBAAiB,EAAE,OAAO,GACzB,sBAAsB,EAAE,CA2D1B;AAED;+CAC+C;AAC/C,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,gBAAgB,EAC5B,IAAI,EAAE,YAAY,GACjB,sBAAsB,CAYxB"}
@@ -1,13 +1,16 @@
1
- import { RULE_BUDGET_MS, compileRuleCondition, conditionRefusals } from "./rule-condition.js";
2
- import { RESOURCE_RULES_ANNOTATION, celSourceOf, findDynamicLeaf, pointerSegments, readRawResourceRules, readResourceRules, readNodes, resolveRuleSubjects, } from "./resource-rule.js";
1
+ import { RULE_BUDGET_MS, UNTAGGED_CONDITION, compileRuleCondition, conditionRefusals, } from "./rule-condition.js";
2
+ import { RESOURCE_RULES_ANNOTATION, celSourceOf, findDynamicLeaf, isTaggedCondition, pointerSegments, readRawResourceRules, readResourceRules, readNodes, resolveRuleSubjects, } from "./resource-rule.js";
3
3
  /** Published name for the shared budget — see `rule-condition.ts`. */
4
4
  export const RESOURCE_RULE_BUDGET_MS = RULE_BUDGET_MS;
5
5
  function isObject(value) {
6
6
  return !!value && typeof value === "object" && !Array.isArray(value);
7
7
  }
8
8
  /** Navigate a kind's own schema to the node describing what a pointer names, so
9
- * an `in:` naming a field the kind does not declare is caught at the kind. */
10
- function schemaAtPointer(schema, pointer) {
9
+ * an `in:` naming a field the kind does not declare is caught at the kind.
10
+ * Exported because the peer-rule strict half asks the same question of the
11
+ * REFERRER's schema, and two navigators would eventually disagree about what a
12
+ * pointer names. */
13
+ export function schemaAtPointer(schema, pointer) {
11
14
  const segments = pointerSegments(pointer);
12
15
  if (!segments)
13
16
  return undefined;
@@ -27,7 +30,7 @@ function schemaAtPointer(schema, pointer) {
27
30
  }
28
31
  /** True when a schema node describes something a rule can iterate. Unknown or
29
32
  * absent `type` passes: an open schema is not evidence of a defect. */
30
- function isIterableSchema(node) {
33
+ export function isIterableSchema(node) {
31
34
  if (!isObject(node))
32
35
  return true;
33
36
  const type = node.type;
@@ -121,6 +124,9 @@ effectiveSchema) {
121
124
  }
122
125
  }
123
126
  }
127
+ if (condition !== undefined && condition.length > 0 && !isTaggedCondition(entry.condition)) {
128
+ issue(`${at}.condition`, UNTAGGED_CONDITION);
129
+ }
124
130
  if (condition) {
125
131
  for (const refusal of conditionRefusals(condition))
126
132
  issue(`${at}.condition`, refusal);
@@ -159,14 +165,14 @@ export function evaluateResourceRules(manifest, definitionSchema) {
159
165
  // Only the nodes this condition READS decide whether it can run — see
160
166
  // `readNodes`. Scanning the whole subject would disable every
161
167
  // resource-wide rule on any manifest containing one unrelated expression.
162
- let dynamicAt;
168
+ let dynamic;
163
169
  for (const node of readNodes(chains, { self, this: subject.value })) {
164
- dynamicAt = findDynamicLeaf(node);
165
- if (dynamicAt !== undefined)
170
+ dynamic = findDynamicLeaf(node);
171
+ if (dynamic !== undefined)
166
172
  break;
167
173
  }
168
- if (dynamicAt !== undefined) {
169
- findings.push({ kind: "skipped", rule, path: subject.path, dynamicAt });
174
+ if (dynamic !== undefined) {
175
+ findings.push({ kind: "skipped", rule, path: subject.path, dynamic });
170
176
  continue;
171
177
  }
172
178
  let held;
@@ -239,8 +245,8 @@ declarationIsOurs) {
239
245
  code: "RESOURCE_RULE_SKIPPED",
240
246
  severity: "information",
241
247
  message: `${manifest.kind}/${name}: rule '${finding.rule.code}' did not run` +
242
- `${at ? ` at '${at}'` : ""} — the value holds a CEL expression at ` +
243
- `'${finding.dynamicAt}', which is not known until the resource is created. ` +
248
+ `${at ? ` at '${at}'` : ""} — the value holds ${finding.dynamic.what} at ` +
249
+ `'${finding.dynamic.path}', which is not known until the resource is created. ` +
244
250
  "Reported rather than dropped: a check whose coverage varies invisibly reads as passing.",
245
251
  manifest,
246
252
  path: at,
@@ -1 +1 @@
1
- {"version":3,"file":"validate-schema-projection.d.ts","sourceRoot":"","sources":["../src/validate-schema-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQrD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,2BAA2B,GAAG,uBAAuB,CAAC;IAC5D,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,qBAAqB,EAAE,CA2G5F"}
1
+ {"version":3,"file":"validate-schema-projection.d.ts","sourceRoot":"","sources":["../src/validate-schema-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AASrD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,2BAA2B,GAAG,uBAAuB,CAAC;IAC5D,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,qBAAqB,EAAE,CAqJ5F"}
@@ -1,4 +1,4 @@
1
- import { projectionKeyMap, rawSchemaProjection, readSchemaProjection, schemaProjectionIsMisplaced, } from "./schema-projection.js";
1
+ import { projectionKeyMap, rawSchemaProjection, readSchemaProjection, schemaMapBranch, schemaProjectionIsMisplaced, } from "./schema-projection.js";
2
2
  const PROJECTION = "x-telo-schema-projection";
3
3
  function isObject(value) {
4
4
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -52,6 +52,30 @@ export function validateSchemaProjection(manifest) {
52
52
  `entry's identity — without it the projection yields a schema that rejects every value.`),
53
53
  ];
54
54
  }
55
+ const reference = raw.reference;
56
+ if (reference !== undefined) {
57
+ if (!isObject(reference)) {
58
+ issues.push(issue("SCHEMA_PROJECTION_INVALID", `${base}.reference`, `'reference' says how an entry whose '${projection.key}' holds a REFERENCE projects. ` +
59
+ `It is an object naming 'from' (the target field to read), 'keyword' (the schema ` +
60
+ `keyword its values become) and one of 'base' / 'baseFrom'.`));
61
+ }
62
+ else {
63
+ const absent = ["from", "keyword"].filter((f) => typeof reference[f] !== "string");
64
+ if (absent.length > 0) {
65
+ issues.push(issue("SCHEMA_PROJECTION_INVALID", `${base}.reference`, `'reference' is missing ${absent.map((f) => `'${f}'`).join(" and ")}. Without them a ` +
66
+ `referenced entry projects to nothing, so it vanishes from every consumer's view.`));
67
+ }
68
+ const bases = ["base", "baseFrom"].filter((f) => reference[f] !== undefined);
69
+ if (bases.length !== 1) {
70
+ issues.push(issue("SCHEMA_PROJECTION_INVALID", `${base}.reference`, bases.length === 0
71
+ ? `'reference' declares neither 'base' nor 'baseFrom', so the projected node has no ` +
72
+ `type. Write 'base' where the named type IS its own base type, or 'baseFrom' ` +
73
+ `naming the target field that holds a value of this kind's own map.`
74
+ : `'reference' declares both 'base' and 'baseFrom'. They are two answers to one ` +
75
+ `question — where the projected node's type comes from — so declare exactly one.`));
76
+ }
77
+ }
78
+ }
55
79
  const map = projectionKeyMap(schema, projection);
56
80
  if (!map) {
57
81
  return [
@@ -106,5 +130,12 @@ function keySchema(schema, entries, key) {
106
130
  if (!isObject(entry) || !isObject(entry.properties))
107
131
  return undefined;
108
132
  const field = entry.properties[key];
109
- return isObject(field) ? field : undefined;
133
+ if (!isObject(field))
134
+ return undefined;
135
+ // The vocabulary may sit on a BRANCH — a slot unioning a closed value set with
136
+ // a reference — and the branch carrying the map is the one whose `enum` the map
137
+ // has to answer for. Reading the union node instead finds no `enum` at all, so
138
+ // the completeness check would silently stop running exactly where the two
139
+ // halves can disagree. Asked through the single reader, never re-derived here.
140
+ return schemaMapBranch(field) ?? field;
110
141
  }
@@ -8,7 +8,7 @@ import type { ResourceManifest } from "@telorun/sdk";
8
8
  * manifest (the dedup that backs `DUPLICATE_RESOURCE_NAME` reads
9
9
  * `(source, sourceLine)` to distinguish pipeline echoes from real
10
10
  * collisions). Production callers — the `Loader`, `flattenForAnalyzer`,
11
- * the telo-editor's `emitDocsFor`, the VSCode extension — all stamp
11
+ * telo studio's `emitDocsFor`, the VSCode extension — all stamp
12
12
  * positions already. This helper is the escape hatch for **programmatic
13
13
  * callers** (tests, ad-hoc scripts) that construct `ResourceManifest`
14
14
  * literals without going through a loader: it gives every otherwise-naked
@@ -8,7 +8,7 @@ import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
8
8
  * manifest (the dedup that backs `DUPLICATE_RESOURCE_NAME` reads
9
9
  * `(source, sourceLine)` to distinguish pipeline echoes from real
10
10
  * collisions). Production callers — the `Loader`, `flattenForAnalyzer`,
11
- * the telo-editor's `emitDocsFor`, the VSCode extension — all stamp
11
+ * telo studio's `emitDocsFor`, the VSCode extension — all stamp
12
12
  * positions already. This helper is the escape hatch for **programmatic
13
13
  * callers** (tests, ad-hoc scripts) that construct `ResourceManifest`
14
14
  * literals without going through a loader: it gives every otherwise-naked
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -43,13 +43,13 @@
43
43
  "jsonpath-plus": "^10.3.0",
44
44
  "packageurl-js": "^2.0.1",
45
45
  "yaml": "^2.8.3",
46
- "@telorun/templating": "0.16.0"
46
+ "@telorun/templating": "0.17.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^20.0.0",
50
50
  "typescript": "^5.0.0",
51
51
  "vitest": "^2.1.8",
52
- "@telorun/sdk": "0.82.0"
52
+ "@telorun/sdk": "0.82.1"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@telorun/sdk": "*"