@m6d/cortex-server 1.1.0 → 1.1.2
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/README.md +38 -38
- package/dist/src/factory.d.ts +13 -1
- package/dist/src/ws/index.d.ts +1 -1
- package/package.json +54 -54
- package/src/adapters/database.ts +21 -28
- package/src/adapters/minio.ts +69 -69
- package/src/adapters/mssql.ts +171 -195
- package/src/adapters/storage.ts +4 -4
- package/src/ai/fetch.ts +31 -31
- package/src/ai/helpers.ts +18 -22
- package/src/ai/index.ts +101 -113
- package/src/ai/interceptors/resolve-captured-files.ts +42 -49
- package/src/ai/prompt.ts +80 -83
- package/src/ai/tools/call-endpoint.tool.ts +75 -82
- package/src/ai/tools/capture-files.tool.ts +15 -17
- package/src/ai/tools/execute-code.tool.ts +73 -80
- package/src/ai/tools/query-graph.tool.ts +17 -17
- package/src/auth/middleware.ts +51 -51
- package/src/cli/extract-endpoints.ts +436 -474
- package/src/config.ts +124 -134
- package/src/db/migrate.ts +13 -13
- package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +303 -303
- package/src/db/schema.ts +46 -58
- package/src/factory.ts +136 -139
- package/src/graph/generate-cypher.ts +97 -97
- package/src/graph/helpers.ts +37 -37
- package/src/graph/index.ts +20 -20
- package/src/graph/neo4j.ts +82 -89
- package/src/graph/resolver.ts +201 -211
- package/src/graph/seed.ts +101 -114
- package/src/graph/types.ts +88 -88
- package/src/graph/validate.ts +55 -57
- package/src/index.ts +5 -5
- package/src/routes/chat.ts +23 -23
- package/src/routes/files.ts +75 -80
- package/src/routes/threads.ts +52 -54
- package/src/routes/ws.ts +22 -22
- package/src/types.ts +30 -30
- package/src/ws/connections.ts +11 -11
- package/src/ws/events.ts +2 -2
- package/src/ws/index.ts +1 -5
- package/src/ws/notify.ts +4 -4
package/src/graph/seed.ts
CHANGED
|
@@ -19,154 +19,141 @@ import { validateDomain } from "./validate.ts";
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
|
|
21
21
|
export type EmbeddingConfig = {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
model: EmbeddingModel;
|
|
23
|
+
dimension: number;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export type SeedGraphConfig = {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
neo4j: Neo4jConfig;
|
|
28
|
+
embedding: EmbeddingConfig;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
// Internal helpers
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
async function runStatements(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.error(` ✗ Exception: ${e}`);
|
|
58
|
-
console.error(` Statement: ${stmt.substring(0, 120)}...`);
|
|
59
|
-
failed++;
|
|
35
|
+
async function runStatements(client: Neo4jClient, label: string, statements: string[]) {
|
|
36
|
+
console.log(`\n--- ${label} (${statements.length} statements) ---`);
|
|
37
|
+
|
|
38
|
+
let success = 0;
|
|
39
|
+
let failed = 0;
|
|
40
|
+
|
|
41
|
+
for (const stmt of statements) {
|
|
42
|
+
try {
|
|
43
|
+
const result = await client.query(stmt);
|
|
44
|
+
const parsed = JSON.parse(result);
|
|
45
|
+
if (parsed.error) {
|
|
46
|
+
console.error(` ✗ Failed: ${parsed.message}`);
|
|
47
|
+
console.error(` Statement: ${stmt.substring(0, 120)}...`);
|
|
48
|
+
failed++;
|
|
49
|
+
} else {
|
|
50
|
+
success++;
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(` ✗ Exception: ${e}`);
|
|
54
|
+
console.error(` Statement: ${stmt.substring(0, 120)}...`);
|
|
55
|
+
failed++;
|
|
56
|
+
}
|
|
60
57
|
}
|
|
61
|
-
}
|
|
62
58
|
|
|
63
|
-
|
|
59
|
+
console.log(` ✓ ${success} succeeded, ${failed} failed`);
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
async function generateEmbeddings(
|
|
67
|
-
|
|
68
|
-
embeddingConfig: EmbeddingConfig,
|
|
69
|
-
) {
|
|
70
|
-
console.log("Seeding concept embeddings...");
|
|
62
|
+
async function generateEmbeddings(client: Neo4jClient, embeddingConfig: EmbeddingConfig) {
|
|
63
|
+
console.log("Seeding concept embeddings...");
|
|
71
64
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
// 1. Create the vector index (idempotent)
|
|
66
|
+
await client.query(
|
|
67
|
+
`CREATE VECTOR INDEX concept_embeddings IF NOT EXISTS
|
|
75
68
|
FOR (c:Concept) ON (c.embedding)
|
|
76
69
|
OPTIONS {indexConfig: {
|
|
77
70
|
\`vector.dimensions\`: ${embeddingConfig.dimension},
|
|
78
71
|
\`vector.similarity_function\`: 'cosine'
|
|
79
72
|
}}`,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
`MATCH (c:Concept {name: $name})
|
|
73
|
+
);
|
|
74
|
+
console.log("Vector index created (or already exists)");
|
|
75
|
+
|
|
76
|
+
// 2. Fetch all concepts
|
|
77
|
+
const conceptsRaw = await client.query(
|
|
78
|
+
"MATCH (c:Concept) RETURN c.name AS name, c.description AS description",
|
|
79
|
+
);
|
|
80
|
+
const concepts: { name: string; description: string }[] = JSON.parse(conceptsRaw);
|
|
81
|
+
|
|
82
|
+
if (!Array.isArray(concepts) || concepts.length === 0) {
|
|
83
|
+
console.log("No concepts found in Neo4j");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`Found ${concepts.length} concepts, generating embeddings...`);
|
|
88
|
+
|
|
89
|
+
// 3. Generate and store embeddings for each concept
|
|
90
|
+
const embeddingResults = await Promise.all(
|
|
91
|
+
concepts.map(async (concept) => ({
|
|
92
|
+
concept,
|
|
93
|
+
embedding: (
|
|
94
|
+
await embed({
|
|
95
|
+
model: embeddingConfig.model,
|
|
96
|
+
value: concept.description,
|
|
97
|
+
})
|
|
98
|
+
).embedding,
|
|
99
|
+
})),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// 4. Store the embeddings in the graph
|
|
103
|
+
await Promise.all(
|
|
104
|
+
embeddingResults.map(
|
|
105
|
+
async (result) =>
|
|
106
|
+
await client.query(
|
|
107
|
+
`MATCH (c:Concept {name: $name})
|
|
116
108
|
SET c.embedding = $embedding`,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
{
|
|
110
|
+
name: result.concept.name,
|
|
111
|
+
embedding: result.embedding,
|
|
112
|
+
},
|
|
113
|
+
),
|
|
121
114
|
),
|
|
122
|
-
)
|
|
123
|
-
);
|
|
115
|
+
);
|
|
124
116
|
|
|
125
|
-
|
|
117
|
+
console.log("Embedding seeding complete!");
|
|
126
118
|
}
|
|
127
119
|
|
|
128
120
|
// ---------------------------------------------------------------------------
|
|
129
121
|
// Public API
|
|
130
122
|
// ---------------------------------------------------------------------------
|
|
131
123
|
|
|
132
|
-
export async function seedGraph(
|
|
133
|
-
|
|
134
|
-
|
|
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)`,
|
|
124
|
+
export async function seedGraph(config: SeedGraphConfig, domains: Record<string, DomainDef>) {
|
|
125
|
+
// Validate all domains
|
|
126
|
+
const validationErrors = Object.entries(domains).flatMap(([name, domain]) =>
|
|
127
|
+
validateDomain(domain).map((err) => `[${name}] ${err}`),
|
|
145
128
|
);
|
|
146
|
-
}
|
|
147
129
|
|
|
148
|
-
|
|
149
|
-
|
|
130
|
+
if (validationErrors.length) {
|
|
131
|
+
validationErrors.forEach((err) => console.error(err));
|
|
132
|
+
throw new Error(`Validation failed with ${validationErrors.length} error(s)`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const client = createNeo4jClient(config.neo4j, config.embedding.model);
|
|
136
|
+
const targets = Object.entries(domains);
|
|
150
137
|
|
|
151
|
-
|
|
138
|
+
console.log(`Seeding ${targets.length} domain(s)...`);
|
|
152
139
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
140
|
+
// Generate Cypher for all target domains
|
|
141
|
+
const allCypher: [string, GeneratedCypher][] = targets.map(([name, data]) => [
|
|
142
|
+
name,
|
|
143
|
+
generateCypher(data),
|
|
144
|
+
]);
|
|
158
145
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
146
|
+
// Phase 1: Create all nodes across every domain first.
|
|
147
|
+
// This ensures cross-domain references resolve correctly.
|
|
148
|
+
const allNodes = allCypher.flatMap(([, c]) => c.nodes);
|
|
149
|
+
await runStatements(client, "Phase 1: Nodes", allNodes);
|
|
163
150
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
151
|
+
// Phase 2: Create all edges now that every node exists.
|
|
152
|
+
const allEdges = allCypher.flatMap(([, c]) => c.edges);
|
|
153
|
+
await runStatements(client, "Phase 2: Edges", allEdges);
|
|
167
154
|
|
|
168
|
-
|
|
155
|
+
console.log("\nGraph seeding complete!");
|
|
169
156
|
|
|
170
|
-
|
|
171
|
-
|
|
157
|
+
// Phase 3: Generate embeddings for concepts
|
|
158
|
+
await generateEmbeddings(client, config.embedding);
|
|
172
159
|
}
|
package/src/graph/types.ts
CHANGED
|
@@ -12,33 +12,33 @@
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
|
|
14
14
|
export type EndpointScalarType =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
| "uuid"
|
|
16
|
+
| "number"
|
|
17
|
+
| "date"
|
|
18
|
+
| "datetime"
|
|
19
|
+
| "string"
|
|
20
|
+
| "boolean"
|
|
21
|
+
| "any"
|
|
22
|
+
| "object";
|
|
23
23
|
|
|
24
24
|
export type EndpointProperty = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
31
|
};
|
|
32
32
|
|
|
33
33
|
export type ResponseKind = "object" | "array" | "paginated" | "file" | "none";
|
|
34
34
|
|
|
35
35
|
export type AutoGenerated = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
42
|
};
|
|
43
43
|
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
@@ -46,12 +46,12 @@ export type AutoGenerated = {
|
|
|
46
46
|
// ---------------------------------------------------------------------------
|
|
47
47
|
|
|
48
48
|
export type ConceptDef = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
readonly __brand: "concept";
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
aliases?: string[];
|
|
53
|
+
parentConcept?: ConceptDef;
|
|
54
|
+
governedBy?: RuleDef[];
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
@@ -59,31 +59,31 @@ export type ConceptDef = {
|
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
|
|
61
61
|
export type EndpointDef = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
87
|
};
|
|
88
88
|
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
@@ -91,27 +91,27 @@ export type EndpointDef = {
|
|
|
91
91
|
// ---------------------------------------------------------------------------
|
|
92
92
|
|
|
93
93
|
export type EndpointInput = {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
115
|
};
|
|
116
116
|
|
|
117
117
|
// ---------------------------------------------------------------------------
|
|
@@ -119,12 +119,12 @@ export type EndpointInput = {
|
|
|
119
119
|
// ---------------------------------------------------------------------------
|
|
120
120
|
|
|
121
121
|
export type ServiceDef = {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
readonly __brand: "service";
|
|
123
|
+
name: string;
|
|
124
|
+
description: string;
|
|
125
|
+
builtInId: string;
|
|
126
|
+
belongsTo: ConceptDef;
|
|
127
|
+
governedBy?: RuleDef[];
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
// ---------------------------------------------------------------------------
|
|
@@ -132,9 +132,9 @@ export type ServiceDef = {
|
|
|
132
132
|
// ---------------------------------------------------------------------------
|
|
133
133
|
|
|
134
134
|
export type RuleDef = {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
readonly __brand: "rule";
|
|
136
|
+
name: string;
|
|
137
|
+
description: string;
|
|
138
138
|
};
|
|
139
139
|
|
|
140
140
|
// ---------------------------------------------------------------------------
|
|
@@ -142,11 +142,11 @@ export type RuleDef = {
|
|
|
142
142
|
// ---------------------------------------------------------------------------
|
|
143
143
|
|
|
144
144
|
export type DomainDef = {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
readonly __brand: "domain";
|
|
146
|
+
name: string;
|
|
147
|
+
description: string;
|
|
148
|
+
concepts?: ConceptDef[];
|
|
149
|
+
endpoints?: EndpointDef[];
|
|
150
|
+
services?: ServiceDef[];
|
|
151
|
+
rules?: RuleDef[];
|
|
152
152
|
};
|
package/src/graph/validate.ts
CHANGED
|
@@ -1,80 +1,78 @@
|
|
|
1
1
|
import type { DomainDef } from "./types.ts";
|
|
2
2
|
|
|
3
3
|
export function validateDomain(data: DomainDef) {
|
|
4
|
-
|
|
4
|
+
const errors: string[] = [];
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const seenConceptNames = new Set<string>();
|
|
7
|
+
for (const c of data.concepts ?? []) {
|
|
8
|
+
if (seenConceptNames.has(c.name)) {
|
|
9
|
+
errors.push(`Duplicate concept name: "${c.name}"`);
|
|
10
|
+
}
|
|
11
|
+
seenConceptNames.add(c.name);
|
|
10
12
|
}
|
|
11
|
-
seenConceptNames.add(c.name);
|
|
12
|
-
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const seenEndpointNames = new Set<string>();
|
|
15
|
+
for (const e of data.endpoints ?? []) {
|
|
16
|
+
if (seenEndpointNames.has(e.name)) {
|
|
17
|
+
errors.push(`Duplicate endpoint name: "${e.name}"`);
|
|
18
|
+
}
|
|
19
|
+
seenEndpointNames.add(e.name);
|
|
18
20
|
}
|
|
19
|
-
seenEndpointNames.add(e.name);
|
|
20
|
-
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const seenServiceNames = new Set<string>();
|
|
23
|
+
for (const s of data.services ?? []) {
|
|
24
|
+
if (seenServiceNames.has(s.name)) {
|
|
25
|
+
errors.push(`Duplicate service name: "${s.name}"`);
|
|
26
|
+
}
|
|
27
|
+
seenServiceNames.add(s.name);
|
|
26
28
|
}
|
|
27
|
-
seenServiceNames.add(s.name);
|
|
28
|
-
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const seenRuleNames = new Set<string>();
|
|
31
|
+
for (const r of data.rules ?? []) {
|
|
32
|
+
if (seenRuleNames.has(r.name)) {
|
|
33
|
+
errors.push(`Duplicate rule name: "${r.name}"`);
|
|
34
|
+
}
|
|
35
|
+
seenRuleNames.add(r.name);
|
|
34
36
|
}
|
|
35
|
-
seenRuleNames.add(r.name);
|
|
36
|
-
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// Cross-domain references are allowed (concepts/endpoints/services may
|
|
39
|
+
// be defined in another domain), so we do not enforce local membership.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const referencedConcepts = new Set<string>();
|
|
42
|
+
const referencedRules = new Set<string>();
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
for (const ep of data.endpoints ?? []) {
|
|
45
|
+
ep.queries?.forEach((c) => referencedConcepts.add(c.name));
|
|
46
|
+
ep.mutates?.forEach((c) => referencedConcepts.add(c.name));
|
|
47
|
+
ep.returns?.forEach((r) => referencedConcepts.add(r.concept.name));
|
|
48
|
+
ep.governedBy?.forEach((r) => referencedRules.add(r.name));
|
|
49
|
+
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
for (const svc of data.services ?? []) {
|
|
52
|
+
referencedConcepts.add(svc.belongsTo.name);
|
|
53
|
+
svc.governedBy?.forEach((r) => referencedRules.add(r.name));
|
|
54
|
+
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
for (const c of data.concepts ?? []) {
|
|
57
|
+
c.governedBy?.forEach((r) => referencedRules.add(r.name));
|
|
58
|
+
if (c.parentConcept) {
|
|
59
|
+
referencedConcepts.add(c.parentConcept.name);
|
|
60
|
+
}
|
|
60
61
|
}
|
|
61
|
-
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
);
|
|
63
|
+
for (const c of data.concepts ?? []) {
|
|
64
|
+
if (!referencedConcepts.has(c.name)) {
|
|
65
|
+
errors.push(`Concept "${c.name}" is never referenced by any endpoint, or service`);
|
|
66
|
+
}
|
|
68
67
|
}
|
|
69
|
-
}
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
for (const r of data.rules ?? []) {
|
|
70
|
+
if (!referencedRules.has(r.name)) {
|
|
71
|
+
errors.push(
|
|
72
|
+
`Rule "${r.name}" is never referenced by any endpoint, service, or concept`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
76
75
|
}
|
|
77
|
-
}
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
return errors;
|
|
80
78
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Config types
|
|
2
2
|
export type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
CortexConfig,
|
|
4
|
+
CortexAgentDefinition,
|
|
5
|
+
KnowledgeConfig,
|
|
6
|
+
DatabaseConfig,
|
|
7
|
+
StorageConfig,
|
|
8
8
|
} from "./config";
|
|
9
9
|
|
|
10
10
|
// Types consumers may need
|