@m6d/cortex-cli 0.1.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 (61) hide show
  1. package/README.md +175 -0
  2. package/bin/cortex.js +52 -0
  3. package/package.json +41 -0
  4. package/src/cc/client.ts +295 -0
  5. package/src/cc/flags.ts +135 -0
  6. package/src/cc/output.ts +77 -0
  7. package/src/cc/profiles.ts +105 -0
  8. package/src/cli.ts +56 -0
  9. package/src/commands/cc.ts +504 -0
  10. package/src/commands/graph.ts +229 -0
  11. package/src/commands/new.ts +333 -0
  12. package/src/commands/swagger.ts +261 -0
  13. package/src/config/load.ts +122 -0
  14. package/src/config/validate.ts +46 -0
  15. package/src/contracts/charts/chart-spec.ts +201 -0
  16. package/src/contracts/charts/index.ts +7 -0
  17. package/src/contracts/client-tools/index.ts +82 -0
  18. package/src/contracts/egress/index.ts +161 -0
  19. package/src/contracts/egress/scrub.ts +59 -0
  20. package/src/contracts/graph/clients/embed.ts +45 -0
  21. package/src/contracts/graph/clients/neo4j.ts +85 -0
  22. package/src/contracts/graph/helpers.ts +57 -0
  23. package/src/contracts/graph/index.ts +30 -0
  24. package/src/contracts/graph/schema.ts +65 -0
  25. package/src/contracts/graph/types.ts +125 -0
  26. package/src/contracts/markdown/index.ts +39 -0
  27. package/src/contracts/rich-text/index.ts +220 -0
  28. package/src/contracts/runtime/api.ts +328 -0
  29. package/src/contracts/runtime/auth.ts +41 -0
  30. package/src/contracts/runtime/dataset-sql-guide.ts +67 -0
  31. package/src/contracts/runtime/dataset-sql.ts +2522 -0
  32. package/src/contracts/runtime/datasets.fixtures.json +2849 -0
  33. package/src/contracts/runtime/datasets.ts +1391 -0
  34. package/src/contracts/runtime/grammar.ts +161 -0
  35. package/src/contracts/runtime/index.ts +14 -0
  36. package/src/contracts/runtime/signature.ts +237 -0
  37. package/src/contracts/runtime/sync.ts +172 -0
  38. package/src/contracts/runtime/triggers.ts +224 -0
  39. package/src/contracts/wire/index.ts +264 -0
  40. package/src/graph/expand-domains.ts +288 -0
  41. package/src/graph/generate-cypher.ts +201 -0
  42. package/src/graph/seed.ts +207 -0
  43. package/src/graph/validate.ts +78 -0
  44. package/src/scaffold/features.ts +190 -0
  45. package/src/scaffold/files.ts +411 -0
  46. package/src/scaffold/ports.ts +86 -0
  47. package/src/scaffold/templates/domains-README.md.txt +16 -0
  48. package/src/scaffold/templates/eslint.config.mjs.txt +20 -0
  49. package/src/scaffold/templates/fallowrc.jsonc.txt +42 -0
  50. package/src/scaffold/templates/gitignore.txt +19 -0
  51. package/src/scaffold/templates/index.ts.txt +18 -0
  52. package/src/scaffold/templates/prettier.config.mjs.txt +5 -0
  53. package/src/scaffold/templates/tsconfig.json.txt +25 -0
  54. package/src/swagger/extract-endpoints.ts +266 -0
  55. package/src/swagger/generated-block.ts +74 -0
  56. package/src/swagger/openapi-schema.ts +161 -0
  57. package/src/swagger/parse-response.ts +88 -0
  58. package/src/swagger/types.ts +25 -0
  59. package/src/ui/prompts.ts +125 -0
  60. package/src/ui/report.ts +107 -0
  61. package/tsconfig.json +46 -0
