@m6d/cortex-server 1.4.0 → 1.5.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/dist/src/graph/expand-domains.d.ts +2 -0
- package/dist/src/graph/helpers.d.ts +5 -0
- package/dist/src/graph/resolver.d.ts +2 -0
- package/dist/src/graph/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/ai/prompt.ts +6 -3
- package/src/graph/expand-domains.ts +276 -0
- package/src/graph/generate-cypher.ts +18 -5
- package/src/graph/helpers.ts +1 -0
- package/src/graph/resolver.ts +10 -0
- package/src/graph/seed.ts +5 -2
- package/src/graph/types.ts +6 -0
- package/dist/src/ai/active-streams.test.d.ts +0 -1
- package/src/ai/active-streams.test.ts +0 -21
|
@@ -8,17 +8,20 @@ export declare function defineConcept(def: Omit<ConceptDef, "__brand">): {
|
|
|
8
8
|
aliases?: string[] | undefined;
|
|
9
9
|
parentConcept?: ConceptDef | undefined;
|
|
10
10
|
governedBy?: RuleDef[] | undefined;
|
|
11
|
+
metadata?: Record<string, unknown> | undefined;
|
|
11
12
|
__brand: "concept";
|
|
12
13
|
};
|
|
13
14
|
export declare function defineRule(def: Omit<RuleDef, "__brand">): {
|
|
14
15
|
name: string;
|
|
15
16
|
description: string;
|
|
17
|
+
metadata?: Record<string, unknown> | undefined;
|
|
16
18
|
__brand: "rule";
|
|
17
19
|
};
|
|
18
20
|
export declare function defineService(def: Omit<ServiceDef, "__brand">): {
|
|
19
21
|
name: string;
|
|
20
22
|
description: string;
|
|
21
23
|
governedBy?: RuleDef[] | undefined;
|
|
24
|
+
metadata?: Record<string, unknown> | undefined;
|
|
22
25
|
builtInId: string;
|
|
23
26
|
belongsTo: ConceptDef;
|
|
24
27
|
__brand: "service";
|
|
@@ -26,6 +29,7 @@ export declare function defineService(def: Omit<ServiceDef, "__brand">): {
|
|
|
26
29
|
export declare function defineDomain(def: Omit<DomainDef, "__brand">): {
|
|
27
30
|
name: string;
|
|
28
31
|
description: string;
|
|
32
|
+
metadata?: Record<string, unknown> | undefined;
|
|
29
33
|
concepts?: ConceptDef[] | undefined;
|
|
30
34
|
endpoints?: EndpointDef[] | undefined;
|
|
31
35
|
services?: ServiceDef[] | undefined;
|
|
@@ -57,4 +61,5 @@ export declare function defineEndpoint(input: EndpointInput): {
|
|
|
57
61
|
fromField: string;
|
|
58
62
|
}[] | undefined;
|
|
59
63
|
governedBy: RuleDef[] | undefined;
|
|
64
|
+
metadata: Record<string, unknown> | undefined;
|
|
60
65
|
};
|
|
@@ -34,6 +34,7 @@ export type ResolvedEndpoint = {
|
|
|
34
34
|
response: string;
|
|
35
35
|
dependencies: EndpointDependency[];
|
|
36
36
|
rules: string[];
|
|
37
|
+
metadata: string;
|
|
37
38
|
};
|
|
38
39
|
export type ResolvedService = {
|
|
39
40
|
concept: string;
|
|
@@ -41,6 +42,7 @@ export type ResolvedService = {
|
|
|
41
42
|
builtInId: string;
|
|
42
43
|
description: string;
|
|
43
44
|
rules: string[];
|
|
45
|
+
metadata: string;
|
|
44
46
|
};
|
|
45
47
|
export type ResolvedContext = {
|
|
46
48
|
readEndpoints: ResolvedEndpoint[];
|
|
@@ -31,6 +31,7 @@ export type ConceptDef = {
|
|
|
31
31
|
aliases?: string[];
|
|
32
32
|
parentConcept?: ConceptDef;
|
|
33
33
|
governedBy?: RuleDef[];
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
34
35
|
};
|
|
35
36
|
export type EndpointDef = {
|
|
36
37
|
readonly __brand: "endpoint";
|
|
@@ -57,6 +58,7 @@ export type EndpointDef = {
|
|
|
57
58
|
fromField: string;
|
|
58
59
|
}[];
|
|
59
60
|
governedBy?: RuleDef[];
|
|
61
|
+
metadata?: Record<string, unknown>;
|
|
60
62
|
};
|
|
61
63
|
export type EndpointInput = {
|
|
62
64
|
name: string;
|
|
@@ -79,6 +81,7 @@ export type EndpointInput = {
|
|
|
79
81
|
fromField: string;
|
|
80
82
|
}[];
|
|
81
83
|
governedBy?: RuleDef[];
|
|
84
|
+
metadata?: Record<string, unknown>;
|
|
82
85
|
};
|
|
83
86
|
export type ServiceDef = {
|
|
84
87
|
readonly __brand: "service";
|
|
@@ -87,11 +90,13 @@ export type ServiceDef = {
|
|
|
87
90
|
builtInId: string;
|
|
88
91
|
belongsTo: ConceptDef;
|
|
89
92
|
governedBy?: RuleDef[];
|
|
93
|
+
metadata?: Record<string, unknown>;
|
|
90
94
|
};
|
|
91
95
|
export type RuleDef = {
|
|
92
96
|
readonly __brand: "rule";
|
|
93
97
|
name: string;
|
|
94
98
|
description: string;
|
|
99
|
+
metadata?: Record<string, unknown>;
|
|
95
100
|
};
|
|
96
101
|
export type DomainDef = {
|
|
97
102
|
readonly __brand: "domain";
|
|
@@ -101,4 +106,5 @@ export type DomainDef = {
|
|
|
101
106
|
endpoints?: EndpointDef[];
|
|
102
107
|
services?: ServiceDef[];
|
|
103
108
|
rules?: RuleDef[];
|
|
109
|
+
metadata?: Record<string, unknown>;
|
|
104
110
|
};
|
package/package.json
CHANGED
package/src/ai/prompt.ts
CHANGED
|
@@ -65,6 +65,7 @@ The following endpoints were automatically matched to the user's message.`,
|
|
|
65
65
|
)
|
|
66
66
|
.join("\n")
|
|
67
67
|
: "";
|
|
68
|
+
const meta = ep.metadata !== "{}" ? `\n- Metadata: ${ep.metadata}` : "";
|
|
68
69
|
parts.push(
|
|
69
70
|
`
|
|
70
71
|
### ${ep.concept} (read)
|
|
@@ -72,7 +73,7 @@ The following endpoints were automatically matched to the user's message.`,
|
|
|
72
73
|
- ${ep.method} ${ep.path}
|
|
73
74
|
- Params: ${ep.params}
|
|
74
75
|
- Body: ${ep.body}
|
|
75
|
-
- Response: ${ep.response}${rules}${deps}`,
|
|
76
|
+
- Response: ${ep.response}${rules}${deps}${meta}`,
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -88,6 +89,7 @@ The following endpoints were automatically matched to the user's message.`,
|
|
|
88
89
|
)
|
|
89
90
|
.join("\n")
|
|
90
91
|
: "";
|
|
92
|
+
const meta = ep.metadata !== "{}" ? `\n- Metadata: ${ep.metadata}` : "";
|
|
91
93
|
parts.push(
|
|
92
94
|
`
|
|
93
95
|
### ${ep.concept} (write)
|
|
@@ -95,17 +97,18 @@ The following endpoints were automatically matched to the user's message.`,
|
|
|
95
97
|
- ${ep.method} ${ep.path}
|
|
96
98
|
- Params: ${ep.params}
|
|
97
99
|
- Body: ${ep.body}
|
|
98
|
-
- Response: ${ep.response}${rules}${deps}`,
|
|
100
|
+
- Response: ${ep.response}${rules}${deps}${meta}`,
|
|
99
101
|
);
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
for (const svc of resolved.services) {
|
|
103
105
|
const rules = svc.rules.length > 0 ? `\n Rules: ${svc.rules.join("; ")}` : "";
|
|
106
|
+
const meta = svc.metadata !== "{}" ? `\n- Metadata: ${svc.metadata}` : "";
|
|
104
107
|
parts.push(
|
|
105
108
|
`
|
|
106
109
|
### ${svc.concept} via ${svc.serviceName} (service)
|
|
107
110
|
- Built-in ID: ${svc.builtInId}
|
|
108
|
-
- Description: ${svc.description || "N/A"}${rules}`,
|
|
111
|
+
- Description: ${svc.description || "N/A"}${rules}${meta}`,
|
|
109
112
|
);
|
|
110
113
|
}
|
|
111
114
|
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { ConceptDef, DomainDef, EndpointDef, RuleDef, ServiceDef } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
type DomainCollections = {
|
|
4
|
+
concepts: ConceptDef[];
|
|
5
|
+
endpoints: EndpointDef[];
|
|
6
|
+
services: ServiceDef[];
|
|
7
|
+
rules: RuleDef[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type OwnershipMaps = {
|
|
11
|
+
concepts: WeakMap<ConceptDef, string>;
|
|
12
|
+
endpoints: WeakMap<EndpointDef, string>;
|
|
13
|
+
services: WeakMap<ServiceDef, string>;
|
|
14
|
+
rules: WeakMap<RuleDef, string>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function expandDomains(domains: Record<string, DomainDef>) {
|
|
18
|
+
const ownership = createOwnershipMaps(domains);
|
|
19
|
+
const expanded: Record<string, DomainDef> = {};
|
|
20
|
+
|
|
21
|
+
for (const [domainKey, domain] of Object.entries(domains)) {
|
|
22
|
+
const collections = cloneCollections(domain);
|
|
23
|
+
const pendingConcepts = [...collections.concepts];
|
|
24
|
+
const pendingEndpoints = [...collections.endpoints];
|
|
25
|
+
const pendingServices = [...collections.services];
|
|
26
|
+
|
|
27
|
+
while (
|
|
28
|
+
pendingConcepts.length > 0 ||
|
|
29
|
+
pendingEndpoints.length > 0 ||
|
|
30
|
+
pendingServices.length > 0
|
|
31
|
+
) {
|
|
32
|
+
const concept = pendingConcepts.pop();
|
|
33
|
+
if (concept) {
|
|
34
|
+
includeConcept({
|
|
35
|
+
domainKey,
|
|
36
|
+
concept,
|
|
37
|
+
ownership,
|
|
38
|
+
collections,
|
|
39
|
+
pendingConcepts,
|
|
40
|
+
pendingEndpoints,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const endpoint = pendingEndpoints.pop();
|
|
45
|
+
if (endpoint) {
|
|
46
|
+
includeEndpoint({
|
|
47
|
+
domainKey,
|
|
48
|
+
endpoint,
|
|
49
|
+
ownership,
|
|
50
|
+
collections,
|
|
51
|
+
pendingConcepts,
|
|
52
|
+
pendingEndpoints,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const service = pendingServices.pop();
|
|
57
|
+
if (service) {
|
|
58
|
+
includeService({
|
|
59
|
+
domainKey,
|
|
60
|
+
service,
|
|
61
|
+
ownership,
|
|
62
|
+
collections,
|
|
63
|
+
pendingConcepts,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
expanded[domainKey] = {
|
|
69
|
+
...domain,
|
|
70
|
+
concepts: collections.concepts,
|
|
71
|
+
endpoints: collections.endpoints,
|
|
72
|
+
services: collections.services,
|
|
73
|
+
rules: collections.rules,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return expanded;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createOwnershipMaps(domains: Record<string, DomainDef>) {
|
|
81
|
+
const ownership: OwnershipMaps = {
|
|
82
|
+
concepts: new WeakMap<ConceptDef, string>(),
|
|
83
|
+
endpoints: new WeakMap<EndpointDef, string>(),
|
|
84
|
+
services: new WeakMap<ServiceDef, string>(),
|
|
85
|
+
rules: new WeakMap<RuleDef, string>(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
for (const [domainKey, domain] of Object.entries(domains)) {
|
|
89
|
+
for (const concept of domain.concepts ?? []) {
|
|
90
|
+
assignOwner(ownership.concepts, concept, domainKey);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const endpoint of domain.endpoints ?? []) {
|
|
94
|
+
assignOwner(ownership.endpoints, endpoint, domainKey);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const service of domain.services ?? []) {
|
|
98
|
+
assignOwner(ownership.services, service, domainKey);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const rule of domain.rules ?? []) {
|
|
102
|
+
assignOwner(ownership.rules, rule, domainKey);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return ownership;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assignOwner<T extends object>(owners: WeakMap<T, string>, value: T, domainKey: string) {
|
|
110
|
+
if (!owners.has(value)) {
|
|
111
|
+
owners.set(value, domainKey);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cloneCollections(domain: DomainDef) {
|
|
116
|
+
return {
|
|
117
|
+
concepts: [...(domain.concepts ?? [])],
|
|
118
|
+
endpoints: [...(domain.endpoints ?? [])],
|
|
119
|
+
services: [...(domain.services ?? [])],
|
|
120
|
+
rules: [...(domain.rules ?? [])],
|
|
121
|
+
} satisfies DomainCollections;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function includeConcept(options: {
|
|
125
|
+
domainKey: string;
|
|
126
|
+
concept: ConceptDef;
|
|
127
|
+
ownership: OwnershipMaps;
|
|
128
|
+
collections: DomainCollections;
|
|
129
|
+
pendingConcepts: ConceptDef[];
|
|
130
|
+
pendingEndpoints: EndpointDef[];
|
|
131
|
+
}) {
|
|
132
|
+
const { domainKey, concept, ownership, collections, pendingConcepts, pendingEndpoints } =
|
|
133
|
+
options;
|
|
134
|
+
|
|
135
|
+
includeRule(domainKey, concept.governedBy ?? [], ownership, collections);
|
|
136
|
+
|
|
137
|
+
if (shouldAttach(ownership.concepts, concept, domainKey)) {
|
|
138
|
+
pushUnique(collections.concepts, concept);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
concept.parentConcept &&
|
|
143
|
+
shouldAttach(ownership.concepts, concept.parentConcept, domainKey)
|
|
144
|
+
) {
|
|
145
|
+
if (pushUnique(collections.concepts, concept.parentConcept)) {
|
|
146
|
+
pendingConcepts.push(concept.parentConcept);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const endpoint of collections.endpoints) {
|
|
151
|
+
if (referencesConcept(endpoint, concept)) {
|
|
152
|
+
includeEndpoint({
|
|
153
|
+
domainKey,
|
|
154
|
+
endpoint,
|
|
155
|
+
ownership,
|
|
156
|
+
collections,
|
|
157
|
+
pendingConcepts,
|
|
158
|
+
pendingEndpoints,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function includeEndpoint(options: {
|
|
165
|
+
domainKey: string;
|
|
166
|
+
endpoint: EndpointDef;
|
|
167
|
+
ownership: OwnershipMaps;
|
|
168
|
+
collections: DomainCollections;
|
|
169
|
+
pendingConcepts: ConceptDef[];
|
|
170
|
+
pendingEndpoints: EndpointDef[];
|
|
171
|
+
}) {
|
|
172
|
+
const { domainKey, endpoint, ownership, collections, pendingConcepts, pendingEndpoints } =
|
|
173
|
+
options;
|
|
174
|
+
|
|
175
|
+
if (shouldAttach(ownership.endpoints, endpoint, domainKey)) {
|
|
176
|
+
pushUnique(collections.endpoints, endpoint);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
includeRule(domainKey, endpoint.governedBy ?? [], ownership, collections);
|
|
180
|
+
|
|
181
|
+
for (const concept of endpoint.queries ?? []) {
|
|
182
|
+
if (
|
|
183
|
+
shouldAttach(ownership.concepts, concept, domainKey) &&
|
|
184
|
+
pushUnique(collections.concepts, concept)
|
|
185
|
+
) {
|
|
186
|
+
pendingConcepts.push(concept);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const concept of endpoint.mutates ?? []) {
|
|
191
|
+
if (
|
|
192
|
+
shouldAttach(ownership.concepts, concept, domainKey) &&
|
|
193
|
+
pushUnique(collections.concepts, concept)
|
|
194
|
+
) {
|
|
195
|
+
pendingConcepts.push(concept);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const returned of endpoint.returns ?? []) {
|
|
200
|
+
if (
|
|
201
|
+
shouldAttach(ownership.concepts, returned.concept, domainKey) &&
|
|
202
|
+
pushUnique(collections.concepts, returned.concept)
|
|
203
|
+
) {
|
|
204
|
+
pendingConcepts.push(returned.concept);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const dependency of endpoint.dependsOn ?? []) {
|
|
209
|
+
if (
|
|
210
|
+
shouldAttach(ownership.endpoints, dependency.endpoint, domainKey) &&
|
|
211
|
+
pushUnique(collections.endpoints, dependency.endpoint)
|
|
212
|
+
) {
|
|
213
|
+
pendingEndpoints.push(dependency.endpoint);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function includeService(options: {
|
|
219
|
+
domainKey: string;
|
|
220
|
+
service: ServiceDef;
|
|
221
|
+
ownership: OwnershipMaps;
|
|
222
|
+
collections: DomainCollections;
|
|
223
|
+
pendingConcepts: ConceptDef[];
|
|
224
|
+
}) {
|
|
225
|
+
const { domainKey, service, ownership, collections, pendingConcepts } = options;
|
|
226
|
+
|
|
227
|
+
if (shouldAttach(ownership.services, service, domainKey)) {
|
|
228
|
+
pushUnique(collections.services, service);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
includeRule(domainKey, service.governedBy ?? [], ownership, collections);
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
shouldAttach(ownership.concepts, service.belongsTo, domainKey) &&
|
|
235
|
+
pushUnique(collections.concepts, service.belongsTo)
|
|
236
|
+
) {
|
|
237
|
+
pendingConcepts.push(service.belongsTo);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function includeRule(
|
|
242
|
+
domainKey: string,
|
|
243
|
+
rules: readonly RuleDef[],
|
|
244
|
+
ownership: OwnershipMaps,
|
|
245
|
+
collections: DomainCollections,
|
|
246
|
+
) {
|
|
247
|
+
for (const rule of rules) {
|
|
248
|
+
if (shouldAttach(ownership.rules, rule, domainKey)) {
|
|
249
|
+
pushUnique(collections.rules, rule);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function shouldAttach<T extends object>(owners: WeakMap<T, string>, value: T, domainKey: string) {
|
|
255
|
+
const owner = owners.get(value);
|
|
256
|
+
return owner == null || owner === domainKey;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function pushUnique<T extends object>(values: T[], value: T) {
|
|
260
|
+
if (values.includes(value)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
values.push(value);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function referencesConcept(endpoint: EndpointDef, concept: ConceptDef) {
|
|
269
|
+
return (
|
|
270
|
+
endpoint.queries?.includes(concept) ||
|
|
271
|
+
endpoint.mutates?.includes(concept) ||
|
|
272
|
+
endpoint.returns?.some(function (returned) {
|
|
273
|
+
return returned.concept === concept;
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -31,9 +31,12 @@ export function generateCypher(data: DomainDef) {
|
|
|
31
31
|
const edges: string[] = [];
|
|
32
32
|
|
|
33
33
|
// -- Domain node --
|
|
34
|
+
const domainMeta = data.metadata
|
|
35
|
+
? `,\n d.metadata = '${jsonStr(data.metadata)}'`
|
|
36
|
+
: "";
|
|
34
37
|
nodes.push(
|
|
35
38
|
`MERGE (d:Domain {name: '${esc(data.name)}'})
|
|
36
|
-
SET d.description = '${esc(data.description)}'`,
|
|
39
|
+
SET d.description = '${esc(data.description)}'${domainMeta}`,
|
|
37
40
|
);
|
|
38
41
|
|
|
39
42
|
// -- Concept nodes --
|
|
@@ -42,10 +45,11 @@ export function generateCypher(data: DomainDef) {
|
|
|
42
45
|
c.aliases && c.aliases.length > 0
|
|
43
46
|
? `, c.aliases = [${c.aliases.map((a) => `'${esc(a)}'`).join(", ")}]`
|
|
44
47
|
: "";
|
|
48
|
+
const conceptMeta = c.metadata ? `, c.metadata = '${jsonStr(c.metadata)}'` : "";
|
|
45
49
|
|
|
46
50
|
nodes.push(
|
|
47
51
|
`MERGE (c:Concept {name: '${esc(c.name)}'})
|
|
48
|
-
SET c.description = '${esc(c.description)}'${aliasClause}`,
|
|
52
|
+
SET c.description = '${esc(c.description)}'${aliasClause}${conceptMeta}`,
|
|
49
53
|
);
|
|
50
54
|
|
|
51
55
|
edges.push(
|
|
@@ -73,6 +77,9 @@ export function generateCypher(data: DomainDef) {
|
|
|
73
77
|
|
|
74
78
|
// -- Endpoint nodes --
|
|
75
79
|
for (const ep of data.endpoints ?? []) {
|
|
80
|
+
const endpointMeta = ep.metadata
|
|
81
|
+
? `,\n e.metadata = '${jsonStr(ep.metadata)}'`
|
|
82
|
+
: "";
|
|
76
83
|
nodes.push(
|
|
77
84
|
`MERGE (e:Endpoint {path: '${esc(ep.path)}', method: '${ep.method}'})
|
|
78
85
|
SET e.name = '${esc(ep.name)}',
|
|
@@ -82,7 +89,7 @@ export function generateCypher(data: DomainDef) {
|
|
|
82
89
|
e.response = '${jsonStr(ep.response)}',
|
|
83
90
|
e.propertiesDescriptions = '${jsonStr(ep.propertiesDescriptions)}',
|
|
84
91
|
e.successStatus = ${ep.successStatus},
|
|
85
|
-
e.errorStatuses = [${ep.errorStatuses.join(", ")}]`,
|
|
92
|
+
e.errorStatuses = [${ep.errorStatuses.join(", ")}]${endpointMeta}`,
|
|
86
93
|
);
|
|
87
94
|
|
|
88
95
|
for (const concept of ep.queries ?? []) {
|
|
@@ -131,10 +138,13 @@ export function generateCypher(data: DomainDef) {
|
|
|
131
138
|
|
|
132
139
|
// -- Service nodes --
|
|
133
140
|
for (const svc of data.services ?? []) {
|
|
141
|
+
const serviceMeta = svc.metadata
|
|
142
|
+
? `,\n s.metadata = '${jsonStr(svc.metadata)}'`
|
|
143
|
+
: "";
|
|
134
144
|
nodes.push(
|
|
135
145
|
`MERGE (s:Service {builtInId: '${esc(svc.builtInId)}'})
|
|
136
146
|
SET s.name = '${esc(svc.name)}',
|
|
137
|
-
s.description = '${esc(svc.description)}'`,
|
|
147
|
+
s.description = '${esc(svc.description)}'${serviceMeta}`,
|
|
138
148
|
);
|
|
139
149
|
|
|
140
150
|
edges.push(
|
|
@@ -154,9 +164,12 @@ export function generateCypher(data: DomainDef) {
|
|
|
154
164
|
|
|
155
165
|
// -- Rule nodes --
|
|
156
166
|
for (const rule of data.rules ?? []) {
|
|
167
|
+
const ruleMeta = rule.metadata
|
|
168
|
+
? `,\n r.metadata = '${jsonStr(rule.metadata)}'`
|
|
169
|
+
: "";
|
|
157
170
|
nodes.push(
|
|
158
171
|
`MERGE (r:Rule {name: '${esc(rule.name)}'})
|
|
159
|
-
SET r.description = '${esc(rule.description)}'`,
|
|
172
|
+
SET r.description = '${esc(rule.description)}'${ruleMeta}`,
|
|
160
173
|
);
|
|
161
174
|
}
|
|
162
175
|
|
package/src/graph/helpers.ts
CHANGED
package/src/graph/resolver.ts
CHANGED
|
@@ -50,6 +50,7 @@ export type ResolvedEndpoint = {
|
|
|
50
50
|
response: string;
|
|
51
51
|
dependencies: EndpointDependency[];
|
|
52
52
|
rules: string[];
|
|
53
|
+
metadata: string;
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
export type ResolvedService = {
|
|
@@ -58,6 +59,7 @@ export type ResolvedService = {
|
|
|
58
59
|
builtInId: string;
|
|
59
60
|
description: string;
|
|
60
61
|
rules: string[];
|
|
62
|
+
metadata: string;
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
export type ResolvedContext = {
|
|
@@ -79,6 +81,7 @@ type EndpointRow = {
|
|
|
79
81
|
params: string | null;
|
|
80
82
|
body: string | null;
|
|
81
83
|
response: string | null;
|
|
84
|
+
metadata: string | null;
|
|
82
85
|
dependencies: (EndpointDependency | { depPath: null })[];
|
|
83
86
|
rules: (string | null)[];
|
|
84
87
|
};
|
|
@@ -88,6 +91,7 @@ type ServiceRow = {
|
|
|
88
91
|
serviceName: string;
|
|
89
92
|
builtInId: string;
|
|
90
93
|
description: string | null;
|
|
94
|
+
metadata: string | null;
|
|
91
95
|
rules: (string | null)[];
|
|
92
96
|
};
|
|
93
97
|
|
|
@@ -158,6 +162,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
158
162
|
e.params AS params,
|
|
159
163
|
e.body AS body,
|
|
160
164
|
e.response AS response,
|
|
165
|
+
e.metadata AS metadata,
|
|
161
166
|
dependencies,
|
|
162
167
|
collect(DISTINCT rEndpoint.description) +
|
|
163
168
|
collect(DISTINCT rConcept.description) AS rules`,
|
|
@@ -177,6 +182,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
177
182
|
params: row.params ?? "[]",
|
|
178
183
|
body: row.body ?? "[]",
|
|
179
184
|
response: row.response ?? "[]",
|
|
185
|
+
metadata: row.metadata ?? "{}",
|
|
180
186
|
dependencies: row.dependencies.filter(
|
|
181
187
|
(dep): dep is EndpointDependency => dep.depPath != null,
|
|
182
188
|
),
|
|
@@ -198,6 +204,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
198
204
|
svc.name AS serviceName,
|
|
199
205
|
svc.builtInId AS builtInId,
|
|
200
206
|
svc.description AS description,
|
|
207
|
+
svc.metadata AS metadata,
|
|
201
208
|
collect(DISTINCT rService.description) +
|
|
202
209
|
collect(DISTINCT rConcept.description) AS rules`,
|
|
203
210
|
{ names: conceptNames },
|
|
@@ -210,6 +217,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
210
217
|
serviceName: row.serviceName,
|
|
211
218
|
builtInId: row.builtInId,
|
|
212
219
|
description: row.description ?? "",
|
|
220
|
+
metadata: row.metadata ?? "{}",
|
|
213
221
|
rules: row.rules.filter((rule): rule is string => Boolean(rule)),
|
|
214
222
|
};
|
|
215
223
|
}),
|
|
@@ -239,6 +247,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
239
247
|
e.params AS params,
|
|
240
248
|
e.body AS body,
|
|
241
249
|
e.response AS response,
|
|
250
|
+
e.metadata AS metadata,
|
|
242
251
|
dependencies,
|
|
243
252
|
collect(DISTINCT rEndpoint.description) +
|
|
244
253
|
collect(DISTINCT rConcept.description) AS rules`,
|
|
@@ -259,6 +268,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
259
268
|
params: row.params ?? "[]",
|
|
260
269
|
body: row.body ?? "[]",
|
|
261
270
|
response: row.response ?? "[]",
|
|
271
|
+
metadata: row.metadata ?? "{}",
|
|
262
272
|
dependencies: row.dependencies.filter(
|
|
263
273
|
(dep): dep is EndpointDependency => dep.depPath != null,
|
|
264
274
|
),
|
package/src/graph/seed.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { EmbeddingModel } from "ai";
|
|
|
10
10
|
import { embed } from "ai";
|
|
11
11
|
import type { Neo4jClient, Neo4jConfig } from "./neo4j.ts";
|
|
12
12
|
import { createNeo4jClient } from "./neo4j.ts";
|
|
13
|
+
import { expandDomains } from "./expand-domains.ts";
|
|
13
14
|
import { generateCypher, type GeneratedCypher } from "./generate-cypher.ts";
|
|
14
15
|
import type { DomainDef } from "./types.ts";
|
|
15
16
|
import { validateDomain } from "./validate.ts";
|
|
@@ -122,8 +123,10 @@ async function generateEmbeddings(client: Neo4jClient, embeddingConfig: Embeddin
|
|
|
122
123
|
// ---------------------------------------------------------------------------
|
|
123
124
|
|
|
124
125
|
export async function seedGraph(config: SeedGraphConfig, domains: Record<string, DomainDef>) {
|
|
126
|
+
const expandedDomains = expandDomains(domains);
|
|
127
|
+
|
|
125
128
|
// Validate all domains
|
|
126
|
-
const validationErrors = Object.entries(
|
|
129
|
+
const validationErrors = Object.entries(expandedDomains).flatMap(([name, domain]) =>
|
|
127
130
|
validateDomain(domain).map((err) => `[${name}] ${err}`),
|
|
128
131
|
);
|
|
129
132
|
|
|
@@ -133,7 +136,7 @@ export async function seedGraph(config: SeedGraphConfig, domains: Record<string,
|
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
const client = createNeo4jClient(config.neo4j, config.embedding.model);
|
|
136
|
-
const targets = Object.entries(
|
|
139
|
+
const targets = Object.entries(expandedDomains);
|
|
137
140
|
|
|
138
141
|
console.log(`Seeding ${targets.length} domain(s)...`);
|
|
139
142
|
|
package/src/graph/types.ts
CHANGED
|
@@ -52,6 +52,7 @@ export type ConceptDef = {
|
|
|
52
52
|
aliases?: string[];
|
|
53
53
|
parentConcept?: ConceptDef;
|
|
54
54
|
governedBy?: RuleDef[];
|
|
55
|
+
metadata?: Record<string, unknown>;
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
// ---------------------------------------------------------------------------
|
|
@@ -84,6 +85,7 @@ export type EndpointDef = {
|
|
|
84
85
|
}[];
|
|
85
86
|
|
|
86
87
|
governedBy?: RuleDef[];
|
|
88
|
+
metadata?: Record<string, unknown>;
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
// ---------------------------------------------------------------------------
|
|
@@ -112,6 +114,7 @@ export type EndpointInput = {
|
|
|
112
114
|
}[];
|
|
113
115
|
|
|
114
116
|
governedBy?: RuleDef[];
|
|
117
|
+
metadata?: Record<string, unknown>;
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
// ---------------------------------------------------------------------------
|
|
@@ -125,6 +128,7 @@ export type ServiceDef = {
|
|
|
125
128
|
builtInId: string;
|
|
126
129
|
belongsTo: ConceptDef;
|
|
127
130
|
governedBy?: RuleDef[];
|
|
131
|
+
metadata?: Record<string, unknown>;
|
|
128
132
|
};
|
|
129
133
|
|
|
130
134
|
// ---------------------------------------------------------------------------
|
|
@@ -135,6 +139,7 @@ export type RuleDef = {
|
|
|
135
139
|
readonly __brand: "rule";
|
|
136
140
|
name: string;
|
|
137
141
|
description: string;
|
|
142
|
+
metadata?: Record<string, unknown>;
|
|
138
143
|
};
|
|
139
144
|
|
|
140
145
|
// ---------------------------------------------------------------------------
|
|
@@ -149,4 +154,5 @@ export type DomainDef = {
|
|
|
149
154
|
endpoints?: EndpointDef[];
|
|
150
155
|
services?: ServiceDef[];
|
|
151
156
|
rules?: RuleDef[];
|
|
157
|
+
metadata?: Record<string, unknown>;
|
|
152
158
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { registerStream, removeStream, subscribe } from "./active-streams.ts";
|
|
3
|
-
|
|
4
|
-
describe("active-streams", function () {
|
|
5
|
-
afterEach(function () {
|
|
6
|
-
removeStream("thread-under-test");
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("does not remove a newer stream when cleaning up an older one", function () {
|
|
10
|
-
const firstStream = registerStream("thread-under-test", new AbortController());
|
|
11
|
-
const secondStream = registerStream("thread-under-test", new AbortController());
|
|
12
|
-
|
|
13
|
-
removeStream("thread-under-test", firstStream.id);
|
|
14
|
-
|
|
15
|
-
expect(subscribe("thread-under-test")).not.toBeNull();
|
|
16
|
-
|
|
17
|
-
removeStream("thread-under-test", secondStream.id);
|
|
18
|
-
|
|
19
|
-
expect(subscribe("thread-under-test")).toBeNull();
|
|
20
|
-
});
|
|
21
|
-
});
|