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,121 @@
1
+ import type { z } from "zod";
2
+
3
+ /**
4
+ * Configuration for an environment (dev, test, prod)
5
+ */
6
+ export interface EnvironmentConfig {
7
+ /** Enable debug mode */
8
+ debug?: boolean;
9
+ /** Any additional environment-specific settings */
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ /**
14
+ * Configuration for an access group
15
+ */
16
+ export interface AccessGroupConfig {
17
+ /** Description of this access group */
18
+ description: string;
19
+ }
20
+
21
+ /**
22
+ * Definition of an entity in the ontology
23
+ */
24
+ export interface EntityDefinition {
25
+ /** Human-readable description of this entity */
26
+ description: string;
27
+ }
28
+
29
+ /**
30
+ * Option returned by functions used as field sources.
31
+ * Functions referenced by `fieldFrom()` should return an array of these.
32
+ */
33
+ export interface FieldOption {
34
+ /** The stored value */
35
+ value: string;
36
+ /** Human-readable label for display */
37
+ label: string;
38
+ }
39
+
40
+ /**
41
+ * Definition of a function in the ontology
42
+ */
43
+ export interface FunctionDefinition<
44
+ TGroups extends string = string,
45
+ TEntities extends string = string,
46
+ > {
47
+ /** Human-readable description of what this function does */
48
+ description: string;
49
+ /** Which access groups can call this function */
50
+ access: TGroups[];
51
+ /** Which entities this function relates to (use empty array [] if none) */
52
+ entities: TEntities[];
53
+ /** Zod schema for input validation */
54
+ inputs: z.ZodType<unknown>;
55
+ /** Zod schema for output validation/documentation */
56
+ outputs?: z.ZodType<unknown>;
57
+ /** Path to the resolver file (relative to ontology.config.ts) */
58
+ resolver: string;
59
+ }
60
+
61
+ /**
62
+ * Auth function that determines access groups for a request
63
+ */
64
+ export type AuthFunction = (req: Request) => Promise<string[]> | string[];
65
+
66
+ /**
67
+ * The main Ontology configuration object
68
+ */
69
+ export interface OntologyConfig<
70
+ TGroups extends string = string,
71
+ TEntities extends string = string,
72
+ TFunctions extends Record<
73
+ string,
74
+ FunctionDefinition<TGroups, TEntities>
75
+ > = Record<string, FunctionDefinition<TGroups, TEntities>>,
76
+ > {
77
+ /** Name of this ontology/API */
78
+ name: string;
79
+
80
+ /** Environment configurations */
81
+ environments: Record<string, EnvironmentConfig>;
82
+
83
+ /** Pluggable auth function */
84
+ auth: AuthFunction;
85
+
86
+ /** Access group definitions */
87
+ accessGroups: Record<TGroups, AccessGroupConfig>;
88
+
89
+ /** Entity definitions for categorization */
90
+ entities?: Record<TEntities, EntityDefinition>;
91
+
92
+ /** Function definitions */
93
+ functions: TFunctions;
94
+ }
95
+
96
+ /**
97
+ * Context passed to resolvers
98
+ */
99
+ export interface ResolverContext {
100
+ /** Current environment name */
101
+ env: string;
102
+ /** Environment configuration */
103
+ envConfig: EnvironmentConfig;
104
+ /** Logger instance */
105
+ logger: {
106
+ info: (message: string, ...args: unknown[]) => void;
107
+ warn: (message: string, ...args: unknown[]) => void;
108
+ error: (message: string, ...args: unknown[]) => void;
109
+ debug: (message: string, ...args: unknown[]) => void;
110
+ };
111
+ /** Access groups for the current request */
112
+ accessGroups: string[];
113
+ }
114
+
115
+ /**
116
+ * Resolver function signature
117
+ */
118
+ export type ResolverFunction<TArgs = unknown, TResult = unknown> = (
119
+ ctx: ResolverContext,
120
+ args: TArgs
121
+ ) => Promise<TResult> | TResult;
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Ontology - Ontology-first backends with human-approved AI access & edits
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { defineOntology, fieldFrom } from 'ont-run';
7
+ * import { z } from 'zod';
8
+ *
9
+ * export default defineOntology({
10
+ * name: 'my-api',
11
+ * environments: { dev: { debug: true }, prod: { debug: false } },
12
+ * auth: async (req) => req.headers.get('Authorization') ? ['admin'] : ['public'],
13
+ * accessGroups: {
14
+ * public: { description: 'Unauthenticated' },
15
+ * admin: { description: 'Administrators' },
16
+ * },
17
+ * entities: {
18
+ * User: { description: 'A user account' },
19
+ * },
20
+ * functions: {
21
+ * hello: {
22
+ * description: 'Say hello',
23
+ * access: ['public'],
24
+ * entities: [],
25
+ * inputs: z.object({ name: z.string() }),
26
+ * resolver: './resolvers/hello.ts',
27
+ * },
28
+ * },
29
+ * });
30
+ * ```
31
+ */
32
+
33
+ // Main API
34
+ export { defineOntology } from "./config/define.js";
35
+ export { fieldFrom } from "./config/categorical.js";
36
+ export { startOnt } from "./server/start.js";
37
+ export type { StartOntOptions, StartOntResult } from "./server/start.js";
38
+
39
+ // Types
40
+ export type {
41
+ OntologyConfig,
42
+ FunctionDefinition,
43
+ AccessGroupConfig,
44
+ EnvironmentConfig,
45
+ EntityDefinition,
46
+ AuthFunction,
47
+ ResolverContext,
48
+ ResolverFunction,
49
+ FieldOption,
50
+ } from "./config/types.js";
51
+
52
+ // Re-export Zod for convenience
53
+ export { z } from "zod";
@@ -0,0 +1,242 @@
1
+ import type {
2
+ OntologySnapshot,
3
+ OntologyDiff,
4
+ FunctionChange,
5
+ } from "./types.js";
6
+ import { hashOntology } from "./hasher.js";
7
+
8
+ /**
9
+ * Compare two ontology snapshots and generate a diff.
10
+ * @param oldOntology - The previous ontology (from lockfile), or null if first run
11
+ * @param newOntology - The current ontology (from config)
12
+ */
13
+ export function diffOntology(
14
+ oldOntology: OntologySnapshot | null,
15
+ newOntology: OntologySnapshot
16
+ ): OntologyDiff {
17
+ const newHash = hashOntology(newOntology);
18
+
19
+ // First run - no old ontology
20
+ if (!oldOntology) {
21
+ const functions: FunctionChange[] = Object.entries(
22
+ newOntology.functions
23
+ ).map(([name, fn]) => ({
24
+ name,
25
+ type: "added" as const,
26
+ newAccess: fn.access,
27
+ newDescription: fn.description,
28
+ newEntities: fn.entities,
29
+ }));
30
+
31
+ return {
32
+ hasChanges: true,
33
+ addedGroups: newOntology.accessGroups,
34
+ removedGroups: [],
35
+ addedEntities: newOntology.entities ?? [],
36
+ removedEntities: [],
37
+ functions,
38
+ newOntology,
39
+ newHash,
40
+ };
41
+ }
42
+
43
+ // Compare access groups
44
+ const oldGroupSet = new Set(oldOntology.accessGroups);
45
+ const newGroupSet = new Set(newOntology.accessGroups);
46
+
47
+ const addedGroups = newOntology.accessGroups.filter(
48
+ (g) => !oldGroupSet.has(g)
49
+ );
50
+ const removedGroups = oldOntology.accessGroups.filter(
51
+ (g) => !newGroupSet.has(g)
52
+ );
53
+
54
+ // Compare entities
55
+ const oldEntitySet = new Set(oldOntology.entities ?? []);
56
+ const newEntitySet = new Set(newOntology.entities ?? []);
57
+
58
+ const addedEntities = (newOntology.entities ?? []).filter(
59
+ (e) => !oldEntitySet.has(e)
60
+ );
61
+ const removedEntities = (oldOntology.entities ?? []).filter(
62
+ (e) => !newEntitySet.has(e)
63
+ );
64
+
65
+ // Compare functions
66
+ const functions: FunctionChange[] = [];
67
+ const oldFnNames = new Set(Object.keys(oldOntology.functions));
68
+ const newFnNames = new Set(Object.keys(newOntology.functions));
69
+
70
+ // Added functions
71
+ for (const name of newFnNames) {
72
+ if (!oldFnNames.has(name)) {
73
+ const fn = newOntology.functions[name];
74
+ functions.push({
75
+ name,
76
+ type: "added",
77
+ newAccess: fn.access,
78
+ newDescription: fn.description,
79
+ });
80
+ }
81
+ }
82
+
83
+ // Removed functions
84
+ for (const name of oldFnNames) {
85
+ if (!newFnNames.has(name)) {
86
+ const fn = oldOntology.functions[name];
87
+ functions.push({
88
+ name,
89
+ type: "removed",
90
+ oldAccess: fn.access,
91
+ oldDescription: fn.description,
92
+ });
93
+ }
94
+ }
95
+
96
+ // Modified functions
97
+ for (const name of newFnNames) {
98
+ if (oldFnNames.has(name)) {
99
+ const oldFn = oldOntology.functions[name];
100
+ const newFn = newOntology.functions[name];
101
+
102
+ // Check if anything changed
103
+ const accessChanged =
104
+ JSON.stringify(oldFn.access) !== JSON.stringify(newFn.access);
105
+ const descriptionChanged = oldFn.description !== newFn.description;
106
+ const inputsChanged =
107
+ JSON.stringify(oldFn.inputsSchema) !==
108
+ JSON.stringify(newFn.inputsSchema);
109
+ const outputsChanged =
110
+ JSON.stringify(oldFn.outputsSchema) !==
111
+ JSON.stringify(newFn.outputsSchema);
112
+ const entitiesChanged =
113
+ JSON.stringify(oldFn.entities) !== JSON.stringify(newFn.entities);
114
+ const fieldReferencesChanged =
115
+ JSON.stringify(oldFn.fieldReferences) !==
116
+ JSON.stringify(newFn.fieldReferences);
117
+
118
+ if (
119
+ accessChanged ||
120
+ descriptionChanged ||
121
+ inputsChanged ||
122
+ outputsChanged ||
123
+ entitiesChanged ||
124
+ fieldReferencesChanged
125
+ ) {
126
+ functions.push({
127
+ name,
128
+ type: "modified",
129
+ oldAccess: accessChanged ? oldFn.access : undefined,
130
+ newAccess: accessChanged ? newFn.access : undefined,
131
+ oldDescription: descriptionChanged ? oldFn.description : undefined,
132
+ newDescription: descriptionChanged ? newFn.description : undefined,
133
+ inputsChanged: inputsChanged || undefined,
134
+ outputsChanged: outputsChanged || undefined,
135
+ entitiesChanged: entitiesChanged || undefined,
136
+ oldEntities: entitiesChanged ? oldFn.entities : undefined,
137
+ newEntities: entitiesChanged ? newFn.entities : undefined,
138
+ fieldReferencesChanged: fieldReferencesChanged || undefined,
139
+ });
140
+ }
141
+ }
142
+ }
143
+
144
+ const hasChanges =
145
+ addedGroups.length > 0 ||
146
+ removedGroups.length > 0 ||
147
+ addedEntities.length > 0 ||
148
+ removedEntities.length > 0 ||
149
+ functions.length > 0;
150
+
151
+ return {
152
+ hasChanges,
153
+ addedGroups,
154
+ removedGroups,
155
+ addedEntities,
156
+ removedEntities,
157
+ functions,
158
+ newOntology,
159
+ newHash,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Format a diff for console output
165
+ */
166
+ export function formatDiffForConsole(diff: OntologyDiff): string {
167
+ if (!diff.hasChanges) {
168
+ return "No changes detected.";
169
+ }
170
+
171
+ const lines: string[] = ["Ontology changes detected:", ""];
172
+
173
+ if (diff.addedGroups.length > 0) {
174
+ lines.push("Added access groups:");
175
+ for (const group of diff.addedGroups) {
176
+ lines.push(` + ${group}`);
177
+ }
178
+ lines.push("");
179
+ }
180
+
181
+ if (diff.removedGroups.length > 0) {
182
+ lines.push("Removed access groups:");
183
+ for (const group of diff.removedGroups) {
184
+ lines.push(` - ${group}`);
185
+ }
186
+ lines.push("");
187
+ }
188
+
189
+ if (diff.addedEntities.length > 0) {
190
+ lines.push("Added entities:");
191
+ for (const entity of diff.addedEntities) {
192
+ lines.push(` + ${entity}`);
193
+ }
194
+ lines.push("");
195
+ }
196
+
197
+ if (diff.removedEntities.length > 0) {
198
+ lines.push("Removed entities:");
199
+ for (const entity of diff.removedEntities) {
200
+ lines.push(` - ${entity}`);
201
+ }
202
+ lines.push("");
203
+ }
204
+
205
+ if (diff.functions.length > 0) {
206
+ lines.push("Function changes:");
207
+ for (const fn of diff.functions) {
208
+ if (fn.type === "added") {
209
+ lines.push(` + ${fn.name}`);
210
+ lines.push(` Access: [${fn.newAccess?.join(", ")}]`);
211
+ if (fn.newEntities && fn.newEntities.length > 0) {
212
+ lines.push(` Entities: [${fn.newEntities.join(", ")}]`);
213
+ }
214
+ } else if (fn.type === "removed") {
215
+ lines.push(` - ${fn.name}`);
216
+ } else {
217
+ lines.push(` ~ ${fn.name}`);
218
+ if (fn.oldAccess && fn.newAccess) {
219
+ lines.push(
220
+ ` Access: [${fn.oldAccess.join(", ")}] -> [${fn.newAccess.join(", ")}]`
221
+ );
222
+ }
223
+ if (fn.oldEntities && fn.newEntities) {
224
+ lines.push(
225
+ ` Entities: [${fn.oldEntities.join(", ")}] -> [${fn.newEntities.join(", ")}]`
226
+ );
227
+ }
228
+ if (fn.inputsChanged) {
229
+ lines.push(` Inputs: schema changed`);
230
+ }
231
+ if (fn.outputsChanged) {
232
+ lines.push(` Outputs: schema changed`);
233
+ }
234
+ if (fn.fieldReferencesChanged) {
235
+ lines.push(` Field references: changed`);
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ return lines.join("\n");
242
+ }
@@ -0,0 +1,175 @@
1
+ import { createHash } from "crypto";
2
+ import { z } from "zod";
3
+ import { zodToJsonSchema } from "zod-to-json-schema";
4
+ import type { OntologyConfig } from "../config/types.js";
5
+ import { getFieldFromMetadata } from "../config/categorical.js";
6
+ import type {
7
+ OntologySnapshot,
8
+ FunctionShape,
9
+ FieldReference,
10
+ } from "./types.js";
11
+
12
+ /**
13
+ * Recursively extract fieldFrom references from a Zod schema
14
+ */
15
+ function extractFieldReferences(
16
+ schema: z.ZodType<unknown>,
17
+ path: string = ""
18
+ ): FieldReference[] {
19
+ const results: FieldReference[] = [];
20
+
21
+ // Check if this schema has fieldFrom metadata
22
+ const metadata = getFieldFromMetadata(schema);
23
+ if (metadata) {
24
+ results.push({
25
+ path: path || "(root)",
26
+ functionName: metadata.functionName,
27
+ });
28
+ }
29
+
30
+ // Handle ZodObject - recurse into properties
31
+ if (schema instanceof z.ZodObject) {
32
+ const shape = schema.shape;
33
+ for (const [key, value] of Object.entries(shape)) {
34
+ const fieldPath = path ? `${path}.${key}` : key;
35
+ results.push(
36
+ ...extractFieldReferences(value as z.ZodType<unknown>, fieldPath)
37
+ );
38
+ }
39
+ }
40
+
41
+ // Handle ZodOptional - unwrap
42
+ if (schema instanceof z.ZodOptional) {
43
+ results.push(...extractFieldReferences(schema.unwrap(), path));
44
+ }
45
+
46
+ // Handle ZodNullable - unwrap
47
+ if (schema instanceof z.ZodNullable) {
48
+ results.push(...extractFieldReferences(schema.unwrap(), path));
49
+ }
50
+
51
+ // Handle ZodArray - recurse into element
52
+ if (schema instanceof z.ZodArray) {
53
+ results.push(...extractFieldReferences(schema.element, `${path}[]`));
54
+ }
55
+
56
+ // Handle ZodDefault - unwrap
57
+ if (schema instanceof z.ZodDefault) {
58
+ results.push(...extractFieldReferences(schema._def.innerType, path));
59
+ }
60
+
61
+ return results;
62
+ }
63
+
64
+ /**
65
+ * Extract the ontology snapshot from an OntologyConfig.
66
+ * This extracts ONLY the security-relevant parts:
67
+ * - Function names
68
+ * - Access lists
69
+ * - Input schemas
70
+ * - Output schemas
71
+ * - Descriptions
72
+ * - Entities
73
+ * - Field references (fieldFrom)
74
+ *
75
+ * It DOES NOT include:
76
+ * - Resolver paths (so resolver code can change freely)
77
+ * - Environment configs
78
+ * - Auth function
79
+ */
80
+ export function extractOntology(config: OntologyConfig): OntologySnapshot {
81
+ const functions: Record<string, FunctionShape> = {};
82
+
83
+ for (const [name, fn] of Object.entries(config.functions)) {
84
+ // Convert Zod schema to JSON Schema for hashing
85
+ // This ensures we're comparing the shape, not the Zod instance
86
+ let inputsSchema: Record<string, unknown>;
87
+ try {
88
+ inputsSchema = zodToJsonSchema(fn.inputs, {
89
+ // Remove $schema to make hashing more stable
90
+ $refStrategy: "none",
91
+ }) as Record<string, unknown>;
92
+ // Remove $schema key if present
93
+ delete inputsSchema.$schema;
94
+ } catch {
95
+ // If zodToJsonSchema fails, use a placeholder
96
+ inputsSchema = { type: "unknown" };
97
+ }
98
+
99
+ // Convert outputs schema if present
100
+ let outputsSchema: Record<string, unknown> | undefined;
101
+ if (fn.outputs) {
102
+ try {
103
+ outputsSchema = zodToJsonSchema(fn.outputs, {
104
+ $refStrategy: "none",
105
+ }) as Record<string, unknown>;
106
+ delete outputsSchema.$schema;
107
+ } catch {
108
+ outputsSchema = { type: "unknown" };
109
+ }
110
+ }
111
+
112
+ // Extract field references
113
+ const fieldReferences = extractFieldReferences(fn.inputs);
114
+
115
+ functions[name] = {
116
+ description: fn.description,
117
+ // Sort access groups for consistent hashing
118
+ access: [...fn.access].sort(),
119
+ // Sort entities for consistent hashing
120
+ entities: [...fn.entities].sort(),
121
+ inputsSchema,
122
+ outputsSchema,
123
+ fieldReferences:
124
+ fieldReferences.length > 0 ? fieldReferences : undefined,
125
+ };
126
+ }
127
+
128
+ return {
129
+ name: config.name,
130
+ // Sort access groups for consistent hashing
131
+ accessGroups: Object.keys(config.accessGroups).sort(),
132
+ // Sort entities for consistent hashing
133
+ entities: config.entities
134
+ ? Object.keys(config.entities).sort()
135
+ : undefined,
136
+ functions,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Create a deterministic hash of an ontology snapshot.
142
+ * Uses SHA256 and returns a 16-character hex string.
143
+ */
144
+ export function hashOntology(ontology: OntologySnapshot): string {
145
+ // Sort all keys at all levels for deterministic hashing
146
+ const normalized = JSON.stringify(ontology, (_, value) => {
147
+ if (value && typeof value === "object" && !Array.isArray(value)) {
148
+ // Sort object keys
149
+ return Object.keys(value)
150
+ .sort()
151
+ .reduce(
152
+ (sorted, key) => {
153
+ sorted[key] = value[key];
154
+ return sorted;
155
+ },
156
+ {} as Record<string, unknown>
157
+ );
158
+ }
159
+ return value;
160
+ });
161
+
162
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
163
+ }
164
+
165
+ /**
166
+ * Extract ontology snapshot and compute hash in one step
167
+ */
168
+ export function computeOntologyHash(config: OntologyConfig): {
169
+ ontology: OntologySnapshot;
170
+ hash: string;
171
+ } {
172
+ const ontology = extractOntology(config);
173
+ const hash = hashOntology(ontology);
174
+ return { ontology, hash };
175
+ }