@ultimat3/query 30.5.0 → 30.7.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/CLAUDE.md CHANGED
@@ -16,6 +16,7 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
16
16
  | `read.ts` | **the one read path** (`runQuery`, `sourceFor`) + the private declaration store `sql` lives in |
17
17
  | `contract-test.ts` | `.contract()`, the generated suite. The three `X_CONTRACT_*` codes and `X_CONTRACT_DRIFT` are borrowed (`QUERY_BORROWED_ERROR_CODES`) |
18
18
  | `exercise.ts` | the exercise twin. "Reached" means `sql()` built a source, read off the refusal (`beforeSql`), because `read.ts` keeps no counter. No transaction: a read writes nothing |
19
+ | `exercise-declarations.ts` | action's twin: `.contract({ exercise })` takes the app's `defineExercise()` value and reads the entry by the query's name (`apps/web/exercise.ts`, the one home) |
19
20
  | `declared-errors.ts` | `judgedSql` / `judgedRead`, around `sql` and the source's `execute` in `read.ts`. `errors:` is checked where it is lifted (`facade.ts`), not in `query.ts` |
20
21
  | `declared-errors-contract.ts`, `openapi-errors.ts` | `.contract()`'s source scan; core's builder bound to http's status |
21
22
  | `http-pin.ts` | a read's pinned `http.path`, its check at `query()`, `servedQueryPath`, `queryHttpPinOf` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/query",
3
- "version": "30.5.0",
3
+ "version": "30.7.0",
4
4
  "description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",
5
5
  "keywords": [
6
6
  "ultimate",
@@ -47,12 +47,12 @@
47
47
  "test": "bun test"
48
48
  },
49
49
  "dependencies": {
50
- "@ultimat3/cache": "30.5.0",
51
- "@ultimat3/core": "30.5.0",
52
- "@ultimat3/db": "30.5.0",
53
- "@ultimat3/entity": "30.5.0",
54
- "@ultimat3/http": "30.5.0",
55
- "@ultimat3/policy": "30.5.0",
56
- "@ultimat3/schema": "30.5.0"
50
+ "@ultimat3/cache": "30.7.0",
51
+ "@ultimat3/core": "30.7.0",
52
+ "@ultimat3/db": "30.7.0",
53
+ "@ultimat3/entity": "30.7.0",
54
+ "@ultimat3/http": "30.7.0",
55
+ "@ultimat3/policy": "30.7.0",
56
+ "@ultimat3/schema": "30.7.0"
57
57
  }
58
58
  }
@@ -7,8 +7,9 @@
7
7
  import type { Ctx } from '@ultimat3/core';
8
8
  import { admitsAnonymous } from '@ultimat3/policy';
9
9
  import { declaredErrorsContract } from './declared-errors-contract';
10
- import type { QueryExerciseOptions } from './exercise';
11
10
  import { exerciseQuery, queryExerciseTestName, queryExerciseUndeclared } from './exercise';
11
+ import type { QueryContractExercise } from './exercise-declarations';
12
+ import { isQueryExerciseDeclarations, resolveQueryExercise } from './exercise-declarations';
12
13
  import type { AnyQuery } from './query';
13
14
  import { queryName } from './read';
14
15
 