@@ -0,0 +1,328 @@
1
+ import { z } from "zod";
2
+ import {
3
+ agentRefSchema,
4
+ boundVariablesSchema,
5
+ catalogVersionSchema,
6
+ greetingSchema,
7
+ localeSchema,
8
+ localizedTextSchema,
9
+ mentionedToolSchema,
10
+ slugSchema,
11
+ suggestionsSchema,
12
+ TOOL_NAME_PATTERN,
13
+ toolSurfaceSchema,
14
+ variableDeclarationSchema,
15
+ } from "./grammar";
16
+ import { publishedAuthSchema } from "./auth";
17
+ import { triggerDeclarationSchema } from "./triggers";
18
+ import { datasetHostDeclarationSchema, datasetIdentifierSchema } from "./datasets";
19
+
20
+ /*
21
+ * The cc runtime API (`/api/runtime/agents/:agentSlug/*`): the console
22
+ * validates requests against these schemas and `@m6d/cortex-server` validates
23
+ * the responses it gets back. Request types use `z.input` because the callers
24
+ * build payloads the runtime's parser then fills defaults into.
25
+ */
26
+
27
+ export const RUNTIME_ERROR_KINDS = [
28
+ "timeout",
29
+ "upstream_4xx",
30
+ "upstream_5xx",
31
+ "schema_mismatch",
32
+ "rate_limited",
33
+ "not_found",
34
+ "unauthorized",
35
+ "invalid_request",
36
+ "egress_blocked",
37
+ "internal",
38
+ ] as const;
39
+ export const runtimeErrorKindSchema = z.enum(RUNTIME_ERROR_KINDS);
40
+ export type RuntimeErrorKind = z.infer<typeof runtimeErrorKindSchema>;
41
+
42
+ /** The HTTP status and default detail each error kind ships with; both sides rely on these exact values. */
43
+ export const RUNTIME_ERRORS = {
44
+ invalid_request: { status: 400, detail: "invalid request", retryable: false },
45
+ unauthorized: { status: 401, detail: "unauthorized", retryable: false },
46
+ egress_blocked: { status: 422, detail: "egress blocked", retryable: false },
47
+ not_found: { status: 404, detail: "not found", retryable: false },
48
+ rate_limited: { status: 429, detail: "rate limited", retryable: true },
49
+ internal: { status: 500, detail: "internal error", retryable: true },
50
+ upstream_4xx: { status: 502, detail: "upstream request failed", retryable: false },
51
+ upstream_5xx: { status: 502, detail: "upstream request failed", retryable: true },
52
+ schema_mismatch: {
53
+ status: 502,
54
+ detail: "upstream response did not match the schema",
55
+ retryable: false,
56
+ },
57
+ timeout: { status: 504, detail: "request timed out", retryable: true },
58
+ } as const satisfies Record<
59
+ RuntimeErrorKind,
60
+ { status: number; detail: string; retryable: boolean }
61
+ >;
62
+
63
+ export const runtimeErrorSchema = z.object({
64
+ error: z.object({
65
+ kind: runtimeErrorKindSchema,
66
+ detail: z.string().max(512),
67
+ retryable: z.boolean(),
68
+ retryAfterSeconds: z.number().int().positive().optional(),
69
+ upstreamStatus: z.number().int().min(100).max(599).optional(),
70
+ }),
71
+ });
72
+
73
+ /** The ways an execute output may disagree with the tool's declared schema. */
74
+ export const JSON_SCHEMA_ISSUE_KINDS = ["missing", "unexpected", "type_mismatch"] as const;
75
+
76
+ export const sharedShapeSchema = z.object({
77
+ ref: z.string().regex(/^\$R\d+$/),
78
+ shape: z.string(),
79
+ });
80
+
81
+ export const knowledgeChunkSchema = z.object({
82
+ chunkId: z.uuid(),
83
+ text: z.string(),
84
+ score: z.number(),
85
+ citation: z.object({
86
+ documentId: z.uuid(),
87
+ documentTitle: z.string(),
88
+ page: z.number().int().positive().nullable(),
89
+ section: z.string().nullable(),
90
+ uri: z.url().nullable(),
91
+ service: z.string().nullable(),
92
+ }),
93
+ });
94
+
95
+ /**
96
+ * Present on signatures of `embedded` tools: flows the end user completes in
97
+ * an embedded surface inside the chat widget. The tool's endpoint fields act
98
+ * as the *initiate* call (returns the embed URL); the result the page reports
99
+ * back is relayed as-is, verifying it is the integrating backend's job.
100
+ */
101
+ export const toolEmbedSchema = z.object({
102
+ surface: toolSurfaceSchema,
103
+ embedOrigin: z.url(),
104
+ });
105
+ export type ToolEmbed = z.infer<typeof toolEmbedSchema>;
106
+
107
+ const toolSignatureBaseSchema = z.object({
108
+ toolId: z.uuid(),
109
+ name: z.string().regex(TOOL_NAME_PATTERN),
110
+ version: z.number().int().positive(),
111
+ readOnly: z.boolean(),
112
+ /**
113
+ * The call parks the run until the end user approves it. Declared to the
114
+ * model as a function call like an embedded tool, never as a sandbox
115
+ * binding, so the schema travels with it.
116
+ */
117
+ requiresApproval: z.boolean(),
118
+ /** The question the widget asks before an approval-gated call runs; only such tools carry one. */
119
+ approvalMessage: localizedTextSchema.optional(),
120
+ tags: z.array(z.string()),
121
+ signature: z.string(),
122
+ score: z.number().nullable(),
123
+ pinned: z.boolean(),
124
+ /** Variables the tool's input reads (`x-source` of kind `variable`); filled server-side, never seen by the model. */
125
+ variables: boundVariablesSchema,
126
+ });
127
+
128
+ /**
129
+ * A sandbox tool is described to the model by its rendered `signature`; a
130
+ * client tool is declared to the model as a function call, which needs the
131
+ * published input JSON Schema verbatim.
132
+ */
133
+ export const toolSignatureSchema = z.union([
134
+ toolSignatureBaseSchema.extend({
135
+ embed: z.undefined().optional(),
136
+ inputSchema: z.record(z.string(), z.unknown()).optional(),
137
+ }),
138
+ toolSignatureBaseSchema.extend({
139
+ embed: toolEmbedSchema,
140
+ inputSchema: z.record(z.string(), z.unknown()),
141
+ }),
142
+ ]);
143
+ export type ToolSignature = z.infer<typeof toolSignatureSchema>;
144
+
145
+ /**
146
+ * One always-listed skill line. The description is the whole trigger: the
147
+ * model reads it every turn and calls `loadSkill` when it matches the task.
148
+ */
149
+ export const skillLineSchema = z.object({
150
+ name: slugSchema,
151
+ description: z.string().max(500),
152
+ });
153
+ export type SkillLine = z.infer<typeof skillLineSchema>;
154
+
155
+ export const serviceCardSchema = z.object({
156
+ serviceId: z.uuid(),
157
+ key: slugSchema,
158
+ name: z.string(),
159
+ card: z.string(),
160
+ score: z.number(),
161
+ /** Skills scoped to this service; listed only while the card is in context. */
162
+ skills: z.array(skillLineSchema),
163
+ });
164
+
165
+ /**
166
+ * A skill body served on `loadSkill`: mentions rendered to current names and
167
+ * headings demoted by the console, `{{variables}}` left for the server to
168
+ * fill per request. `mentionedTools` are the sandbox bindings the body
169
+ * refers to, so the turn can register them before the model acts on it.
170
+ */
171
+ export const skillBodyResponseSchema = z.object({
172
+ name: z.string(),
173
+ body: z.string(),
174
+ mentionedTools: z.array(mentionedToolSchema),
175
+ /** Datasets the body mentions; the server lists them under the body so the model can query them at once. */
176
+ mentionedDatasets: z.array(datasetIdentifierSchema),
177
+ catalogVersion: catalogVersionSchema,
178
+ });
179
+ export type SkillBodyResponse = z.infer<typeof skillBodyResponseSchema>;
180
+
181
+ export const runtimeAgentConfigSchema = z.object({
182
+ /**
183
+ * End-user token verification for this agent, managed by the agent's
184
+ * owner. Absent = the server's own auth default applies.
185
+ */
186
+ auth: publishedAuthSchema.optional(),
187
+ systemPrompt: z.string(),
188
+ /** Every declared variable the published prompt, skills, or triggers reference. */
189
+ promptVariables: z.array(variableDeclarationSchema.shape.name),
190
+ defaultLocale: localeSchema,
191
+ /**
192
+ * Whether end users may upload files to this agent. Operational config
193
+ * like `auth`: read from the agent row, so toggling applies without
194
+ * republishing. The runtime rejects new uploads when false; existing
195
+ * attachments stay downloadable and deletable.
196
+ */
197
+ attachmentsEnabled: z.boolean(),
198
+ metaTools: z.object({
199
+ searchKnowledge: z.boolean(),
200
+ searchTools: z.boolean(),
201
+ searchServices: z.boolean(),
202
+ }),
203
+ limits: z.object({
204
+ resolveTokenBudget: z.number().int().positive(),
205
+ executeTimeoutMs: z.number().int().positive(),
206
+ maxResponseBytes: z.number().int().positive(),
207
+ }),
208
+ /** Unscoped published skills; service-scoped ones travel on their card. */
209
+ skills: z.array(skillLineSchema),
210
+ triggers: z.array(triggerDeclarationSchema),
211
+ /** Published dataset hosts with the exposed definitions as of their pinned manifest version. */
212
+ datasetHosts: z.array(datasetHostDeclarationSchema),
213
+ /** Whether end users may message this agent; false makes it headless. */
214
+ messageTrigger: z.boolean(),
215
+ /** Operational like `messageTrigger`: edited on the agent row without republishing. */
216
+ suggestions: suggestionsSchema,
217
+ /** Operational like `suggestions`. */
218
+ greeting: greetingSchema,
219
+ /** The resolved agent's id; reports about it carry this so they attach to it whatever the slug or alias later means. */
220
+ agentRef: agentRefSchema,
221
+ catalogVersion: catalogVersionSchema,
222
+ publishedAt: z.iso.datetime(),
223
+ });
224
+ export type RuntimeAgentConfig = z.infer<typeof runtimeAgentConfigSchema>;
225
+
226
+ const retrievalQuerySchema = z.string().trim().min(1).max(4000);
227
+ const threadIdSchema = z.string().max(128);
228
+
229
+ export const resolveRequestSchema = z.object({
230
+ query: retrievalQuerySchema,
231
+ threadId: threadIdSchema,
232
+ /** Sandbox tools the thread called recently; resolve ranks them ahead of cold ones. */
233
+ recentToolNames: z.array(z.string()).max(20),
234
+ limits: z
235
+ .object({
236
+ knowledge: z.number().int().min(0).max(20),
237
+ tools: z.number().int().min(0).max(20),
238
+ services: z.number().int().min(0).max(20),
239
+ datasets: z.number().int().min(0).max(20).default(6),
240
+ })
241
+ .prefault({ knowledge: 6, tools: 8, services: 5, datasets: 6 }),
242
+ });
243
+ export type ResolveRequest = z.input<typeof resolveRequestSchema>;
244
+
245
+ /** A dataset the turn should list: ranked in by the question, or pinned by the author. */
246
+ export const resolvedDatasetSchema = z.object({
247
+ name: datasetIdentifierSchema,
248
+ score: z.number().nullable(),
249
+ pinned: z.boolean(),
250
+ });
251
+
252
+ export type ResolvedDataset = z.infer<typeof resolvedDatasetSchema>;
253
+
254
+ export const resolveResponseSchema = z.object({
255
+ resolveId: z.uuid(),
256
+ catalogVersion: catalogVersionSchema,
257
+ locale: localeSchema,
258
+ knowledge: z.array(knowledgeChunkSchema),
259
+ tools: z.array(toolSignatureSchema),
260
+ services: z.array(serviceCardSchema),
261
+ /** One line each in the turn context; the definitions come from the config's dataset hosts. */
262
+ datasets: z.array(resolvedDatasetSchema),
263
+ sharedShapes: z.array(sharedShapeSchema),
264
+ budget: z.object({
265
+ requested: z.number().int(),
266
+ used: z.number().int(),
267
+ truncated: z.boolean(),
268
+ }),
269
+ });
270
+ export type ResolveResponse = z.infer<typeof resolveResponseSchema>;
271
+
272
+ export const searchRequestSchema = z.object({
273
+ query: retrievalQuerySchema,
274
+ limit: z.number().int().min(1).max(20).default(5),
275
+ locale: localeSchema.optional(),
276
+ threadId: threadIdSchema.optional(),
277
+ service: slugSchema.optional(),
278
+ });
279
+ export type SearchRequest = z.input<typeof searchRequestSchema>;
280
+
281
+ export const searchToolsResponseSchema = z.object({
282
+ catalogVersion: catalogVersionSchema,
283
+ tools: z.array(toolSignatureSchema),
284
+ sharedShapes: z.array(sharedShapeSchema),
285
+ });
286
+
287
+ export const searchServicesResponseSchema = z.object({
288
+ catalogVersion: catalogVersionSchema,
289
+ services: z.array(serviceCardSchema),
290
+ tools: z.array(toolSignatureSchema),
291
+ sharedShapes: z.array(sharedShapeSchema),
292
+ });
293
+
294
+ export const searchKnowledgeResponseSchema = z.object({
295
+ catalogVersion: catalogVersionSchema,
296
+ knowledge: z.array(knowledgeChunkSchema),
297
+ });
298
+
299
+ export const executeRequestSchema = z.object({
300
+ input: z.record(z.string(), z.unknown()),
301
+ context: z.object({
302
+ threadId: threadIdSchema,
303
+ userId: z.string().max(128),
304
+ /** Values of the variables the tool's input reads; only those names travel. */
305
+ variables: z.record(z.string(), z.unknown()),
306
+ }),
307
+ });
308
+ export type ExecuteRequest = z.input<typeof executeRequestSchema>;
309
+
310
+ export const executeResponseSchema = z.object({
311
+ toolId: z.uuid(),
312
+ name: z.string(),
313
+ version: z.number().int().positive(),
314
+ output: z.unknown(),
315
+ meta: z.object({
316
+ upstreamStatus: z.number().int().min(100).max(599),
317
+ durationMs: z.number().int().min(0),
318
+ replayed: z.boolean(),
319
+ truncated: z.boolean(),
320
+ truncatedPaths: z.array(z.string()),
321
+ drift: z.array(
322
+ z.object({
323
+ path: z.string(),
324
+ kind: z.enum(JSON_SCHEMA_ISSUE_KINDS),
325
+ }),
326
+ ),
327
+ }),
328
+ });
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+
3
+ /*
4
+ * How a server, or one agent on it, verifies the end user's token. One union
5
+ * for every reader: a code config, an agent the Control Center created, and
6
+ * the runtime config cc publishes. A `custom` verifier exists in code only,
7
+ * so it is not part of this contract.
8
+ */
9
+
10
+ export const AUTH_JWT_SECRET_ALGORITHMS = ["HS256", "HS384", "HS512"] as const;
11
+
12
+ const issuerSchema = z.string().trim().min(1).max(2048);
13
+ /** When set, the token's `aud` must contain it, so a token minted for another audience under the same issuer cannot be replayed here. */
14
+ const audienceSchema = z.string().trim().min(1).max(2048).optional();
15
+
16
+ /** Asymmetric tokens: the issuer publishes its public keys at the JWKS URI. */
17
+ export const jwksAuthSchema = z.object({
18
+ kind: z.literal("jwks"),
19
+ jwksUri: z.url(),
20
+ issuer: issuerSchema,
21
+ audience: audienceSchema,
22
+ });
23
+
24
+ /**
25
+ * Symmetric tokens: the issuer and this server share the secret. At least
26
+ * 32 characters, the JOSE minimum for HMAC keys; the algorithms are pinned
27
+ * so a token signed another way is refused before the key is tried.
28
+ */
29
+ export const jwtSecretAuthSchema = z.object({
30
+ kind: z.literal("jwt-secret"),
31
+ secret: z.string().min(32),
32
+ algorithms: z.array(z.enum(AUTH_JWT_SECRET_ALGORITHMS)).min(1),
33
+ issuer: issuerSchema,
34
+ audience: audienceSchema,
35
+ });
36
+
37
+ export const publishedAuthSchema = z.discriminatedUnion("kind", [
38
+ jwksAuthSchema,
39
+ jwtSecretAuthSchema,
40
+ ]);
41
+ export type PublishedAuth = z.infer<typeof publishedAuthSchema>;
@@ -0,0 +1,67 @@
1
+ /*
2
+ * What the model reads about the SQL subset: the grammar in one list, then
3
+ * statements it can copy, each over the shapes datasets commonly have. One
4
+ * text for the system prompt, the tool descriptions and the sandbox, so the
5
+ * three never disagree.
6
+ */
7
+
8
+ /** Every statement in the guide, with the intent it answers; tests parse each one. */
9
+ export const DATASET_SQL_EXAMPLES = [
10
+ {
11
+ intent: "share of a total per group, as a ratio of two aggregates",
12
+ sql: "SELECT pets.species, COUNT(*) AS orderCount, COUNT(*) FILTER (WHERE orders.status = 'cancelled') * 1.0 / COUNT(*) AS cancelledShare FROM orders LEFT JOIN pets ON orders.petId = pets.id GROUP BY pets.species ORDER BY cancelledShare DESC",
13
+ },
14
+ {
15
+ intent: "this year against last year in one row",
16
+ sql: "SELECT SUM(total) FILTER (WHERE EXTRACT(YEAR FROM placedAt) = EXTRACT(YEAR FROM CURRENT_DATE)) AS thisYear, SUM(total) FILTER (WHERE EXTRACT(YEAR FROM placedAt) = EXTRACT(YEAR FROM CURRENT_DATE) - 1) AS lastYear, thisYear / lastYear - 1 AS growth FROM orders",
17
+ },
18
+ {
19
+ intent: "the last 30 days",
20
+ sql: "SELECT COUNT(*) AS orderCount, SUM(total) AS revenue FROM orders WHERE placedAt >= CURRENT_DATE - INTERVAL '30 days'",
21
+ },
22
+ {
23
+ intent: "a monthly trend",
24
+ sql: "SELECT DATE_TRUNC('month', placedAt) AS month, COUNT(*) AS orderCount, SUM(total) AS revenue FROM orders GROUP BY month ORDER BY month",
25
+ },
26
+ {
27
+ intent: "the top 3 of each group",
28
+ sql: "SELECT customers.name, orders.total, orders.placedAt FROM orders LEFT JOIN customers ON orders.customerId = customers.id QUALIFY RANK() OVER (PARTITION BY orders.customerId ORDER BY orders.total DESC) <= 3",
29
+ },
30
+ {
31
+ intent: "value bands",
32
+ sql: "SELECT CASE WHEN total < 100 THEN 'small' WHEN total < 500 THEN 'mid' ELSE 'large' END AS band, COUNT(*) AS orderCount FROM orders GROUP BY band",
33
+ },
34
+ {
35
+ intent: "a match regardless of case",
36
+ sql: "SELECT id, name FROM customers WHERE LOWER(name) = 'alice'",
37
+ },
38
+ {
39
+ intent: "groups filtered and ordered by a computed share, rounded",
40
+ sql: "SELECT customerId, COUNT(*) AS orderCount, ROUND(COUNT(*) FILTER (WHERE status = 'cancelled') * 100.0 / COUNT(*), 1) AS cancelledPct FROM orders GROUP BY customerId HAVING cancelledPct > 20 ORDER BY cancelledPct DESC LIMIT 10",
41
+ },
42
+ {
43
+ intent: "each group's share of the total",
44
+ sql: "SELECT pets.species, COUNT(*) AS orderCount, COUNT(*) * 100.0 / SUM(COUNT(*)) OVER () AS sharePct FROM orders LEFT JOIN pets ON orders.petId = pets.id GROUP BY pets.species",
45
+ },
46
+ {
47
+ intent: "rows above their group's average",
48
+ sql: "SELECT id, customerId, total, AVG(total) OVER (PARTITION BY customerId) AS customerAvg FROM orders QUALIFY total > customerAvg",
49
+ },
50
+ ] as const;
51
+
52
+ /** The grammar in one paragraph, as the tool descriptions and the prompt state it. */
53
+ export const DATASET_SQL_GRAMMAR =
54
+ "One SELECT over one dataset. LEFT JOIN related datasets exactly as the dataset's description lists them (LEFT JOIN keeps rows without a match; JOIN or INNER JOIN drops them); reference dataset.column. " +
55
+ "SELECT: columns, DATE_TRUNC('unit', column), EXTRACT(part FROM column), CASE WHEN ... END, LOWER/UPPER/TRIM/LENGTH/ABS/ROUND/COALESCE, COUNT/SUM/AVG/MIN/MAX over a column or arithmetic each with an optional FILTER (WHERE ...), and named arithmetic between aggregates (SUM(a) / COUNT(*) AS avg). " +
56
+ "WHERE and HAVING: AND, OR, NOT, parentheses, comparisons with literals or other columns, IN, BETWEEN, LIKE, IS NULL; CURRENT_DATE and INTERVAL arithmetic on it. " +
57
+ "GROUP BY columns or select aliases. An aggregate with OVER () or OVER (PARTITION BY ...) is a window: that aggregate over the rows sharing the partition, on every row (SUM(COUNT(*)) OVER () is the grand total); name it AS, use it in arithmetic, and QUALIFY filters the finished rows. " +
58
+ "QUALIFY RANK() OVER (PARTITION BY ... ORDER BY ...) <= n for the top n per group (rows tied on every ORDER BY key share a rank and all stay); ORDER BY; LIMIT; OFFSET. " +
59
+ "Rows nest qualified columns under the qualifier as written (pets.name comes back as row.pets.name); aliases and aggregates are top-level.";
60
+
61
+ /** The guide as the model reads it: the grammar, then the examples. */
62
+ export function datasetSqlGuide() {
63
+ const examples = DATASET_SQL_EXAMPLES.map(
64
+ (example) => `-- ${example.intent}\n${example.sql}`,
65
+ ).join("\n");
66
+ return `${DATASET_SQL_GRAMMAR}\n\nExamples over three sample datasets (orders, pets, customers); query the datasets listed under Datasets:\n${examples}`;
67
+ }