@m6d/cortex-server 1.0.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 (82) hide show
  1. package/README.md +64 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/src/adapters/database.d.ts +27 -0
  4. package/dist/src/adapters/minio.d.ts +10 -0
  5. package/dist/src/adapters/mssql.d.ts +3 -0
  6. package/dist/src/adapters/storage.d.ts +6 -0
  7. package/dist/src/ai/fetch.d.ts +2 -0
  8. package/dist/src/ai/helpers.d.ts +5 -0
  9. package/dist/src/ai/index.d.ts +4 -0
  10. package/dist/src/ai/interceptors/resolve-captured-files.d.ts +11 -0
  11. package/dist/src/ai/prompt.d.ts +4 -0
  12. package/dist/src/ai/tools/call-endpoint.tool.d.ts +7 -0
  13. package/dist/src/ai/tools/capture-files.tool.d.ts +6 -0
  14. package/dist/src/ai/tools/execute-code.tool.d.ts +4 -0
  15. package/dist/src/ai/tools/query-graph.tool.d.ts +5 -0
  16. package/dist/src/auth/middleware.d.ts +4 -0
  17. package/dist/src/cli/extract-endpoints.d.ts +6 -0
  18. package/dist/src/config.d.ts +145 -0
  19. package/dist/src/db/migrate.d.ts +1 -0
  20. package/dist/src/db/schema.d.ts +345 -0
  21. package/dist/src/factory.d.ts +17 -0
  22. package/dist/src/graph/generate-cypher.d.ts +22 -0
  23. package/dist/src/graph/helpers.d.ts +60 -0
  24. package/dist/src/graph/index.d.ts +11 -0
  25. package/dist/src/graph/neo4j.d.ts +18 -0
  26. package/dist/src/graph/resolver.d.ts +51 -0
  27. package/dist/src/graph/seed.d.ts +19 -0
  28. package/dist/src/graph/types.d.ts +104 -0
  29. package/dist/src/graph/validate.d.ts +2 -0
  30. package/dist/src/index.d.ts +10 -0
  31. package/dist/src/routes/chat.d.ts +3 -0
  32. package/dist/src/routes/files.d.ts +3 -0
  33. package/dist/src/routes/index.d.ts +4 -0
  34. package/dist/src/routes/threads.d.ts +3 -0
  35. package/dist/src/routes/ws.d.ts +3 -0
  36. package/dist/src/types.d.ts +56 -0
  37. package/dist/src/ws/connections.d.ts +4 -0
  38. package/dist/src/ws/events.d.ts +8 -0
  39. package/dist/src/ws/index.d.ts +3 -0
  40. package/dist/src/ws/notify.d.ts +2 -0
  41. package/index.ts +1 -0
  42. package/package.json +57 -0
  43. package/src/adapters/database.ts +33 -0
  44. package/src/adapters/minio.ts +89 -0
  45. package/src/adapters/mssql.ts +203 -0
  46. package/src/adapters/storage.ts +6 -0
  47. package/src/ai/fetch.ts +39 -0
  48. package/src/ai/helpers.ts +36 -0
  49. package/src/ai/index.ts +145 -0
  50. package/src/ai/interceptors/resolve-captured-files.ts +64 -0
  51. package/src/ai/prompt.ts +120 -0
  52. package/src/ai/tools/call-endpoint.tool.ts +96 -0
  53. package/src/ai/tools/capture-files.tool.ts +22 -0
  54. package/src/ai/tools/execute-code.tool.ts +108 -0
  55. package/src/ai/tools/query-graph.tool.ts +35 -0
  56. package/src/auth/middleware.ts +63 -0
  57. package/src/cli/extract-endpoints.ts +588 -0
  58. package/src/config.ts +155 -0
  59. package/src/db/migrate.ts +21 -0
  60. package/src/db/migrations/20260309012148_cloudy_maria_hill/migration.sql +36 -0
  61. package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +305 -0
  62. package/src/db/schema.ts +77 -0
  63. package/src/factory.ts +159 -0
  64. package/src/graph/generate-cypher.ts +179 -0
  65. package/src/graph/helpers.ts +68 -0
  66. package/src/graph/index.ts +47 -0
  67. package/src/graph/neo4j.ts +117 -0
  68. package/src/graph/resolver.ts +357 -0
  69. package/src/graph/seed.ts +172 -0
  70. package/src/graph/types.ts +152 -0
  71. package/src/graph/validate.ts +80 -0
  72. package/src/index.ts +27 -0
  73. package/src/routes/chat.ts +38 -0
  74. package/src/routes/files.ts +105 -0
  75. package/src/routes/index.ts +4 -0
  76. package/src/routes/threads.ts +69 -0
  77. package/src/routes/ws.ts +33 -0
  78. package/src/types.ts +50 -0
  79. package/src/ws/connections.ts +23 -0
  80. package/src/ws/events.ts +6 -0
  81. package/src/ws/index.ts +7 -0
  82. package/src/ws/notify.ts +9 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Pre-resolves relevant API endpoints from the Neo4j knowledge graph
