@@ -259,9 +259,19 @@ function describeObject(keys: readonly string[]): string {
259259 return `an object with no operator key (keys ${ shown } ${ more } )` ;
260260}
261261
262- /** The column, as the words name it at its position. */
263- function describeColumn ( refusal : NoOperatorObjectRefusal ) : string {
262+ /**
263+ * The column, as the words name it at its position. `named: false` is the
264+ * short form the verdict uses: the head has already named the field twice
265+ * (`filter on 'F' … at PATH.F`), and a third copy would cost the prescription
266+ * its room inside the client cap.
267+ */
268+ function describeColumn ( refusal : NoOperatorObjectRefusal , named = true ) : string {
264269 const { field, column } = refusal ;
270+ if ( ! named ) {
271+ if ( refusal . aggregated ) return `an aggregated ${ column . type } column` ;
272+ if ( column . provisioned ) return `a platform-provisioned ${ column . type } column` ;
273+ return `a ${ column . type } field` ;
274+ }
265275 if ( refusal . aggregated ) return `the aggregated column '${ field } ', which carries a ${ column . type } value` ;
266276 if ( column . provisioned ) return `the platform-provisioned ${ column . type } column '${ field } '` ;
267277 return `the declared ${ column . type } field '${ field } '` ;
@@ -271,18 +281,27 @@ function describeColumn(refusal: NoOperatorObjectRefusal): string {
271281 * The scalar kind's words (#20546), less the context and the object.
272282 *
273283 * [#20745] Every kind's words put the verdict and the route FIRST and the
274- * reasoning after: the REST door bounds a 4xx message at 500 characters by
275- * truncation (`CLIENT_MESSAGE_MAX`, `@objectstack/rest`'s
276- * `error-response.ts`), so what a caller must do next has to land inside it.
284+ * reasoning after: a client reads at most 500 characters of a 4xx message
285+ * (`CLIENT_MESSAGE_MAX`, `@objectstack/types`' `mapDataError`, cut with `…`),
286+ * so what a caller must do next has to land inside it.
287+ *
288+ * [#27337] Every kind is ordered the same way, and the cap stays: the verdict
289+ * (short — the column by its type, since the head names the field), then the
290+ * route, then "NOT applied" and the reasoning, which is the part a cut may
291+ * take. The relation kind printed its `$in` route after the reasoning, past
292+ * the cut on an aggregation's `filter` and `having`; with a long field name
293+ * every kind did. `no-operator-object-prescription-cap.test.ts` pins each
294+ * kind's route inside the cap, with margin, the same rule the temporal and
295+ * text-operator doors follow.
277296 */
278297function scalarWords ( refusal : NoOperatorObjectRefusal ) : string {
279- const { field, column } = refusal ;
298+ const { field } = refusal ;
280299 return (
281- `where a value of ${ describeColumn ( refusal ) } belongs. An object with no "$" operator is filter `
282- + 'structure, not a value . The filter was NOT applied. Compare '
283- + `' ${ field } ' with a value ({ "${ field } ": VALUE }) or an operator ({ " ${ field } ": { "$eq": VALUE } }). `
284- + `A ${ column . type } column holds scalar values — one, or a list of them — so no record can match an `
285- + 'object there, and an empty answer would read exactly like a real one.'
300+ `where a value of ${ describeColumn ( refusal , false ) } belongs. Compare ' ${ field } ' with a value `
301+ + `({ " ${ field } ": VALUE }) or an operator ({ " ${ field } ": { "$eq": VALUE } }) . The filter was NOT `
302+ + `applied. An object with no "$" operator is filter structure, not a value: ${ describeColumn ( refusal ) } `
303+ + ' holds scalar values — one, or a list of them — so no record can match an object there, and an '
304+ + 'empty answer would read exactly like a real one.'
286305 ) ;
287306}
288307
@@ -297,24 +316,26 @@ function scalarWords(refusal: NoOperatorObjectRefusal): string {
297316 * value, so neither `$in` nor `$contains` matches a member of it — so `where`
298317 * is its only route. An aggregated column (`having`) names no declaration to
299318 * read either from, so it is given the single-valued spelling.
319+ *
320+ * [#27337] The route comes right after the verdict, ahead of "NOT applied".
300321 */
301322function relationWords ( refusal : NoOperatorObjectRefusal ) : string {
302323 const { field, column } = refusal ;
303324 const def = column . def as { type : string ; multiple ?: boolean } | undefined ;
304325 const target = def === undefined ? undefined : referenceTargetOf ( def ) ;
305- const related = target === undefined ? 'the related object' : `the related object '${ target } '` ;
326+ const related = target === undefined ? 'the related object' : `'${ target } '` ;
306327 const multiple = def !== undefined && isMultiValueField ( def ) ;
307328 const position = refusal . aggregated ? "'having'" : "an aggregation's 'filter'" ;
308329 const route = multiple
309- ? `Put the condition in 'where' instead : here the stored list of ids is compared as one value, so no `
330+ ? `Move the condition to 'where': here the stored list of ids is compared as one value, so no `
310331 + 'operator matches one member of it.'
311- : `Put the condition in 'where' instead , or filter ${ related } first and match ' ${ field } ' against `
312- + `the ids it returns: { "${ field } ": { "$in": [ID, …] } }.` ;
332+ : `Move the condition to 'where', or filter ${ related } first and match the ids it returns: `
333+ + `{ "${ field } ": { "$in": [ID, …] } }.` ;
313334 return (
314- `beneath ${ describeColumn ( refusal ) } — the nested-relation form, which the engine serves in 'where' `
315- + `and not in ${ position } . The filter was NOT applied. ${ route } An object with no "$" operator is `
316- + `filter structure, not a value, here: ' ${ field } ' holds the related record's id, and an empty answer `
317- + 'would read exactly like a real one.'
335+ `beneath ${ describeColumn ( refusal , false ) } — the nested-relation form, which the engine serves in `
336+ + `'where' only. ${ route } The filter was NOT applied. The engine evaluates ${ position } itself, and `
337+ + `there ${ describeColumn ( refusal ) } holds the related record's id, so an object with no "$" operator `
338+ + 'is filter structure, not a value, and an empty answer would read exactly like a real one.'
318339 ) ;
319340}
320341
@@ -323,15 +344,18 @@ function relationWords(refusal: NoOperatorObjectRefusal): string {
323344 * answers alike: presence of the whole value, or a stored field of its own for
324345 * the part the filter is about (the dotted path into a JSON value is live on
325346 * some backends and silently empty on others, so it is not offered).
347+ *
348+ * [#27337] The route comes right after the verdict, ahead of "NOT applied".
326349 */
327350function jsonWords ( refusal : NoOperatorObjectRefusal ) : string {
328351 const { field } = refusal ;
329352 return (
330- `as the value of ${ describeColumn ( refusal ) } — a whole-value match, which the engine does not `
331- + `serve. The filter was NOT applied. Test the whole value's presence with { "${ field } ": { "$null": `
332- + 'false } }, or store the part you filter on in a field of its own and filter that field. An object '
333- + 'with no "$" operator is filter structure, not a value, and the drivers share no meaning for a '
334- + 'whole-value match: one compares the documents, another refuses the bind.'
353+ `as the value of ${ describeColumn ( refusal , false ) } — a whole-value match the engine does not serve. `
354+ + `Test the whole value's presence with { "${ field } ": { "$null": false } }, or store the part you `
355+ + 'filter on in a field of its own and filter that field. The filter was NOT applied. Here '
356+ + `${ describeColumn ( refusal ) } is matched whole: an object with no "$" operator is filter structure, `
357+ + 'not a value, and the drivers share no meaning for a whole-value match — one compares the '
358+ + 'documents, another refuses the bind.'
335359 ) ;
336360}
337361
0 commit comments