ont-run 0.0.1

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 (54) hide show
  1. package/README.md +228 -0
  2. package/bin/ont.ts +5 -0
  3. package/dist/bin/ont.d.ts +2 -0
  4. package/dist/bin/ont.js +13667 -0
  5. package/dist/index.js +23152 -0
  6. package/dist/src/browser/server.d.ts +16 -0
  7. package/dist/src/browser/transform.d.ts +87 -0
  8. package/dist/src/cli/commands/init.d.ts +12 -0
  9. package/dist/src/cli/commands/review.d.ts +17 -0
  10. package/dist/src/cli/index.d.ts +1 -0
  11. package/dist/src/cli/utils/config-loader.d.ts +13 -0
  12. package/dist/src/config/categorical.d.ts +76 -0
  13. package/dist/src/config/define.d.ts +46 -0
  14. package/dist/src/config/index.d.ts +4 -0
  15. package/dist/src/config/schema.d.ts +162 -0
  16. package/dist/src/config/types.d.ts +94 -0
  17. package/dist/src/index.d.ts +37 -0
  18. package/dist/src/lockfile/differ.d.ts +11 -0
  19. package/dist/src/lockfile/hasher.d.ts +31 -0
  20. package/dist/src/lockfile/index.d.ts +53 -0
  21. package/dist/src/lockfile/types.d.ts +90 -0
  22. package/dist/src/runtime/index.d.ts +28 -0
  23. package/dist/src/server/api/index.d.ts +20 -0
  24. package/dist/src/server/api/middleware.d.ts +34 -0
  25. package/dist/src/server/api/router.d.ts +18 -0
  26. package/dist/src/server/mcp/index.d.ts +23 -0
  27. package/dist/src/server/mcp/tools.d.ts +35 -0
  28. package/dist/src/server/resolver.d.ts +30 -0
  29. package/dist/src/server/start.d.ts +37 -0
  30. package/package.json +63 -0
  31. package/src/browser/server.ts +2567 -0
  32. package/src/browser/transform.ts +473 -0
  33. package/src/cli/commands/init.ts +226 -0
  34. package/src/cli/commands/review.ts +126 -0
  35. package/src/cli/index.ts +19 -0
  36. package/src/cli/utils/config-loader.ts +78 -0
  37. package/src/config/categorical.ts +101 -0
  38. package/src/config/define.ts +78 -0
  39. package/src/config/index.ts +23 -0
  40. package/src/config/schema.ts +196 -0
  41. package/src/config/types.ts +121 -0
  42. package/src/index.ts +53 -0
  43. package/src/lockfile/differ.ts +242 -0
  44. package/src/lockfile/hasher.ts +175 -0
  45. package/src/lockfile/index.ts +159 -0
  46. package/src/lockfile/types.ts +95 -0
  47. package/src/runtime/index.ts +114 -0
  48. package/src/server/api/index.ts +92 -0
  49. package/src/server/api/middleware.ts +118 -0
  50. package/src/server/api/router.ts +102 -0
  51. package/src/server/mcp/index.ts +182 -0
  52. package/src/server/mcp/tools.ts +199 -0
  53. package/src/server/resolver.ts +109 -0
  54. package/src/server/start.ts +151 -0