3
+ * using vector similarity search on concept descriptions.
4
+ *
5
+ * All configuration is passed explicitly — no hardcoded model references
6
+ * or environment variables.
7
+ */
8
+
9
+ import type { EmbeddingModel } from "ai";
10
+ import { embed } from "ai";
11
+ import type { Neo4jClient } from "./neo4j.ts";
12
+ import type { ConceptDef } from "./types.ts";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Config
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export type RerankerConfig = {
19
+ url: string;
20
+ apiKey: string;
21
+ };
22
+
23
+ export type ResolverConfig = {
24
+ neo4j: Neo4jClient;
25
+ embeddingModel: EmbeddingModel;
26
+ reranker?: RerankerConfig;
27
+ };
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Result types
31
+ // ---------------------------------------------------------------------------
32
+
33
+ type EndpointDependency = {
34
+ depPath: string;
35
+ depMethod: string;
36
+ paramName: string;
37
+ fromField: string;
38
+ };
39
+
40
+ type EndpointRelation = "read" | "write";
41
+
42
+ export type ResolvedEndpoint = {
43
+ concept: string;
44
+ relation: EndpointRelation;
45
+ name: string;
46
+ path: string;
47
+ method: string;
48
+ params: string;
49
+ body: string;
50
+ response: string;
51
+ dependencies: EndpointDependency[];
52
+ rules: string[];
53
+ };
54
+
55
+ export type ResolvedService = {
56
+ concept: string;
57
+ serviceName: string;
58
+ builtInId: string;
59
+ description: string;
60
+ rules: string[];
61
+ };
62
+
63
+ export type ResolvedContext = {
64
+ readEndpoints: ResolvedEndpoint[];
65
+ writeEndpoints: ResolvedEndpoint[];
66
+ services: ResolvedService[];
67
+ };
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Internal row types
71
+ // ---------------------------------------------------------------------------
72
+
73
+ type EndpointRow = {
74
+ concept: string;
75
+ relationType: "QUERIED_VIA" | "MUTATED_VIA";
76
+ name: string;
77
+ path: string;
78
+ method: string;
79
+ params: string | null;
80
+ body: string | null;
81
+ response: string | null;
82
+ dependencies: (EndpointDependency | { depPath: null })[];
83
+ rules: (string | null)[];
84
+ };
85
+
86
+ type ServiceRow = {
87
+ concept: string;
88
+ serviceName: string;
89
+ builtInId: string;
90
+ description: string | null;
91
+ rules: (string | null)[];
92
+ };
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Public API
96
+ // ---------------------------------------------------------------------------
97
+
98
+ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
99
+ const empty: ResolvedContext = {
100
+ readEndpoints: [],
101
+ writeEndpoints: [],
102
+ services: [],
103
+ };
104
+
105
+ try {
106
+ const { embedding } = await embed({
107
+ model: config.embeddingModel,
108
+ value: prompt,
109
+ });
110
+
111
+ const conceptsRaw = await config.neo4j.query(
112
+ `CALL db.index.vector.queryNodes('concept_embeddings', 10, $embedding)
113
+ YIELD node, score
114
+ WHERE score > 0.3
115
+ RETURN node.name AS name, node.description AS description, score
116
+ ORDER BY score DESC`,
117
+ { embedding },
118
+ );
119
+
120
+ let concepts = JSON.parse(conceptsRaw) as (Pick<
121
+ ConceptDef,
122
+ "name" | "description"
123
+ > & { score: number })[];
124
+
125
+ if (!Array.isArray(concepts) || concepts.length === 0) {
126
+ return empty;
127
+ }
128
+
129
+ if (config.reranker) {
130
+ concepts = await computeRelevanceScores(
131
+ prompt,
132
+ concepts,
133
+ config.reranker,
134
+ );
135
+ } else {
136
+ concepts = concepts.slice(0, 3);
137
+ }
138
+
139
+ const conceptNames = concepts.map((c) => c.name);
140
+
141
+ const endpointsRaw = await config.neo4j.query(
142
+ `UNWIND $names AS conceptName
143
+ MATCH (c:Concept {name: conceptName})-[:SPECIALIZES*0..3]->(ancestor:Concept)
144
+ WITH conceptName, collect(DISTINCT ancestor) AS family
145
+ UNWIND family AS related
146
+ MATCH (related)-[rel:QUERIED_VIA|MUTATED_VIA]->(e:Endpoint)
147
+ WITH conceptName, related, rel, e
148
+ OPTIONAL MATCH (e)-[d:DEPENDS_ON]->(dep:Endpoint)
149
+ WITH conceptName, related, rel, e,
150
+ collect(DISTINCT {
151
+ depPath: dep.path,
152
+ depMethod: dep.method,
153
+ paramName: d.paramName,
154
+ fromField: d.fromField
155
+ }) AS dependencies
156
+ OPTIONAL MATCH (rEndpoint:Rule)-[:GOVERNS]->(e)
157
+ OPTIONAL MATCH (rConcept:Rule)-[:GOVERNS]->(related)
158
+ RETURN conceptName AS concept,
159
+ type(rel) AS relationType,
160
+ e.name AS name,
161
+ e.path AS path,
162
+ e.method AS method,
163
+ e.params AS params,
164
+ e.body AS body,
165
+ e.response AS response,
166
+ dependencies,
167
+ collect(DISTINCT rEndpoint.description) +
168
+ collect(DISTINCT rConcept.description) AS rules`,
169
+ { names: conceptNames },
170
+ );
171
+
172
+ const endpointRows = asArray<EndpointRow>(JSON.parse(endpointsRaw));
173
+ const allEndpoints = dedupeEndpoints(
174
+ endpointRows.map(function (row) {
175
+ return {
176
+ concept: row.concept,
177
+ relation:
178
+ row.relationType === "MUTATED_VIA"
179
+ ? ("write" as const)
180
+ : ("read" as const),
181
+ name: row.name,
182
+ path: row.path,
183
+ method: row.method,
184
+ params: row.params ?? "[]",
185
+ body: row.body ?? "[]",
186
+ response: row.response ?? "[]",
187
+ dependencies: row.dependencies.filter(
188
+ (dep): dep is EndpointDependency => dep.depPath != null,
189
+ ),
190
+ rules: row.rules.filter((rule): rule is string => Boolean(rule)),
191
+ };
192
+ }),
193
+ );
194
+
195
+ const servicesRaw = await config.neo4j.query(
196
+ `UNWIND $names AS conceptName
197
+ MATCH (c:Concept {name: conceptName})-[:SPECIALIZES*0..3]->(ancestor:Concept)
198
+ WITH conceptName, collect(DISTINCT ancestor) AS family
199
+ UNWIND family AS related
200
+ MATCH (related)-[:REQUESTED_VIA]->(svc:Service)
201
+ WITH conceptName, related, svc
202
+ OPTIONAL MATCH (rService:Rule)-[:GOVERNS]->(svc)
203
+ OPTIONAL MATCH (rConcept:Rule)-[:GOVERNS]->(related)
204
+ RETURN conceptName AS concept,
205
+ svc.name AS serviceName,
206
+ svc.builtInId AS builtInId,
207
+ svc.description AS description,
208
+ collect(DISTINCT rService.description) +
209
+ collect(DISTINCT rConcept.description) AS rules`,
210
+ { names: conceptNames },
211
+ );
212
+
213
+ const services = dedupeServices(
214
+ asArray<ServiceRow>(JSON.parse(servicesRaw)).map(function (row) {
215
+ return {
216
+ concept: row.concept,
217
+ serviceName: row.serviceName,
218
+ builtInId: row.builtInId,
219
+ description: row.description ?? "",
220
+ rules: row.rules.filter((rule): rule is string => Boolean(rule)),
221
+ };
222
+ }),
223
+ );
224
+
225
+ // Service-to-Servicing bridge: if any services were resolved, pull in
226
+ // the generic Servicing endpoints
227
+ let servicingEndpoints: ResolvedEndpoint[] = [];
228
+ if (services.length > 0) {
229
+ const servicingRaw = await config.neo4j.query(
230
+ `MATCH (c:Concept {name: 'ServiceRequest'})-[rel:QUERIED_VIA|MUTATED_VIA]->(e:Endpoint)
231
+ OPTIONAL MATCH (e)-[d:DEPENDS_ON]->(dep:Endpoint)
232
+ WITH c, rel, e,
233
+ collect(DISTINCT {
234
+ depPath: dep.path,
235
+ depMethod: dep.method,
236
+ paramName: d.paramName,
237
+ fromField: d.fromField
238
+ }) AS dependencies
239
+ OPTIONAL MATCH (rEndpoint:Rule)-[:GOVERNS]->(e)
240
+ OPTIONAL MATCH (rConcept:Rule)-[:GOVERNS]->(c)
241
+ RETURN 'ServiceRequest' AS concept,
242
+ type(rel) AS relationType,
243
+ e.name AS name,
244
+ e.path AS path,
245
+ e.method AS method,
246
+ e.params AS params,
247
+ e.body AS body,
248
+ e.response AS response,
249
+ dependencies,
250
+ collect(DISTINCT rEndpoint.description) +
251
+ collect(DISTINCT rConcept.description) AS rules`,
252
+ {},
253
+ );
254
+
255
+ servicingEndpoints = dedupeEndpoints(
256
+ asArray<EndpointRow>(JSON.parse(servicingRaw)).map(function (row) {
257
+ return {
258
+ concept: row.concept,
259
+ relation:
260
+ row.relationType === "MUTATED_VIA"
261
+ ? ("write" as const)
262
+ : ("read" as const),
263
+ name: row.name,
264
+ path: row.path,
265
+ method: row.method,
266
+ params: row.params ?? "[]",
267
+ body: row.body ?? "[]",
268
+ response: row.response ?? "[]",
269
+ dependencies: row.dependencies.filter(
270
+ (dep): dep is EndpointDependency => dep.depPath != null,
271
+ ),
272
+ rules: row.rules.filter((rule): rule is string => Boolean(rule)),
273
+ };
274
+ }),
275
+ );
276
+ }
277
+
278
+ const combinedEndpoints = dedupeEndpoints([
279
+ ...allEndpoints,
280
+ ...servicingEndpoints,
281
+ ]);
282
+
283
+ return {
284
+ readEndpoints: combinedEndpoints.filter((ep) => ep.relation === "read"),
285
+ writeEndpoints: combinedEndpoints.filter((ep) => ep.relation === "write"),
286
+ services,
287
+ } satisfies ResolvedContext;
288
+ } catch (error) {
289
+ console.error("Graph resolution failed:", error);
290
+ return empty;
291
+ }
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Internal helpers
296
+ // ---------------------------------------------------------------------------
297
+
298
+ function asArray<T>(value: unknown) {
299
+ return Array.isArray(value) ? (value as T[]) : ([] as T[]);
300
+ }
301
+
302
+ function dedupeEndpoints(endpoints: ResolvedEndpoint[]) {
303
+ const seen = new Set<string>();
304
+ const deduped: ResolvedEndpoint[] = [];
305
+
306
+ for (const ep of endpoints) {
307
+ const key = `${ep.relation}|${ep.concept}|${ep.method}|${ep.path}`;
308
+ if (seen.has(key)) continue;
309
+ seen.add(key);
310
+ deduped.push(ep);
311
+ }
312
+
313
+ return deduped;
314
+ }
315
+
316
+ function dedupeServices(services: ResolvedService[]) {
317
+ const seen = new Set<string>();
318
+ const deduped: ResolvedService[] = [];
319
+
320
+ for (const svc of services) {
321
+ const key = `${svc.concept}|${svc.builtInId}`;
322
+ if (seen.has(key)) continue;
323
+ seen.add(key);
324
+ deduped.push(svc);
325
+ }
326
+
327
+ return deduped;
328
+ }
329
+
330
+ async function computeRelevanceScores(
331
+ prompt: string,
332
+ concepts: (Pick<ConceptDef, "name" | "description"> & { score: number })[],
333
+ reranker: RerankerConfig,
334
+ ) {
335
+ const response = await fetch(reranker.url, {
336
+ method: "POST",
337
+ headers: {
338
+ Authorization: `Bearer ${reranker.apiKey}`,
339
+ "Content-Type": "application/json",
340
+ },
341
+ body: JSON.stringify({
342
+ queries: [prompt],
343
+ documents: concepts.map((x) => x.description),
344
+ }),
345
+ });
346
+
347
+ const data = (await response.json()) as { scores: number[] };
348
+ const scores = data.scores;
349
+
350
+ const scoredConcepts = concepts.map((c, idx) => ({
351
+ ...c,
352
+ score: scores[idx]!,
353
+ }));
354
+
355
+ scoredConcepts.sort((a, b) => b.score - a.score);
356
+ return scoredConcepts.slice(0, 3);
357
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Seed orchestrator for the knowledge graph.
3
+ *
4
+ * Validates domains, generates Cypher, executes against Neo4j,
5
+ * and generates concept embeddings. All configuration is passed
6
+ * explicitly — no environment variables or Convex dependencies.
7
+ */
8
+
9
+ import type { EmbeddingModel } from "ai";
10
+ import { embed } from "ai";
11
+ import type { Neo4jClient, Neo4jConfig } from "./neo4j.ts";
12
+ import { createNeo4jClient } from "./neo4j.ts";
13
+ import { generateCypher, type GeneratedCypher } from "./generate-cypher.ts";
14
+ import type { DomainDef } from "./types.ts";
15
+ import { validateDomain } from "./validate.ts";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Config
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export type EmbeddingConfig = {
22
+ model: EmbeddingModel;
23
+ dimension: number;
24
+ };
25
+
26
+ export type SeedGraphConfig = {
27
+ neo4j: Neo4jConfig;
28
+ embedding: EmbeddingConfig;
29
+ };
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Internal helpers
33
+ // ---------------------------------------------------------------------------
34
+
35
+ async function runStatements(
36
+ client: Neo4jClient,
37
+ label: string,
38
+ statements: string[],
39
+ ) {
40
+ console.log(`\n--- ${label} (${statements.length} statements) ---`);
41
+
42
+ let success = 0;
43
+ let failed = 0;
44
+
45
+ for (const stmt of statements) {
46
+ try {
47
+ const result = await client.query(stmt);
48
+ const parsed = JSON.parse(result);
49
+ if (parsed.error) {
50
+ console.error(` ✗ Failed: ${parsed.message}`);
51
+ console.error(` Statement: ${stmt.substring(0, 120)}...`);
52
+ failed++;
53
+ } else {
54
+ success++;
55
+ }
56
+ } catch (e) {
57
+ console.error(` ✗ Exception: ${e}`);
58
+ console.error(` Statement: ${stmt.substring(0, 120)}...`);
59
+ failed++;
60
+ }
61
+ }
62
+
63
+ console.log(` ✓ ${success} succeeded, ${failed} failed`);
64
+ }
65
+
66
+ async function generateEmbeddings(
67
+ client: Neo4jClient,
68
+ embeddingConfig: EmbeddingConfig,
69
+ ) {
70
+ console.log("Seeding concept embeddings...");
71
+
72
+ // 1. Create the vector index (idempotent)
73
+ await client.query(
74
+ `CREATE VECTOR INDEX concept_embeddings IF NOT EXISTS
75
+ FOR (c:Concept) ON (c.embedding)
76
+ OPTIONS {indexConfig: {
77
+ \`vector.dimensions\`: ${embeddingConfig.dimension},
78
+ \`vector.similarity_function\`: 'cosine'
79
+ }}`,
80
+ );
81
+ console.log("Vector index created (or already exists)");
82
+
83
+ // 2. Fetch all concepts
84
+ const conceptsRaw = await client.query(
85
+ "MATCH (c:Concept) RETURN c.name AS name, c.description AS description",
86
+ );
87
+ const concepts: { name: string; description: string }[] =
88
+ JSON.parse(conceptsRaw);
89
+
90
+ if (!Array.isArray(concepts) || concepts.length === 0) {
91
+ console.log("No concepts found in Neo4j");
92
+ return;
93
+ }
94
+
95
+ console.log(`Found ${concepts.length} concepts, generating embeddings...`);
96
+
97
+ // 3. Generate and store embeddings for each concept
98
+ const embeddingResults = await Promise.all(
99
+ concepts.map(async (concept) => ({
100
+ concept,
101
+ embedding: (
102
+ await embed({
103
+ model: embeddingConfig.model,
104
+ value: concept.description,
105
+ })
106
+ ).embedding,
107
+ })),
108
+ );
109
+
110
+ // 4. Store the embeddings in the graph
111
+ await Promise.all(
112
+ embeddingResults.map(
113
+ async (result) =>
114
+ await client.query(
115
+ `MATCH (c:Concept {name: $name})
116
+ SET c.embedding = $embedding`,
117
+ {
118
+ name: result.concept.name,
119
+ embedding: result.embedding,
120
+ },
121
+ ),
122
+ ),
123
+ );
124
+
125
+ console.log("Embedding seeding complete!");
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Public API
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export async function seedGraph(
133
+ config: SeedGraphConfig,
134
+ domains: Record<string, DomainDef>,
135
+ ) {
136
+ // Validate all domains
137
+ const validationErrors = Object.entries(domains).flatMap(([name, domain]) =>
138
+ validateDomain(domain).map((err) => `[${name}] ${err}`),
139
+ );
140
+
141
+ if (validationErrors.length) {
142
+ validationErrors.forEach((err) => console.error(err));
143
+ throw new Error(
144
+ `Validation failed with ${validationErrors.length} error(s)`,
145
+ );
146
+ }
147
+
148
+ const client = createNeo4jClient(config.neo4j, config.embedding.model);
149
+ const targets = Object.entries(domains);
150
+
151
+ console.log(`Seeding ${targets.length} domain(s)...`);
152
+
153
+ // Generate Cypher for all target domains
154
+ const allCypher: [string, GeneratedCypher][] = targets.map(([name, data]) => [
155
+ name,
156
+ generateCypher(data),
157
+ ]);
158
+
159
+ // Phase 1: Create all nodes across every domain first.
160
+ // This ensures cross-domain references resolve correctly.
161
+ const allNodes = allCypher.flatMap(([, c]) => c.nodes);
162
+ await runStatements(client, "Phase 1: Nodes", allNodes);
163
+
164
+ // Phase 2: Create all edges now that every node exists.
165
+ const allEdges = allCypher.flatMap(([, c]) => c.edges);
166
+ await runStatements(client, "Phase 2: Edges", allEdges);
167
+
168
+ console.log("\nGraph seeding complete!");
169
+
170
+ // Phase 3: Generate embeddings for concepts
171
+ await generateEmbeddings(client, config.embedding);
172
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Declarative types for knowledge graph seed data.
3
+ *
4
+ * This is the v2 schema:
5
+ * - Endpoints use params/body/response EndpointProperty arrays.
6
+ * - Source/default mappings are intentionally removed.
7
+ * - Services are concept associations (no hardcoded endpoint steps).
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Endpoint property model (v2)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export type EndpointScalarType =
15
+ | "uuid"
16
+ | "number"
17
+ | "date"
18
+ | "datetime"
19
+ | "string"
20
+ | "boolean"
21
+ | "any"
22
+ | "object";
23
+
24
+ export type EndpointProperty = {
25
+ readonly name: string;
26
+ readonly required: boolean;
27
+ readonly type: EndpointScalarType | string;
28
+ readonly isArray?: boolean;
29
+ readonly properties?: readonly EndpointProperty[];
30
+ readonly description?: string;
31
+ };
32
+
33
+ export type ResponseKind = "object" | "array" | "paginated" | "file" | "none";
34
+
35
+ export type AutoGenerated = {
36
+ readonly params: readonly EndpointProperty[];
37
+ readonly body: readonly EndpointProperty[];
38
+ readonly response: readonly EndpointProperty[];
39
+ readonly successStatus: number;
40
+ readonly errorStatuses: readonly number[];
41
+ readonly responseKind?: ResponseKind;
42
+ };
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Concept descriptor
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export type ConceptDef = {
49
+ readonly __brand: "concept";
50
+ name: string;
51
+ description: string;
52
+ aliases?: string[];
53
+ parentConcept?: ConceptDef;
54
+ governedBy?: RuleDef[];
55
+ };
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Endpoint descriptor (v2)
59
+ // ---------------------------------------------------------------------------
60
+
61
+ export type EndpointDef = {
62
+ readonly __brand: "endpoint";
63
+ name: string;
64
+ description: string;
65
+ path: string;
66
+ method: "GET" | "POST" | "PUT" | "DELETE";
67
+ propertiesDescriptions: Record<string, string>;
68
+
69
+ params: EndpointProperty[];
70
+ body: EndpointProperty[];
71
+ response: EndpointProperty[];
72
+ successStatus: number;
73
+ errorStatuses: number[];
74
+ responseKind: ResponseKind;
75
+
76
+ queries?: ConceptDef[];
77
+ mutates?: ConceptDef[];
78
+
79
+ returns?: { concept: ConceptDef; field?: string }[];
80
+ dependsOn?: {
81
+ endpoint: EndpointDef;
82
+ paramName: string;
83
+ fromField: string;
84
+ }[];
85
+
86
+ governedBy?: RuleDef[];
87
+ };
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // EndpointInput
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export type EndpointInput = {
94
+ name: string;
95
+ path: string;
96
+ method: "GET" | "POST" | "PUT" | "DELETE";
97
+ description?: string;
98
+ autoGenerated?: AutoGenerated;
99
+
100
+ propertiesDescriptions?: Record<string, string>;
101
+
102
+ queries?: ConceptDef[];
103
+ mutates?: ConceptDef[];
104
+ paramDescriptions?: Partial<Record<string, string>>;
105
+ responseDescriptions?: Partial<Record<string, string>>;
106
+
107
+ returns?: { concept: ConceptDef; field?: string }[];
108
+ dependsOn?: {
109
+ endpoint: EndpointDef;
110
+ paramName: string;
111
+ fromField: string;
112
+ }[];
113
+
114
+ governedBy?: RuleDef[];
115
+ };
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Service descriptor (v2)
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export type ServiceDef = {
122
+ readonly __brand: "service";
123
+ name: string;
124
+ description: string;
125
+ builtInId: string;
126
+ belongsTo: ConceptDef;
127
+ governedBy?: RuleDef[];
128
+ };
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Rule descriptor
132
+ // ---------------------------------------------------------------------------
133
+
134
+ export type RuleDef = {
135
+ readonly __brand: "rule";
136
+ name: string;
137
+ description: string;
138
+ };
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Domain descriptor
142
+ // ---------------------------------------------------------------------------
143
+
144
+ export type DomainDef = {
145
+ readonly __brand: "domain";
146
+ name: string;
147
+ description: string;
148
+ concepts?: ConceptDef[];
149
+ endpoints?: EndpointDef[];
150
+ services?: ServiceDef[];
151
+ rules?: RuleDef[];
152
+ };