@rayfold/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.
package/mock.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * A server that answers from the schema alone.
3
+ *
4
+ * Before any resolver exists, a client team needs something to call: `rayfold mock schema.rayfold` serves every
5
+ * operation with data the schema itself describes - enum values, `@range` bounds, `@example` values, the formats the
6
+ * scalars carry, pages with items - so screens can be built against the real wire, shapes and all.
7
+ *
8
+ * The answers are made up, but they are not random: the same call gives the same result, so a screenshot, a test or a
9
+ * demo does not change under you. Nothing here is a substitute for a resolver; it is a stand-in until there is one.
10
+ */
11
+ import { annotation, baseName } from "@rayfold/schema";
12
+ const WORDS = ["amber", "harbour", "lantern", "meadow", "quartz", "willow", "cinder", "pebble", "thistle", "marrow"];
13
+ /** A fixed point in time, so a mock's answers do not drift between runs. */
14
+ const EPOCH = Date.UTC(2026, 0, 15, 9, 30, 0);
15
+ export function mockResolvers(ir, options = {}) {
16
+ const resolvers = { Query: {}, Command: {}, Stream: {} };
17
+ for (const op of Object.values(ir.ops)) {
18
+ const answer = (args) => {
19
+ // the call is the seed all the way down: every field seeds from this, so a different call is a different answer
20
+ const call = `${op.name}:${stable(args)}`;
21
+ const value = valueFor(ir, op.returns, call, seeded(call), 0, options, sizeFrom(args, options));
22
+ return op.kind === "command" ? echo(value, args) : value;
23
+ };
24
+ if (op.kind === "query")
25
+ resolvers["Query"][op.name] = (args) => answer(args);
26
+ else if (op.kind === "command")
27
+ resolvers["Command"][op.name] = (args) => answer(args);
28
+ else {
29
+ resolvers["Stream"][op.name] = async function* (args) {
30
+ for (let i = 0; i < (options.pageSize ?? 3); i++) {
31
+ yield valueFor(ir, op.returns, `${op.name}:${i}`, seeded(`${op.name}:${i}:${stable(args)}`), 0, options, 1);
32
+ }
33
+ };
34
+ }
35
+ }
36
+ // a field that takes arguments has no value on its parent to fall back to, so it needs a loader of its own
37
+ for (const type of Object.values(ir.types)) {
38
+ if (type.builtin || !("fields" in type))
39
+ continue;
40
+ for (const field of type.fields) {
41
+ if (!field.args.length)
42
+ continue;
43
+ const loaders = (resolvers[type.name] ??= {});
44
+ loaders[field.name] = (parents, args) => parents.map((parent) => {
45
+ const key = `${type.name}.${field.name}:${String(parent["id"] ?? "")}:${stable(args)}`;
46
+ return valueFor(ir, field.type, key, seeded(key), 1, options, sizeFrom(args, options));
47
+ });
48
+ }
49
+ }
50
+ return resolvers;
51
+ }
52
+ /** `page: { first: n }` decides how many items a page holds, so a caller still controls the size. */
53
+ function sizeFrom(args, options) {
54
+ const page = args["page"];
55
+ const first = page && typeof page === "object" ? page["first"] : undefined;
56
+ const asked = typeof first === "number" ? first : typeof args["first"] === "number" ? args["first"] : undefined;
57
+ return Math.max(0, Math.min(asked ?? options.pageSize ?? 3, 50));
58
+ }
59
+ /** What a command was given comes back in what it returns, so a create looks like what was sent. */
60
+ function echo(value, args) {
61
+ if (!value || typeof value !== "object" || Array.isArray(value))
62
+ return value;
63
+ const out = value;
64
+ const sources = [args, ...Object.values(args).filter((a) => !!a && typeof a === "object" && !Array.isArray(a))];
65
+ for (const source of sources) {
66
+ for (const [key, given] of Object.entries(source)) {
67
+ if (key in out && given !== null && typeof given !== "object")
68
+ out[key] = given;
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+ function valueFor(ir, ref, hint, rng, depth, options, size) {
74
+ if (ref.kind === "list") {
75
+ const n = depth > (options.depth ?? 4) ? 0 : Math.min(size, 5);
76
+ return Array.from({ length: n }, (_, i) => valueFor(ir, ref.of, `${hint}:${i}`, seeded(`${hint}:${i}`), depth + 1, options, size));
77
+ }
78
+ if (ref.name === "Page" && ref.args?.[0]) {
79
+ const items = Array.from({ length: size }, (_, i) => valueFor(ir, ref.args[0], `${hint}:${i}`, seeded(`${hint}:${i}`), depth + 1, options, size));
80
+ return { items, cursor: items.length ? `${hint}:${items.length}` : null, hasMore: false, total: items.length };
81
+ }
82
+ const def = ir.types[ref.name];
83
+ if (!def)
84
+ return null;
85
+ if (def.kind === "enum")
86
+ return def.values[Math.floor(rng() * def.values.length)]?.name ?? null;
87
+ if (def.kind === "union") {
88
+ const member = def.members[Math.floor(rng() * def.members.length)];
89
+ return member ? valueFor(ir, { kind: "named", name: member, nullable: false }, hint, rng, depth, options, size) : null;
90
+ }
91
+ if (def.kind === "scalar")
92
+ return scalar(def.name, hint, rng);
93
+ if (depth > (options.depth ?? 4))
94
+ return ref.nullable ? null : {};
95
+ const out = {};
96
+ if (def.kind === "entity")
97
+ out["$type"] = def.name;
98
+ for (const field of def.fields) {
99
+ if (field.args.length)
100
+ continue; // a loader answers that one, with the arguments the caller gave
101
+ out[field.name] = fieldValue(ir, def, field, `${hint}.${field.name}`, depth, options, size);
102
+ }
103
+ return out;
104
+ }
105
+ function fieldValue(ir, owner, field, hint, depth, options, size) {
106
+ const example = exampleOf(field);
107
+ if (example !== undefined)
108
+ return example;
109
+ const rng = seeded(hint);
110
+ if (field.name === "id" && baseName(field.type) === "ID")
111
+ return `${owner.name.toLowerCase()}-${Math.floor(rng() * 900 + 100)}`;
112
+ const range = annotation(field, "range");
113
+ const value = valueFor(ir, field.type, hint, rng, depth + 1, options, size);
114
+ return clamp(value, range?.args["min"], range?.args["max"]);
115
+ }
116
+ function exampleOf(def) {
117
+ const example = annotation(def, "example")?.args["value"];
118
+ return example === undefined ? undefined : example;
119
+ }
120
+ function clamp(value, min, max) {
121
+ if (typeof value !== "number")
122
+ return value;
123
+ const low = typeof min === "number" ? min : undefined;
124
+ const high = typeof max === "number" ? max : undefined;
125
+ return Math.min(high ?? value, Math.max(low ?? value, value));
126
+ }
127
+ function scalar(name, hint, rng) {
128
+ switch (name) {
129
+ case "ID":
130
+ return `${hint.split(/[.:]/)[0] || "id"}-${Math.floor(rng() * 900 + 100)}`;
131
+ case "Int":
132
+ return Math.floor(rng() * 100);
133
+ case "Long":
134
+ return Math.floor(rng() * 1_000_000);
135
+ case "Float":
136
+ return Math.round(rng() * 10_000) / 100;
137
+ case "Decimal":
138
+ return (Math.round(rng() * 10_000) / 100).toFixed(2);
139
+ case "Boolean":
140
+ return rng() > 0.5;
141
+ case "Instant":
142
+ return new Date(EPOCH + Math.floor(rng() * 30) * 86_400_000).toISOString();
143
+ case "Date":
144
+ return new Date(EPOCH + Math.floor(rng() * 30) * 86_400_000).toISOString().slice(0, 10);
145
+ case "Duration":
146
+ return Math.floor(rng() * 60) * 1000;
147
+ case "Bytes":
148
+ return btoa(words(rng, 1));
149
+ case "JSON":
150
+ return {};
151
+ default:
152
+ return words(rng, 2); // a scalar of the schema's own: a word is the most it can assume
153
+ }
154
+ }
155
+ function words(rng, count) {
156
+ return Array.from({ length: count }, () => WORDS[Math.floor(rng() * WORDS.length)]).join(" ");
157
+ }
158
+ /** The same call gives the same answer: the seed is the call, not the clock. */
159
+ function seeded(key) {
160
+ let h = 2166136261;
161
+ for (let i = 0; i < key.length; i++) {
162
+ h ^= key.charCodeAt(i);
163
+ h = Math.imul(h, 16777619);
164
+ }
165
+ let state = h >>> 0;
166
+ return () => {
167
+ state = (state + 0x6d2b79f5) >>> 0;
168
+ let t = state;
169
+ t = Math.imul(t ^ (t >>> 15), t | 1);
170
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
171
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
172
+ };
173
+ }
174
+ function stable(value) {
175
+ if (value === null || typeof value !== "object")
176
+ return JSON.stringify(value) ?? "null";
177
+ if (Array.isArray(value))
178
+ return `[${value.map(stable).join(",")}]`;
179
+ return `{${Object.keys(value)
180
+ .sort()
181
+ .map((k) => `${k}:${stable(value[k])}`)
182
+ .join(",")}}`;
183
+ }
184
+ //# sourceMappingURL=mock.js.map
package/mock.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock.js","sourceRoot":"","sources":["../src/mock.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A server that answers from the schema alone.\n *\n * Before any resolver exists, a client team needs something to call: `rayfold mock schema.rayfold` serves every\n * operation with data the schema itself describes - enum values, `@range` bounds, `@example` values, the formats the\n * scalars carry, pages with items - so screens can be built against the real wire, shapes and all.\n *\n * The answers are made up, but they are not random: the same call gives the same result, so a screenshot, a test or a\n * demo does not change under you. Nothing here is a substitute for a resolver; it is a stand-in until there is one.\n */\nimport { annotation, baseName, type ArgDef, type FieldDef, type OpDef, type RayfoldSchemaIR, type TypeDef, type TypeRef } from \"@rayfold/schema\";\nimport type { Resolvers } from \"@rayfold/server\";\n\nexport interface MockOptions {\n /** Items in a list or a page when the call does not ask for a number. Default 3. */\n pageSize?: number;\n /** How deep a nested object goes before references stop. Default 4. */\n depth?: number;\n}\n\nconst WORDS = [\"amber\", \"harbour\", \"lantern\", \"meadow\", \"quartz\", \"willow\", \"cinder\", \"pebble\", \"thistle\", \"marrow\"];\n/** A fixed point in time, so a mock's answers do not drift between runs. */\nconst EPOCH = Date.UTC(2026, 0, 15, 9, 30, 0);\n\nexport function mockResolvers(ir: RayfoldSchemaIR, options: MockOptions = {}): Resolvers {\n const resolvers: Record<string, Record<string, unknown>> = { Query: {}, Command: {}, Stream: {} };\n\n for (const op of Object.values(ir.ops)) {\n const answer = (args: Record<string, unknown>): unknown => {\n // the call is the seed all the way down: every field seeds from this, so a different call is a different answer\n const call = `${op.name}:${stable(args)}`;\n const value = valueFor(ir, op.returns, call, seeded(call), 0, options, sizeFrom(args, options));\n return op.kind === \"command\" ? echo(value, args) : value;\n };\n if (op.kind === \"query\") resolvers[\"Query\"]![op.name] = (args: Record<string, unknown>) => answer(args);\n else if (op.kind === \"command\") resolvers[\"Command\"]![op.name] = (args: Record<string, unknown>) => answer(args);\n else {\n resolvers[\"Stream\"]![op.name] = async function* (args: Record<string, unknown>) {\n for (let i = 0; i < (options.pageSize ?? 3); i++) {\n yield valueFor(ir, op.returns, `${op.name}:${i}`, seeded(`${op.name}:${i}:${stable(args)}`), 0, options, 1);\n }\n };\n }\n }\n\n // a field that takes arguments has no value on its parent to fall back to, so it needs a loader of its own\n for (const type of Object.values(ir.types)) {\n if (type.builtin || !(\"fields\" in type)) continue;\n for (const field of type.fields) {\n if (!field.args.length) continue;\n const loaders = (resolvers[type.name] ??= {});\n loaders[field.name] = (parents: Array<Record<string, unknown>>, args: Record<string, unknown>) =>\n parents.map((parent) => {\n const key = `${type.name}.${field.name}:${String(parent[\"id\"] ?? \"\")}:${stable(args)}`;\n return valueFor(ir, field.type, key, seeded(key), 1, options, sizeFrom(args, options));\n });\n }\n }\n\n return resolvers as Resolvers;\n}\n\n/** `page: { first: n }` decides how many items a page holds, so a caller still controls the size. */\nfunction sizeFrom(args: Record<string, unknown>, options: MockOptions): number {\n const page = args[\"page\"];\n const first = page && typeof page === \"object\" ? (page as Record<string, unknown>)[\"first\"] : undefined;\n const asked = typeof first === \"number\" ? first : typeof args[\"first\"] === \"number\" ? (args[\"first\"] as number) : undefined;\n return Math.max(0, Math.min(asked ?? options.pageSize ?? 3, 50));\n}\n\n/** What a command was given comes back in what it returns, so a create looks like what was sent. */\nfunction echo(value: unknown, args: Record<string, unknown>): unknown {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return value;\n const out = value as Record<string, unknown>;\n const sources = [args, ...Object.values(args).filter((a): a is Record<string, unknown> => !!a && typeof a === \"object\" && !Array.isArray(a))];\n for (const source of sources) {\n for (const [key, given] of Object.entries(source)) {\n if (key in out && given !== null && typeof given !== \"object\") out[key] = given;\n }\n }\n return out;\n}\n\nfunction valueFor(\n ir: RayfoldSchemaIR,\n ref: TypeRef,\n hint: string,\n rng: () => number,\n depth: number,\n options: MockOptions,\n size: number,\n): unknown {\n if (ref.kind === \"list\") {\n const n = depth > (options.depth ?? 4) ? 0 : Math.min(size, 5);\n return Array.from({ length: n }, (_, i) => valueFor(ir, ref.of, `${hint}:${i}`, seeded(`${hint}:${i}`), depth + 1, options, size));\n }\n if (ref.name === \"Page\" && ref.args?.[0]) {\n const items = Array.from({ length: size }, (_, i) => valueFor(ir, ref.args![0]!, `${hint}:${i}`, seeded(`${hint}:${i}`), depth + 1, options, size));\n return { items, cursor: items.length ? `${hint}:${items.length}` : null, hasMore: false, total: items.length };\n }\n\n const def = ir.types[ref.name];\n if (!def) return null;\n if (def.kind === \"enum\") return def.values[Math.floor(rng() * def.values.length)]?.name ?? null;\n if (def.kind === \"union\") {\n const member = def.members[Math.floor(rng() * def.members.length)];\n return member ? valueFor(ir, { kind: \"named\", name: member, nullable: false }, hint, rng, depth, options, size) : null;\n }\n if (def.kind === \"scalar\") return scalar(def.name, hint, rng);\n\n if (depth > (options.depth ?? 4)) return ref.nullable ? null : {};\n const out: Record<string, unknown> = {};\n if (def.kind === \"entity\") out[\"$type\"] = def.name;\n for (const field of def.fields) {\n if (field.args.length) continue; // a loader answers that one, with the arguments the caller gave\n out[field.name] = fieldValue(ir, def, field, `${hint}.${field.name}`, depth, options, size);\n }\n return out;\n}\n\nfunction fieldValue(ir: RayfoldSchemaIR, owner: TypeDef, field: FieldDef, hint: string, depth: number, options: MockOptions, size: number): unknown {\n const example = exampleOf(field);\n if (example !== undefined) return example;\n const rng = seeded(hint);\n if (field.name === \"id\" && baseName(field.type) === \"ID\") return `${owner.name.toLowerCase()}-${Math.floor(rng() * 900 + 100)}`;\n const range = annotation(field, \"range\");\n const value = valueFor(ir, field.type, hint, rng, depth + 1, options, size);\n return clamp(value, range?.args[\"min\"], range?.args[\"max\"]);\n}\n\nfunction exampleOf(def: FieldDef | ArgDef | OpDef): unknown {\n const example = annotation(def, \"example\")?.args[\"value\"];\n return example === undefined ? undefined : (example as unknown);\n}\n\nfunction clamp(value: unknown, min: unknown, max: unknown): unknown {\n if (typeof value !== \"number\") return value;\n const low = typeof min === \"number\" ? min : undefined;\n const high = typeof max === \"number\" ? max : undefined;\n return Math.min(high ?? value, Math.max(low ?? value, value));\n}\n\nfunction scalar(name: string, hint: string, rng: () => number): unknown {\n switch (name) {\n case \"ID\":\n return `${hint.split(/[.:]/)[0] || \"id\"}-${Math.floor(rng() * 900 + 100)}`;\n case \"Int\":\n return Math.floor(rng() * 100);\n case \"Long\":\n return Math.floor(rng() * 1_000_000);\n case \"Float\":\n return Math.round(rng() * 10_000) / 100;\n case \"Decimal\":\n return (Math.round(rng() * 10_000) / 100).toFixed(2);\n case \"Boolean\":\n return rng() > 0.5;\n case \"Instant\":\n return new Date(EPOCH + Math.floor(rng() * 30) * 86_400_000).toISOString();\n case \"Date\":\n return new Date(EPOCH + Math.floor(rng() * 30) * 86_400_000).toISOString().slice(0, 10);\n case \"Duration\":\n return Math.floor(rng() * 60) * 1000;\n case \"Bytes\":\n return btoa(words(rng, 1));\n case \"JSON\":\n return {};\n default:\n return words(rng, 2); // a scalar of the schema's own: a word is the most it can assume\n }\n}\n\nfunction words(rng: () => number, count: number): string {\n return Array.from({ length: count }, () => WORDS[Math.floor(rng() * WORDS.length)]!).join(\" \");\n}\n\n/** The same call gives the same answer: the seed is the call, not the clock. */\nfunction seeded(key: string): () => number {\n let h = 2166136261;\n for (let i = 0; i < key.length; i++) {\n h ^= key.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n let state = h >>> 0;\n return () => {\n state = (state + 0x6d2b79f5) >>> 0;\n let t = state;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n}\n\nfunction stable(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value) ?? \"null\";\n if (Array.isArray(value)) return `[${value.map(stable).join(\",\")}]`;\n return `{${Object.keys(value as Record<string, unknown>)\n .sort()\n .map((k) => `${k}:${stable((value as Record<string, unknown>)[k])}`)\n .join(\",\")}}`;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@rayfold/cli",
3
+ "version": "0.1.0",
4
+ "description": "Rayfold command line: check, lock, hash, explain, gen, shapes, import from OpenAPI or GraphQL, a mock server, a language server, and a dev server with the explorer.",
5
+ "keywords": [
6
+ "rayfold",
7
+ "api",
8
+ "protocol",
9
+ "cli",
10
+ "codegen",
11
+ "breaking-changes"
12
+ ],
13
+ "license": "Apache-2.0",
14
+ "homepage": "https://eddyboutros.github.io/rayfold/",
15
+ "bugs": {
16
+ "url": "https://github.com/eddyboutros/rayfold/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/eddyboutros/rayfold.git",
21
+ "directory": "packages/cli"
22
+ },
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "bin": {
28
+ "rayfold": "main.js"
29
+ },
30
+ "dependencies": {
31
+ "@rayfold/explorer": "^0.1.0",
32
+ "@rayfold/lsp": "^0.1.0",
33
+ "@rayfold/schema": "^0.1.0",
34
+ "@rayfold/server": "^0.1.0",
35
+ "@rayfold/rb": "^0.1.0"
36
+ },
37
+ "peerDependencies": {
38
+ "graphql": "^16.0.0 || ^17.0.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "graphql": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }
package/report.js ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Findings with the text they are about.
3
+ *
4
+ * `validateIR` reports against a schema coordinate ("Book.price", "books()"), which is precise but leaves the author
5
+ * to find the line. The language server already knows where every declaration sits, so `rayfold check` can show the
6
+ * line, point at it, and - where the fix is obvious from the schema itself - say what it probably should be.
7
+ */
8
+ import { indexDocument, rangeForPath } from "@rayfold/lsp";
9
+ import { KNOWN_ANNOTATIONS, baseName } from "@rayfold/schema";
10
+ const EMPTY = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
11
+ /** Every diagnostic, placed in the text it is about. */
12
+ export function findingsFor(text, diagnostics) {
13
+ const index = indexDocument(text);
14
+ return diagnostics.map((d) => ({ severity: d.severity, code: d.code, at: d.at, message: d.message, range: rangeForPath(index, d.at) }));
15
+ }
16
+ /** A syntax error knows its own position and nothing else: there is no schema to ask. */
17
+ export function syntaxFinding(error) {
18
+ const line = Math.max(0, (error.line ?? 1) - 1);
19
+ const character = Math.max(0, (error.col ?? 1) - 1);
20
+ return {
21
+ severity: "error",
22
+ code: "syntax",
23
+ at: "",
24
+ message: error.message,
25
+ range: { start: { line, character }, end: { line, character: character + 1 } },
26
+ };
27
+ }
28
+ /**
29
+ * One finding as an author reads it:
30
+ *
31
+ * error unknown-type Book.price: Unknown type Money
32
+ * --> bookstore.rayfold:12:3
33
+ * |
34
+ * 12 | price: Money
35
+ * | ^^^^^
36
+ * = did you mean Decimal?
37
+ */
38
+ export function renderFinding(file, text, finding, ir) {
39
+ const label = finding.severity === "error" ? "error " : "warning";
40
+ const head = `${label} ${finding.code} ${finding.at ? `${finding.at}: ` : ""}${finding.message}`;
41
+ const placed = finding.range.start.line !== finding.range.end.line || finding.range.start.character !== finding.range.end.character;
42
+ if (!placed)
43
+ return head; // nothing in the text to point at, so do not point at line 1 and mislead
44
+ const lines = text.split("\n").map((l) => (l.endsWith("\r") ? l.slice(0, -1) : l));
45
+ const number = finding.range.start.line + 1;
46
+ const source = lines[finding.range.start.line] ?? "";
47
+ const gutter = " ".repeat(String(number).length);
48
+ const width = Math.max(1, finding.range.end.character - finding.range.start.character);
49
+ const out = [
50
+ head,
51
+ `${gutter}--> ${file}:${number}:${finding.range.start.character + 1}`,
52
+ `${gutter} |`,
53
+ `${number} | ${source}`,
54
+ `${gutter} | ${" ".repeat(finding.range.start.character)}${"^".repeat(width)}`,
55
+ ];
56
+ const fix = ir ? suggestionFor(finding, ir) : undefined;
57
+ if (fix)
58
+ out.push(`${gutter} = ${fix}`);
59
+ return out.join("\n");
60
+ }
61
+ /** What the schema itself says the fix probably is. Nothing is suggested unless the schema makes it obvious. */
62
+ export function suggestionFor(finding, ir) {
63
+ const [owner, member] = split(finding.at);
64
+ switch (finding.code) {
65
+ case "unknown-type": {
66
+ const missing = missingTypeAt(ir, owner, member);
67
+ if (!missing)
68
+ return undefined;
69
+ const near = nearest(missing, Object.keys(ir.types));
70
+ return near ? `did you mean ${near}?` : `no type named ${missing} is defined; declare it, or import the document that has it`;
71
+ }
72
+ case "unknown-annotation": {
73
+ const unknown = unknownAnnotationAt(ir, owner, member);
74
+ if (!unknown)
75
+ return undefined;
76
+ const near = nearest(unknown, Object.keys(KNOWN_ANNOTATIONS));
77
+ return near ? `did you mean @${near}?` : `write it as @vendor.${unknown} to keep an annotation of your own`;
78
+ }
79
+ case "entity-id":
80
+ return "an entity is addressed by identity: give it an `id: ID` field, or make it an `object`";
81
+ case "reserved-name":
82
+ return "the protocol owns that name on the wire; call the field something else";
83
+ case "policy-this-on-op":
84
+ return "`this` is a row; an operation has none. Put the policy on the type, or use `args` and `viewer`";
85
+ default:
86
+ return undefined;
87
+ }
88
+ }
89
+ function split(at) {
90
+ const coordinate = at.endsWith("()") ? at.slice(0, -2) : at;
91
+ const dot = coordinate.indexOf(".");
92
+ return dot < 0 ? [coordinate, undefined] : [coordinate.slice(0, dot), coordinate.slice(dot + 1)];
93
+ }
94
+ /** The type name the coordinate points at that the schema has no definition for. */
95
+ function missingTypeAt(ir, owner, member) {
96
+ const refs = [];
97
+ const op = ir.ops[owner];
98
+ if (op) {
99
+ refs.push(op.returns, ...op.args.map((a) => a.type));
100
+ }
101
+ const type = ir.types[owner];
102
+ if (type && "fields" in type) {
103
+ for (const f of type.fields) {
104
+ if (member !== undefined && f.name !== member)
105
+ continue;
106
+ refs.push(f.type, ...f.args.map((a) => a.type));
107
+ }
108
+ }
109
+ if (type?.kind === "union")
110
+ for (const m of type.members)
111
+ if (!ir.types[m])
112
+ return m;
113
+ for (const ref of refs) {
114
+ const name = baseName(ref);
115
+ if (!ir.types[name])
116
+ return name;
117
+ }
118
+ return undefined;
119
+ }
120
+ function unknownAnnotationAt(ir, owner, member) {
121
+ const carriers = [];
122
+ const op = ir.ops[owner];
123
+ if (op)
124
+ carriers.push(op, ...op.args);
125
+ const type = ir.types[owner];
126
+ if (type) {
127
+ carriers.push(type);
128
+ if ("fields" in type)
129
+ for (const f of type.fields)
130
+ if (member === undefined || f.name === member)
131
+ carriers.push(f, ...f.args);
132
+ if ("values" in type)
133
+ for (const v of type.values)
134
+ if (member === undefined || v.name === member)
135
+ carriers.push(v);
136
+ }
137
+ for (const carrier of carriers) {
138
+ for (const a of carrier.annotations) {
139
+ if (!a.name.includes(".") && !KNOWN_ANNOTATIONS[a.name])
140
+ return a.name;
141
+ }
142
+ }
143
+ return undefined;
144
+ }
145
+ /** The closest known name, when it is close enough to be a typo rather than a different word. */
146
+ export function nearest(word, known) {
147
+ let best;
148
+ let bestDistance = Number.POSITIVE_INFINITY;
149
+ for (const candidate of known) {
150
+ const d = distance(word.toLowerCase(), candidate.toLowerCase());
151
+ if (d < bestDistance) {
152
+ bestDistance = d;
153
+ best = candidate;
154
+ }
155
+ }
156
+ const allowed = word.length <= 4 ? 1 : word.length <= 8 ? 2 : 3;
157
+ return best !== undefined && bestDistance <= allowed ? best : undefined;
158
+ }
159
+ function distance(a, b) {
160
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
161
+ for (let i = 1; i <= a.length; i++) {
162
+ const row = [i];
163
+ for (let j = 1; j <= b.length; j++) {
164
+ const substitute = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
165
+ row.push(Math.min(substitute, previous[j] + 1, row[j - 1] + 1));
166
+ }
167
+ previous = row;
168
+ }
169
+ return previous[b.length];
170
+ }
171
+ //# sourceMappingURL=report.js.map
package/report.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Findings with the text they are about.\n *\n * `validateIR` reports against a schema coordinate (\"Book.price\", \"books()\"), which is precise but leaves the author\n * to find the line. The language server already knows where every declaration sits, so `rayfold check` can show the\n * line, point at it, and - where the fix is obvious from the schema itself - say what it probably should be.\n */\nimport { indexDocument, rangeForPath, type Range } from \"@rayfold/lsp\";\nimport { KNOWN_ANNOTATIONS, baseName, type Diagnostic, type RayfoldSchemaIR, type TypeRef } from \"@rayfold/schema\";\n\nexport interface Finding {\n severity: \"error\" | \"warning\";\n code: string;\n /** The schema coordinate, or \"\" for a syntax error, which has nothing but a position. */\n at: string;\n message: string;\n range: Range;\n}\n\nconst EMPTY: Range = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };\n\n/** Every diagnostic, placed in the text it is about. */\nexport function findingsFor(text: string, diagnostics: Diagnostic[]): Finding[] {\n const index = indexDocument(text);\n return diagnostics.map((d) => ({ severity: d.severity, code: d.code, at: d.at, message: d.message, range: rangeForPath(index, d.at) }));\n}\n\n/** A syntax error knows its own position and nothing else: there is no schema to ask. */\nexport function syntaxFinding(error: { message: string; line?: number; col?: number }): Finding {\n const line = Math.max(0, (error.line ?? 1) - 1);\n const character = Math.max(0, (error.col ?? 1) - 1);\n return {\n severity: \"error\",\n code: \"syntax\",\n at: \"\",\n message: error.message,\n range: { start: { line, character }, end: { line, character: character + 1 } },\n };\n}\n\n/**\n * One finding as an author reads it:\n *\n * error unknown-type Book.price: Unknown type Money\n * --> bookstore.rayfold:12:3\n * |\n * 12 | price: Money\n * | ^^^^^\n * = did you mean Decimal?\n */\nexport function renderFinding(file: string, text: string, finding: Finding, ir?: RayfoldSchemaIR): string {\n const label = finding.severity === \"error\" ? \"error \" : \"warning\";\n const head = `${label} ${finding.code} ${finding.at ? `${finding.at}: ` : \"\"}${finding.message}`;\n const placed = finding.range.start.line !== finding.range.end.line || finding.range.start.character !== finding.range.end.character;\n if (!placed) return head; // nothing in the text to point at, so do not point at line 1 and mislead\n\n const lines = text.split(\"\\n\").map((l) => (l.endsWith(\"\\r\") ? l.slice(0, -1) : l));\n const number = finding.range.start.line + 1;\n const source = lines[finding.range.start.line] ?? \"\";\n const gutter = \" \".repeat(String(number).length);\n const width = Math.max(1, finding.range.end.character - finding.range.start.character);\n const out = [\n head,\n `${gutter}--> ${file}:${number}:${finding.range.start.character + 1}`,\n `${gutter} |`,\n `${number} | ${source}`,\n `${gutter} | ${\" \".repeat(finding.range.start.character)}${\"^\".repeat(width)}`,\n ];\n const fix = ir ? suggestionFor(finding, ir) : undefined;\n if (fix) out.push(`${gutter} = ${fix}`);\n return out.join(\"\\n\");\n}\n\n/** What the schema itself says the fix probably is. Nothing is suggested unless the schema makes it obvious. */\nexport function suggestionFor(finding: Finding, ir: RayfoldSchemaIR): string | undefined {\n const [owner, member] = split(finding.at);\n switch (finding.code) {\n case \"unknown-type\": {\n const missing = missingTypeAt(ir, owner, member);\n if (!missing) return undefined;\n const near = nearest(missing, Object.keys(ir.types));\n return near ? `did you mean ${near}?` : `no type named ${missing} is defined; declare it, or import the document that has it`;\n }\n case \"unknown-annotation\": {\n const unknown = unknownAnnotationAt(ir, owner, member);\n if (!unknown) return undefined;\n const near = nearest(unknown, Object.keys(KNOWN_ANNOTATIONS));\n return near ? `did you mean @${near}?` : `write it as @vendor.${unknown} to keep an annotation of your own`;\n }\n case \"entity-id\":\n return \"an entity is addressed by identity: give it an `id: ID` field, or make it an `object`\";\n case \"reserved-name\":\n return \"the protocol owns that name on the wire; call the field something else\";\n case \"policy-this-on-op\":\n return \"`this` is a row; an operation has none. Put the policy on the type, or use `args` and `viewer`\";\n default:\n return undefined;\n }\n}\n\nfunction split(at: string): [string, string | undefined] {\n const coordinate = at.endsWith(\"()\") ? at.slice(0, -2) : at;\n const dot = coordinate.indexOf(\".\");\n return dot < 0 ? [coordinate, undefined] : [coordinate.slice(0, dot), coordinate.slice(dot + 1)];\n}\n\n/** The type name the coordinate points at that the schema has no definition for. */\nfunction missingTypeAt(ir: RayfoldSchemaIR, owner: string, member: string | undefined): string | undefined {\n const refs: TypeRef[] = [];\n const op = ir.ops[owner];\n if (op) {\n refs.push(op.returns, ...op.args.map((a) => a.type));\n }\n const type = ir.types[owner];\n if (type && \"fields\" in type) {\n for (const f of type.fields) {\n if (member !== undefined && f.name !== member) continue;\n refs.push(f.type, ...f.args.map((a) => a.type));\n }\n }\n if (type?.kind === \"union\") for (const m of type.members) if (!ir.types[m]) return m;\n for (const ref of refs) {\n const name = baseName(ref);\n if (!ir.types[name]) return name;\n }\n return undefined;\n}\n\nfunction unknownAnnotationAt(ir: RayfoldSchemaIR, owner: string, member: string | undefined): string | undefined {\n const carriers: Array<{ annotations: Array<{ name: string }> }> = [];\n const op = ir.ops[owner];\n if (op) carriers.push(op, ...op.args);\n const type = ir.types[owner];\n if (type) {\n carriers.push(type);\n if (\"fields\" in type) for (const f of type.fields) if (member === undefined || f.name === member) carriers.push(f, ...f.args);\n if (\"values\" in type) for (const v of type.values) if (member === undefined || v.name === member) carriers.push(v);\n }\n for (const carrier of carriers) {\n for (const a of carrier.annotations) {\n if (!a.name.includes(\".\") && !KNOWN_ANNOTATIONS[a.name]) return a.name;\n }\n }\n return undefined;\n}\n\n/** The closest known name, when it is close enough to be a typo rather than a different word. */\nexport function nearest(word: string, known: string[]): string | undefined {\n let best: string | undefined;\n let bestDistance = Number.POSITIVE_INFINITY;\n for (const candidate of known) {\n const d = distance(word.toLowerCase(), candidate.toLowerCase());\n if (d < bestDistance) {\n bestDistance = d;\n best = candidate;\n }\n }\n const allowed = word.length <= 4 ? 1 : word.length <= 8 ? 2 : 3;\n return best !== undefined && bestDistance <= allowed ? best : undefined;\n}\n\nfunction distance(a: string, b: string): number {\n let previous = Array.from({ length: b.length + 1 }, (_, i) => i);\n for (let i = 1; i <= a.length; i++) {\n const row = [i];\n for (let j = 1; j <= b.length; j++) {\n const substitute = previous[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1);\n row.push(Math.min(substitute, previous[j]! + 1, row[j - 1]! + 1));\n }\n previous = row;\n }\n return previous[b.length]!;\n}\n"]}