|
118 | 118 | import { walkFilterFieldKeys } from './filter-walk.js'; |
119 | 119 | import { |
120 | 120 | RELATIONSHIP_FIELD_TYPES, |
| 121 | + describeFieldPathVerdict, |
121 | 122 | indexObjectGraph, |
122 | 123 | isUnjudgeable, |
123 | | - listNames, |
| 124 | + joinablePrefixes, |
124 | 125 | resolveFieldPath, |
125 | | - suggestName, |
126 | | - type FieldPathVerdict, |
127 | 126 | type ObjectGraph, |
128 | 127 | } from './object-graph.js'; |
129 | 128 |
|
@@ -172,72 +171,6 @@ function asArray(v: unknown): AnyRec[] { |
172 | 171 | return []; |
173 | 172 | } |
174 | 173 |
|
175 | | -/** |
176 | | - * The relationship prefixes a dataset declared as joinable. |
177 | | - * |
178 | | - * ADR-0021: *"Declaring `a.b` implicitly includes the intermediate `a`."* So |
179 | | - * every PREFIX of every declared path is joinable, not only the paths as |
180 | | - * written — which is why this expands rather than reading `include` verbatim. |
181 | | - */ |
182 | | -function joinablePrefixes(include: unknown): ReadonlySet<string> { |
183 | | - const prefixes = new Set<string>(); |
184 | | - if (!Array.isArray(include)) return prefixes; |
185 | | - for (const entry of include) { |
186 | | - if (typeof entry !== 'string' || !entry) continue; |
187 | | - const segments = entry.split('.'); |
188 | | - for (let i = 1; i <= segments.length; i++) { |
189 | | - prefixes.add(segments.slice(0, i).join('.')); |
190 | | - } |
191 | | - } |
192 | | - return prefixes; |
193 | | -} |
194 | | - |
195 | | -/** |
196 | | - * Turn a resolution verdict into the message half of an existence finding, or |
197 | | - * `undefined` when the verdict is one no rule may report. |
198 | | - * |
199 | | - * Shared by the three positions that resolve a field PATH (dimension, measure, |
200 | | - * filter key) so they cannot drift into three different accounts of the same |
201 | | - * miss. The caller supplies `subject` — how the position is named in prose — |
202 | | - * and owns the rule id, the path and the hint's prescription. |
203 | | - */ |
204 | | -function existenceMessage( |
205 | | - verdict: FieldPathVerdict, |
206 | | - path: string, |
207 | | - subject: string, |
208 | | -): { message: string; detail: string } | undefined { |
209 | | - switch (verdict.kind) { |
210 | | - case 'ok': |
211 | | - case 'unknowable': |
212 | | - case 'hop-untargeted': |
213 | | - return undefined; |
214 | | - case 'hop-unknown': |
215 | | - return { |
216 | | - message: |
217 | | - `${subject} "${path}" traverses "${verdict.segment}", which is not a field on object ` + |
218 | | - `"${verdict.object}".${suggestName(verdict.segment, verdict.candidates)}`, |
219 | | - detail: `Fields on "${verdict.object}": ${listNames(verdict.candidates)}.`, |
220 | | - }; |
221 | | - case 'hop-not-relationship': |
222 | | - return { |
223 | | - message: |
224 | | - `${subject} "${path}" traverses "${verdict.segment}", which is a` + |
225 | | - `${verdict.type ? ` \`${verdict.type}\`` : 'n ordinary'} field on object ` + |
226 | | - `"${verdict.object}" and not a relationship — there is nothing to join through.`, |
227 | | - detail: |
228 | | - `Only ${[...RELATIONSHIP_FIELD_TYPES].sort().join(' / ')} fields are traversable ` + |
229 | | - `(ADR-0021 derives every join from the object graph; you never write an ON clause).`, |
230 | | - }; |
231 | | - case 'field-unknown': |
232 | | - return { |
233 | | - message: |
234 | | - `${subject} "${path}" is not a field on object "${verdict.object}".` + |
235 | | - `${suggestName(verdict.field, verdict.candidates)}`, |
236 | | - detail: `Fields on "${verdict.object}": ${listNames(verdict.candidates)}.`, |
237 | | - }; |
238 | | - } |
239 | | -} |
240 | | - |
241 | 174 | /** The shared consequence sentence — why an unresolved path is not merely inert. */ |
242 | 175 | const SILENT_EMPTY = |
243 | 176 | 'The path is compiled into the analytics query as written, so it addresses a column ' + |
@@ -309,7 +242,7 @@ export function validateDatasetReferences(stack: AnyRec): DatasetRefFinding[] { |
309 | 242 | return; |
310 | 243 | } |
311 | 244 |
|
312 | | - const account = existenceMessage(verdict, entry, `include[${ii}]`); |
| 245 | + const account = describeFieldPathVerdict(verdict, entry, `include[${ii}]`); |
313 | 246 | if (!account) return; |
314 | 247 | findings.push({ |
315 | 248 | severity: 'error', |
@@ -342,7 +275,7 @@ export function validateDatasetReferences(stack: AnyRec): DatasetRefFinding[] { |
342 | 275 | const verdict = resolveFieldPath(graph, object, written); |
343 | 276 | if (isUnjudgeable(verdict) || !verdict) return; |
344 | 277 |
|
345 | | - const account = existenceMessage(verdict, written, subject); |
| 278 | + const account = describeFieldPathVerdict(verdict, written, subject); |
346 | 279 | if (account) { |
347 | 280 | findings.push({ |
348 | 281 | severity: 'error', |
|
0 commit comments