@@ -20,10 +21,12 @@ export interface QueryContractTest {
20
21
 
21
22
  export interface QueryContractTestOptions {
22
23
  /**
23
- * Who the generated runs read as — required unless the policy admits an anonymous reader — or
24
- * `{ off: '<reason>' }`, which skips the runs out loud: the reason is in the test's name.
24
+ * Who the generated runs read as. In an app, the `exercise` export of `apps/web/exercise.ts` —
25
+ * the `defineExercise()` value `x exercise` reads — and this query's entry in it is the one
26
+ * used: one declaration, both runners. One entry inline (`{ actor }`, `{ off: '<reason>' }`) is
27
+ * for a test binding rows of its own world. Required unless the policy admits an anonymous reader.
25
28
  */
26
- readonly exercise?: QueryExerciseOptions;
29
+ readonly exercise?: QueryContractExercise;
27
30
  /** The context each run starts from; the actor is swapped in, the rest kept. */
28
31
  readonly ctx?: Ctx;
29
32
  }
@@ -37,7 +40,7 @@ export function queryContractTestsFor(
37
40
 
38
41
  function exerciseContract(target: AnyQuery, options: QueryContractTestOptions): QueryContractTest {
39
42
  const name = queryName(target);
40
- const exercise = options.exercise;
43
+ const exercise = resolveQueryExercise(name, options.exercise);
41
44
  if (exercise !== undefined && 'off' in exercise) {
42
45
  const reason = exercise.off.trim();
43
46
  if (reason.length > 0) {
@@ -45,14 +48,21 @@ function exerciseContract(target: AnyQuery, options: QueryContractTestOptions):
45
48
  }
46
49
  return {
47
50
  name: queryExerciseTestName(name),
48
- run: () => Promise.reject(queryExerciseUndeclared(name, true)),
51
+ run: () => Promise.reject(queryExerciseUndeclared(name, 'blank')),
49
52
  };
50
53
  }
51
54
  const chosen = exercise ?? (admitsAnonymous(target.policy) ? { actor: null } : undefined);
52
55
  return {
53
56
  name: queryExerciseTestName(name),
54
57
  run: async () => {
55
- if (chosen === undefined) throw queryExerciseUndeclared(name, false);
58
+ if (chosen === undefined) {
59
+ const declared = options.exercise;
60
+ const why =
61
+ declared !== undefined && isQueryExerciseDeclarations(declared)
62
+ ? 'no-entry'
63
+ : 'undeclared';
64
+ throw queryExerciseUndeclared(name, why);
65
+ }
56
66
  await exerciseQuery(target, chosen, options.ctx);
57
67
  },
58
68
  };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * One declaration of a read's exercise: the app's `defineExercise()` value (apps/web/exercise.ts,
3
+ * the file `x exercise` reads), handed whole to `.contract({ exercise })`, resolved here by the
4
+ * query's own name. Action's twin (`exercise-declarations.ts` there): a sideways import is refused.
5
+ */
6
+
7
+ import type { QueryExerciseOptions } from './exercise';
8
+
9
+ export type { QueryExerciseOptions };
10
+
11
+ /** The one file an app declares its exercise in — every refusal's fix names it. */
12
+ export const QUERY_EXERCISE_DECLARATIONS_FILE = 'apps/web/exercise.ts';
13
+
14
+ /** `defineExercise()`'s value, as far as a contract reads it: the entries, by primitive name. */
15
+ export interface QueryExerciseDeclarations {
16
+ readonly kind: 'exercise-overrides';
17
+ readonly entries: ReadonlyMap<string, QueryExerciseOptions>;
18
+ }
19
+
20
+ /** What `.contract({ exercise })` takes: the app's declarations, or one entry inline. */
21
+ export type QueryContractExercise = QueryExerciseOptions | QueryExerciseDeclarations;
22
+
23
+ export const isQueryExerciseDeclarations = (
24
+ value: QueryContractExercise,
25
+ ): value is QueryExerciseDeclarations => 'kind' in value && value.kind === 'exercise-overrides';
26
+
27
+ /** The entry `name` reads: the declarations' own, or the inline one. `undefined` when none. */
28
+ export function resolveQueryExercise(
29
+ name: string,
30
+ exercise: QueryContractExercise | undefined,
31
+ ): QueryExerciseOptions | undefined {
32
+ if (exercise === undefined || !isQueryExerciseDeclarations(exercise)) return exercise;
33
+ return exercise.entries.get(name);
34
+ }
package/src/exercise.ts CHANGED
@@ -10,23 +10,32 @@ import {
10
10
  ctxOf,
11
11
  exerciseReplay,
12
12
  isUltimateError,
13
+ renderDrawnInput,
14
+ renderExerciseKey,
13
15
  renderFixShellArg,
14
16
  runExercise,
17
+ sampleGapBinding,
15
18
  tryUseContext,
16
19
  UltimateError,
17
20
  } from '@ultimat3/core';
18
21
  import { statusFor, withScratchRateLimitStore } from '@ultimat3/http';
22
+ import { admitsAnonymous } from '@ultimat3/policy';
19
23
  import type { StandardSchemaV1 } from '@ultimat3/schema';
20
24
  import { validateAsync } from '@ultimat3/schema';
21
- import { arbitrary } from '@ultimat3/schema/sample';
25
+ import { arbitrary, describeSampleGap, sampleGaps } from '@ultimat3/schema/sample';
22
26
  import { QueryAuditSinkMissingError } from './audit-errors';
23
27
  import { QueryDeniedError } from './errors';
28
+ import { QUERY_EXERCISE_DECLARATIONS_FILE } from './exercise-declarations';
24
29
  import type { AnyQuery } from './query';
25
30
  import { queryName, sourceFor } from './read';
26
31
 
27
- /** Who reads, and what binds the parts a generated value cannot. Same shape as an action's. */
32
+ /**
33
+ * Who reads, and what binds the parts a generated value cannot. Same shape as an action's: an
34
+ * omitted `actor` is `x exercise`'s pick, and here the signed-out reader where the policy admits
35
+ * one, else `X_CONTRACT_EXERCISE_UNDECLARED`.
36
+ */
28
37
  export interface QueryExerciseActorOptions {
29
- readonly actor: Actor | null;
38
+ readonly actor?: Actor | null | undefined;
30
39
  readonly input?: (drawn: DrawnInput) => unknown;
31
40
  readonly runs?: number;
32
41
  }
@@ -72,6 +81,7 @@ export async function exerciseQuery(
72
81
  ctx?: Ctx,
73
82
  ): Promise<QueryExerciseReport> {
74
83
  const name = queryName(target);
84
+ const actor = readerOf(target, options, name);
75
85
  const base = ctx ?? tryUseContext() ?? ctxOf({});
76
86
  // Counted in a scratch store: a spent bucket would make the file's next real call a 429.
77
87
  const verdict = await withScratchRateLimitStore(() =>
@@ -86,7 +96,7 @@ export async function exerciseQuery(
86
96
  try {
87
97
  const source = await sourceFor(target, input, {
88
98
  ctx: base,
89
- actor: options.actor,
99
+ actor,
90
100
  surface: 'http',
91
101
  });
92
102
  await source.execute();
@@ -102,7 +112,7 @@ export async function exerciseQuery(
102
112
  case 'pass':
103
113
  return verdict.report;
104
114
  case 'drift':
105
- throw queryDrawsRefused(name);
115
+ throw queryDrawsRefused(name, target.input);
106
116
  case 'unreached':
107
117
  throw queryUnreached(name, verdict.outcomes);
108
118
  case 'defect':
@@ -134,27 +144,68 @@ function queryUnreached(name: string, outcomes: readonly string[]): UltimateErro
134
144
  return new UltimateError({
135
145
  code: 'X_CONTRACT_EXERCISE_UNREACHED',
136
146
  cause: `none of ${outcomes.length} generated runs of query ${name} reached its sql() (meta.outcomes lists what each one hit)`,
137
- fix: `${renderFixShellArg(name, '<name>')}.contract({ exercise: { actor } }) with an actor the policy permits`,
147
+ fix: `defineExercise({ ${renderFixShellArg(name, '<name>')}: { actor } }) # in ${QUERY_EXERCISE_DECLARATIONS_FILE}, an actor the policy permits`,
138
148
  meta: { outcomes },
139
149
  });
140
150
  }
141
151
 
142
- function queryDrawsRefused(name: string): UltimateError {
152
+ /** An entry's reader; none declared is the signed-out reader where the policy admits one. */
153
+ function readerOf(target: AnyQuery, options: QueryExerciseActorOptions, name: string) {
154
+ if (options.actor !== undefined) return options.actor;
155
+ if (admitsAnonymous(target.policy)) return null;
156
+ throw queryExerciseUndeclared(name, 'no-actor');
157
+ }
158
+
159
+ /** Action's `everyDrawRefused`, for a read: the field no draw can fill, named and bound. */
160
+ function queryDrawsRefused(name: string, schema: StandardSchemaV1): UltimateError {
161
+ const gaps = sampleGaps(schema);
162
+ const described = gaps.map((path) => describeSampleGap(schema, path));
163
+ const input =
164
+ gaps.length === 0
165
+ ? "(drawn) => ({ ...drawn, field: '<a value the refinement accepts>' })"
166
+ : renderDrawnInput(gaps.map((path, i) => sampleGapBinding(path, described[i] ?? path)));
143
167
  return new UltimateError({
144
168
  code: 'X_CONTRACT_DRIFT',
145
- cause: `query ${name}: its input schema refused every generated value, so the read was never exercised`,
146
- fix: `${renderFixShellArg(name, '<name>')}.contract({ exercise: { actor, input: (drawn) => ({ ...drawn, field: '<a value the refinement accepts>' }) } })`,
169
+ cause:
170
+ gaps.length === 0
171
+ ? `query ${name}: its input schema refused every generated value, so the read was never exercised`
172
+ : `query ${name}: its input schema refused every generated value — no value can be generated for ${described.join(', ')} — so the read was never exercised`,
173
+ fix: `defineExercise({ ${renderExerciseKey(name)}: { input: ${input} } }) # in ${QUERY_EXERCISE_DECLARATIONS_FILE}`,
147
174
  });
148
175
  }
149
176
 
150
- /** A guarded read with no exercise said, or an opt-out with a blank reason. */
151
- export function queryExerciseUndeclared(name: string, blankReason: boolean): UltimateError {
177
+ /** Why no exercise could run: action's four (`ExerciseUndeclaredWhy`), for a read. */
178
+ export type QueryExerciseUndeclaredWhy = 'blank' | 'undeclared' | 'no-entry' | 'no-actor';
179
+
180
+ /**
181
+ * A guarded read with no exercise. The fix is the paste in the ONE home, `apps/web/exercise.ts`
182
+ * — the file `x exercise` reads and `.contract({ exercise })` is handed — never an inline option:
183
+ * an entry declared only in a test is one `x exercise` cannot see. Action's twin.
184
+ */
185
+ export function queryExerciseUndeclared(
186
+ name: string,
187
+ why: QueryExerciseUndeclaredWhy,
188
+ ): UltimateError {
152
189
  const call = renderFixShellArg(name, '<name>');
190
+ const refuses = `query ${name} refuses an anonymous reader`;
153
191
  return new UltimateError({
154
192
  code: 'X_CONTRACT_EXERCISE_UNDECLARED',
155
- cause: blankReason
156
- ? `${name}.contract({ exercise: { off } }) gives a blank reason, so the skip is silent`
157
- : `query ${name} refuses an anonymous reader and its .contract() names no actor to read as`,
158
- fix: `${call}.contract({ exercise: { actor } }) — or ${call}.contract({ exercise: { off: '<why this read cannot run in a test>' } })`,
193
+ cause: queryUndeclaredCause(name, why, refuses),
194
+ fix: `defineExercise({ ${call}: { actor } }) # in ${QUERY_EXERCISE_DECLARATIONS_FILE}, then ${call}.contract({ exercise }) with that file's export — or { off: '<why this read cannot run in a test>' } there`,
159
195
  });
160
196
  }
197
+
198
+ /** What was missing, said in the file it is missing from. A `switch`, never a key lookup on a table. */
199
+ function queryUndeclaredCause(
200
+ name: string,
201
+ why: QueryExerciseUndeclaredWhy,
202
+ refuses: string,
203
+ ): string {
204
+ switch (why) {
205
+ case 'blank':
206
+ return `${name}.contract({ exercise: { off } }) gives a blank reason, so the skip is silent`;
207
+ case 'no-entry':
208
+ return `${refuses} and ${QUERY_EXERCISE_DECLARATIONS_FILE} declares no entry for it, so no actor to read as is known`;
209
+ }
210
+ return `${refuses} and its .contract() names no actor to read as`;
211
+ }
package/src/index.ts CHANGED
@@ -120,6 +120,10 @@ export type {
120
120
  QueryExerciseReport,
121
121
  } from './exercise';
122
122
  export { exerciseQuery } from './exercise';
123
+ export type {
124
+ QueryContractExercise,
125
+ QueryExerciseDeclarations,
126
+ } from './exercise-declarations';
123
127
  /** The HTTP projection: `GET /_x/query/<kebab>`, the URL `client()` derives. */
124
128
  export { toQueryRoute } from './http';
125
129
  export type { QueryHttp } from './http-pin';