@@ -0,0 +1,126 @@
1
+ import { defineCommand } from "citty";
2
+ import consola from "consola";
3
+ import { loadConfig } from "../utils/config-loader.js";
4
+ import {
5
+ computeOntologyHash,
6
+ readLockfile,
7
+ diffOntology,
8
+ formatDiffForConsole,
9
+ writeLockfile,
10
+ } from "../../lockfile/index.js";
11
+ import { startBrowserServer } from "../../browser/server.js";
12
+
13
+ export const reviewCommand = defineCommand({
14
+ meta: {
15
+ name: "review",
16
+ description: "Review and approve ontology changes",
17
+ },
18
+ args: {
19
+ "auto-approve": {
20
+ type: "boolean",
21
+ description: "Automatically approve changes (for CI/scripts)",
22
+ default: false,
23
+ },
24
+ "print-only": {
25
+ type: "boolean",
26
+ description: "Print diff and exit without prompting",
27
+ default: false,
28
+ },
29
+ cloud: {
30
+ type: "boolean",
31
+ description: "Sync with ont Cloud for team approvals",
32
+ default: false,
33
+ },
34
+ },
35
+ async run({ args }) {
36
+ try {
37
+ // Check for cloud mode
38
+ const cloudToken = process.env.ONT_CLOUD_TOKEN;
39
+ if (args.cloud || cloudToken) {
40
+ consola.info("");
41
+ consola.box(
42
+ "ont Cloud coming soon!\n\n" +
43
+ "Team approvals, audit trails, and compliance reporting.\n\n" +
44
+ "Sign up for early access: https://ont.dev/cloud"
45
+ );
46
+ process.exit(0);
47
+ }
48
+
49
+ // Load config
50
+ consola.info("Loading ontology config...");
51
+ const { config, configDir } = await loadConfig();
52
+
53
+ // Compute current ontology snapshot
54
+ const { ontology: newOntology, hash: newHash } = computeOntologyHash(config);
55
+
56
+ // Load existing lockfile
57
+ const lockfile = await readLockfile(configDir);
58
+ const oldOntology = lockfile?.ontology || null;
59
+
60
+ // Compute diff
61
+ const diff = diffOntology(oldOntology, newOntology);
62
+
63
+ // Handle print-only mode
64
+ if (args["print-only"]) {
65
+ if (diff.hasChanges) {
66
+ console.log("\n" + formatDiffForConsole(diff) + "\n");
67
+ process.exit(1);
68
+ } else {
69
+ consola.success("No ontology changes detected.");
70
+ process.exit(0);
71
+ }
72
+ }
73
+
74
+ // Handle auto-approve mode
75
+ if (args["auto-approve"]) {
76
+ if (diff.hasChanges) {
77
+ console.log("\n" + formatDiffForConsole(diff) + "\n");
78
+ consola.info("Auto-approving changes...");
79
+ await writeLockfile(configDir, newOntology, newHash);
80
+ consola.success("Changes approved. Lockfile updated.");
81
+ } else {
82
+ consola.success("No ontology changes detected.");
83
+ if (!lockfile) {
84
+ consola.info("Writing initial lockfile...");
85
+ await writeLockfile(configDir, newOntology, newHash);
86
+ consola.success("Created ont.lock");
87
+ }
88
+ }
89
+ return;
90
+ }
91
+
92
+ // Handle initial lockfile creation (no previous state)
93
+ if (!lockfile && !diff.hasChanges) {
94
+ consola.info("Writing initial lockfile...");
95
+ await writeLockfile(configDir, newOntology, newHash);
96
+ consola.success("Created ont.lock");
97
+ }
98
+
99
+ // Print diff summary to console if there are changes
100
+ if (diff.hasChanges) {
101
+ console.log("\n" + formatDiffForConsole(diff) + "\n");
102
+ }
103
+
104
+ // Start unified browser/review UI
105
+ consola.info(diff.hasChanges ? "Starting review UI..." : "Starting ontology browser...");
106
+ const result = await startBrowserServer({
107
+ config,
108
+ diff: diff.hasChanges ? diff : null,
109
+ configDir,
110
+ });
111
+
112
+ if (diff.hasChanges) {
113
+ if (result.approved) {
114
+ consola.success("Changes approved. Lockfile updated.");
115
+ process.exit(0);
116
+ } else {
117
+ consola.warn("Changes rejected.");
118
+ process.exit(1);
119
+ }
120
+ }
121
+ } catch (error) {
122
+ consola.error(error instanceof Error ? error.message : "Unknown error");
123
+ process.exit(1);
124
+ }
125
+ },
126
+ });
@@ -0,0 +1,19 @@
1
+ import { defineCommand, runMain } from "citty";
2
+ import { initCommand } from "./commands/init.js";
3
+ import { reviewCommand } from "./commands/review.js";
4
+
5
+ const main = defineCommand({
6
+ meta: {
7
+ name: "ont",
8
+ description: "Ontology - Ontology-first backends with human-approved AI access & edits",
9
+ version: "0.1.0",
10
+ },
11
+ subCommands: {
12
+ init: initCommand,
13
+ review: reviewCommand,
14
+ },
15
+ });
16
+
17
+ export function run() {
18
+ runMain(main);
19
+ }
@@ -0,0 +1,78 @@
1
+ import { existsSync } from "fs";
2
+ import { join, dirname, resolve } from "path";
3
+ import type { OntologyConfig } from "../../config/types.js";
4
+
5
+ const CONFIG_FILENAMES = ["ontology.config.ts", "ontology.config.js"];
6
+
7
+ /**
8
+ * Find the ontology config file in the given directory or its parents
9
+ */
10
+ export function findConfigFile(startDir: string = process.cwd()): string | null {
11
+ let currentDir = resolve(startDir);
12
+
13
+ while (true) {
14
+ for (const filename of CONFIG_FILENAMES) {
15
+ const configPath = join(currentDir, filename);
16
+ if (existsSync(configPath)) {
17
+ return configPath;
18
+ }
19
+ }
20
+
21
+ const parentDir = dirname(currentDir);
22
+ if (parentDir === currentDir) {
23
+ // Reached root
24
+ return null;
25
+ }
26
+ currentDir = parentDir;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Load the ontology config from a file
32
+ */
33
+ export async function loadConfig(configPath?: string): Promise<{
34
+ config: OntologyConfig;
35
+ configDir: string;
36
+ configPath: string;
37
+ }> {
38
+ // Find config file if not provided
39
+ const resolvedPath = configPath || findConfigFile();
40
+
41
+ if (!resolvedPath) {
42
+ throw new Error(
43
+ "Could not find ontology.config.ts in current directory or any parent directory.\n" +
44
+ "Run `ont init` to create a new project."
45
+ );
46
+ }
47
+
48
+ if (!existsSync(resolvedPath)) {
49
+ throw new Error(`Config file not found: ${resolvedPath}`);
50
+ }
51
+
52
+ const configDir = dirname(resolvedPath);
53
+
54
+ try {
55
+ // Dynamic import the config
56
+ const module = await import(resolvedPath);
57
+ const config = module.default as OntologyConfig;
58
+
59
+ if (!config || typeof config !== "object") {
60
+ throw new Error(
61
+ "Config file must export a default object created with defineOntology()"
62
+ );
63
+ }
64
+
65
+ if (!config.name || !config.functions) {
66
+ throw new Error(
67
+ "Invalid config: missing required fields (name, functions)"
68
+ );
69
+ }
70
+
71
+ return { config, configDir, configPath: resolvedPath };
72
+ } catch (error) {
73
+ if ((error as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND") {
74
+ throw new Error(`Failed to load config: ${resolvedPath}`);
75
+ }
76
+ throw error;
77
+ }
78
+ }
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Symbol for storing fieldFrom metadata on Zod schemas
5
+ */
6
+ export const FIELD_FROM_METADATA = Symbol.for("ont:fieldFrom");
7
+
8
+ /**
9
+ * Metadata stored on fieldFrom Zod schemas
10
+ */
11
+ export interface FieldFromMetadata {
12
+ /** Name of the function that provides options for this field */
13
+ functionName: string;
14
+ }
15
+
16
+ /**
17
+ * Type for a Zod string with fieldFrom metadata
18
+ */
19
+ export type FieldFromString = z.ZodString & {
20
+ [FIELD_FROM_METADATA]: FieldFromMetadata;
21
+ };
22
+
23
+ /**
24
+ * Create a string field that gets its options from another function.
25
+ *
26
+ * The referenced function should return `{ value: string, label: string }[]`.
27
+ * - If the function has empty inputs `z.object({})`, it's treated as **bulk** (all options fetched at once)
28
+ * - If the function has a `query` input, it's treated as **autocomplete** (options searched)
29
+ *
30
+ * @param functionName - Name of the function that provides options
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * defineOntology({
35
+ * functions: {
36
+ * // Bulk options source (empty inputs)
37
+ * getUserStatuses: {
38
+ * description: 'Get available user statuses',
39
+ * access: ['admin'],
40
+ * entities: [],
41
+ * inputs: z.object({}),
42
+ * outputs: z.array(z.object({ value: z.string(), label: z.string() })),
43
+ * resolver: './resolvers/options/userStatuses.ts',
44
+ * },
45
+ *
46
+ * // Autocomplete source (has query input)
47
+ * searchTeams: {
48
+ * description: 'Search for teams',
49
+ * access: ['admin'],
50
+ * entities: [],
51
+ * inputs: z.object({ query: z.string() }),
52
+ * outputs: z.array(z.object({ value: z.string(), label: z.string() })),
53
+ * resolver: './resolvers/options/searchTeams.ts',
54
+ * },
55
+ *
56
+ * // Function using fieldFrom
57
+ * createUser: {
58
+ * description: 'Create a user',
59
+ * access: ['admin'],
60
+ * entities: ['User'],
61
+ * inputs: z.object({
62
+ * name: z.string(),
63
+ * status: fieldFrom('getUserStatuses'),
64
+ * team: fieldFrom('searchTeams'),
65
+ * }),
66
+ * resolver: './resolvers/createUser.ts',
67
+ * },
68
+ * },
69
+ * })
70
+ * ```
71
+ */
72
+ export function fieldFrom(functionName: string): FieldFromString {
73
+ const schema = z.string() as FieldFromString;
74
+ schema[FIELD_FROM_METADATA] = { functionName };
75
+ return schema;
76
+ }
77
+
78
+ /**
79
+ * Check if a Zod schema has fieldFrom metadata
80
+ */
81
+ export function hasFieldFromMetadata(
82
+ schema: unknown
83
+ ): schema is FieldFromString {
84
+ return (
85
+ schema !== null &&
86
+ typeof schema === "object" &&
87
+ FIELD_FROM_METADATA in schema
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Extract fieldFrom metadata from a Zod schema
93
+ */
94
+ export function getFieldFromMetadata(
95
+ schema: unknown
96
+ ): FieldFromMetadata | null {
97
+ if (hasFieldFromMetadata(schema)) {
98
+ return schema[FIELD_FROM_METADATA];
99
+ }
100
+ return null;
101
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ OntologyConfigSchema,
3
+ validateAccessGroups,
4
+ validateEntityReferences,
5
+ validateFieldFromReferences,
6
+ } from "./schema.js";
7
+ import type {
8
+ OntologyConfig,
9
+ FunctionDefinition,
10
+ AccessGroupConfig,
11
+ EnvironmentConfig,
12
+ EntityDefinition,
13
+ AuthFunction,
14
+ } from "./types.js";
15
+
16
+ /**
17
+ * Define an Ontology configuration with full type inference.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { defineOntology, fieldFrom } from 'ont-run';
22
+ * import { z } from 'zod';
23
+ *
24
+ * export default defineOntology({
25
+ * name: 'my-api',
26
+ * environments: {
27
+ * dev: { debug: true },
28
+ * prod: { debug: false },
29
+ * },
30
+ * auth: async (req) => {
31
+ * const token = req.headers.get('Authorization');
32
+ * return token ? ['admin'] : ['public'];
33
+ * },
34
+ * accessGroups: {
35
+ * public: { description: 'Unauthenticated users' },
36
+ * admin: { description: 'Administrators' },
37
+ * },
38
+ * entities: {
39
+ * User: { description: 'A user account' },
40
+ * },
41
+ * functions: {
42
+ * getUser: {
43
+ * description: 'Get a user by ID',
44
+ * access: ['public', 'admin'],
45
+ * entities: ['User'],
46
+ * inputs: z.object({ id: z.string() }),
47
+ * resolver: './resolvers/getUser.ts',
48
+ * },
49
+ * },
50
+ * });
51
+ * ```
52
+ */
53
+ export function defineOntology<
54
+ TGroups extends string,
55
+ TEntities extends string,
56
+ TFunctions extends Record<string, FunctionDefinition<TGroups, TEntities>>,
57
+ >(config: {
58
+ name: string;
59
+ environments: Record<string, EnvironmentConfig>;
60
+ auth: AuthFunction;
61
+ accessGroups: Record<TGroups, AccessGroupConfig>;
62
+ entities?: Record<TEntities, EntityDefinition>;
63
+ functions: TFunctions;
64
+ }): OntologyConfig<TGroups, TEntities, TFunctions> {
65
+ // Validate the config structure
66
+ const parsed = OntologyConfigSchema.parse(config);
67
+
68
+ // Validate that all access groups referenced in functions exist
69
+ validateAccessGroups(parsed);
70
+
71
+ // Validate that all entities referenced in functions exist
72
+ validateEntityReferences(parsed);
73
+
74
+ // Validate that all fieldFrom() references point to existing functions
75
+ validateFieldFromReferences(parsed);
76
+
77
+ return config as OntologyConfig<TGroups, TEntities, TFunctions>;
78
+ }
@@ -0,0 +1,23 @@
1
+ export { defineOntology } from "./define.js";
2
+ export { fieldFrom } from "./categorical.js";
3
+ export type {
4
+ OntologyConfig,
5
+ FunctionDefinition,
6
+ AccessGroupConfig,
7
+ EnvironmentConfig,
8
+ EntityDefinition,
9
+ AuthFunction,
10
+ ResolverContext,
11
+ ResolverFunction,
12
+ FieldOption,
13
+ } from "./types.js";
14
+ export {
15
+ OntologyConfigSchema,
16
+ FunctionDefinitionSchema,
17
+ AccessGroupConfigSchema,
18
+ EnvironmentConfigSchema,
19
+ EntityDefinitionSchema,
20
+ validateAccessGroups,
21
+ validateEntityReferences,
22
+ validateFieldFromReferences,
23
+ } from "./schema.js";
@@ -0,0 +1,196 @@
1
+ import { z } from "zod";
2
+ import { getFieldFromMetadata } from "./categorical.js";
3
+
4
+ /**
5
+ * Schema for environment configuration
6
+ */
7
+ export const EnvironmentConfigSchema = z
8
+ .object({
9
+ debug: z.boolean().optional(),
10
+ })
11
+ .passthrough();
12
+
13
+ /**
14
+ * Schema for access group configuration
15
+ */
16
+ export const AccessGroupConfigSchema = z.object({
17
+ description: z.string(),
18
+ });
19
+
20
+ /**
21
+ * Schema for entity definition
22
+ */
23
+ export const EntityDefinitionSchema = z.object({
24
+ description: z.string(),
25
+ });
26
+
27
+ /**
28
+ * Check if a value is a Zod schema (duck typing to work across bundle boundaries)
29
+ */
30
+ function isZodSchema(val: unknown): boolean {
31
+ return (
32
+ val !== null &&
33
+ typeof val === "object" &&
34
+ "_def" in val &&
35
+ "safeParse" in val &&
36
+ typeof (val as { safeParse: unknown }).safeParse === "function"
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Schema for function definition
42
+ */
43
+ export const FunctionDefinitionSchema = z.object({
44
+ description: z.string(),
45
+ access: z.array(z.string()).min(1),
46
+ entities: z.array(z.string()),
47
+ inputs: z.custom<z.ZodType>(isZodSchema, {
48
+ message: "inputs must be a Zod schema",
49
+ }),
50
+ outputs: z
51
+ .custom<z.ZodType>(isZodSchema, {
52
+ message: "outputs must be a Zod schema",
53
+ })
54
+ .optional(),
55
+ resolver: z.string(),
56
+ });
57
+
58
+ /**
59
+ * Schema for auth function
60
+ */
61
+ export const AuthFunctionSchema = z
62
+ .function()
63
+ .args(z.custom<Request>())
64
+ .returns(z.union([z.array(z.string()), z.promise(z.array(z.string()))]));
65
+
66
+ /**
67
+ * Schema for the full ontology configuration
68
+ */
69
+ export const OntologyConfigSchema = z.object({
70
+ name: z.string().min(1),
71
+ environments: z.record(z.string(), EnvironmentConfigSchema),
72
+ auth: z.function(),
73
+ accessGroups: z.record(z.string(), AccessGroupConfigSchema),
74
+ entities: z.record(z.string(), EntityDefinitionSchema).optional(),
75
+ functions: z.record(z.string(), FunctionDefinitionSchema),
76
+ });
77
+
78
+ /**
79
+ * Validate that all function access groups exist in accessGroups
80
+ */
81
+ export function validateAccessGroups(
82
+ config: z.infer<typeof OntologyConfigSchema>
83
+ ): void {
84
+ const validGroups = new Set(Object.keys(config.accessGroups));
85
+
86
+ for (const [fnName, fn] of Object.entries(config.functions)) {
87
+ for (const group of fn.access) {
88
+ if (!validGroups.has(group)) {
89
+ throw new Error(
90
+ `Function "${fnName}" references unknown access group "${group}". ` +
91
+ `Valid groups: ${[...validGroups].join(", ")}`
92
+ );
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Validate that all function entity references exist in entities
100
+ */
101
+ export function validateEntityReferences(
102
+ config: z.infer<typeof OntologyConfigSchema>
103
+ ): void {
104
+ const validEntities = config.entities
105
+ ? new Set(Object.keys(config.entities))
106
+ : new Set<string>();
107
+
108
+ for (const [fnName, fn] of Object.entries(config.functions)) {
109
+ for (const entity of fn.entities) {
110
+ if (!validEntities.has(entity)) {
111
+ const validList =
112
+ validEntities.size > 0
113
+ ? [...validEntities].join(", ")
114
+ : "(none defined)";
115
+ throw new Error(
116
+ `Function "${fnName}" references unknown entity "${entity}". ` +
117
+ `Valid entities: ${validList}`
118
+ );
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Recursively extract fieldFrom references from a Zod schema
126
+ */
127
+ function extractFieldFromRefs(
128
+ schema: z.ZodType<unknown>,
129
+ path: string = ""
130
+ ): Array<{ path: string; functionName: string }> {
131
+ const results: Array<{ path: string; functionName: string }> = [];
132
+
133
+ // Check if this schema has fieldFrom metadata
134
+ const metadata = getFieldFromMetadata(schema);
135
+ if (metadata) {
136
+ results.push({
137
+ path: path || "(root)",
138
+ functionName: metadata.functionName,
139
+ });
140
+ }
141
+
142
+ // Handle ZodObject - recurse into properties
143
+ if (schema instanceof z.ZodObject) {
144
+ const shape = schema.shape;
145
+ for (const [key, value] of Object.entries(shape)) {
146
+ const fieldPath = path ? `${path}.${key}` : key;
147
+ results.push(
148
+ ...extractFieldFromRefs(value as z.ZodType<unknown>, fieldPath)
149
+ );
150
+ }
151
+ }
152
+
153
+ // Handle ZodOptional - unwrap
154
+ if (schema instanceof z.ZodOptional) {
155
+ results.push(...extractFieldFromRefs(schema.unwrap(), path));
156
+ }
157
+
158
+ // Handle ZodNullable - unwrap
159
+ if (schema instanceof z.ZodNullable) {
160
+ results.push(...extractFieldFromRefs(schema.unwrap(), path));
161
+ }
162
+
163
+ // Handle ZodArray - recurse into element
164
+ if (schema instanceof z.ZodArray) {
165
+ results.push(...extractFieldFromRefs(schema.element, `${path}[]`));
166
+ }
167
+
168
+ // Handle ZodDefault - unwrap
169
+ if (schema instanceof z.ZodDefault) {
170
+ results.push(...extractFieldFromRefs(schema._def.innerType, path));
171
+ }
172
+
173
+ return results;
174
+ }
175
+
176
+ /**
177
+ * Validate that all fieldFrom() references point to existing functions
178
+ */
179
+ export function validateFieldFromReferences(
180
+ config: z.infer<typeof OntologyConfigSchema>
181
+ ): void {
182
+ const validFunctions = new Set(Object.keys(config.functions));
183
+
184
+ for (const [fnName, fn] of Object.entries(config.functions)) {
185
+ const refs = extractFieldFromRefs(fn.inputs);
186
+
187
+ for (const ref of refs) {
188
+ if (!validFunctions.has(ref.functionName)) {
189
+ throw new Error(
190
+ `Function "${fnName}" field "${ref.path}" references unknown function "${ref.functionName}" via fieldFrom(). ` +
191
+ `Valid functions: ${[...validFunctions].join(", ")}`
192
+ );
193
+ }
194
+ }
195
+ }
196
+ }