@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.
@@ -0,0 +1,2 @@
1
+ import type { DomainDef } from "./types.ts";
2
+ export declare function expandDomains(domains: Record<string, DomainDef>): Record<string, DomainDef>;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m6d/cortex-server",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Reusable AI agent chat server library for Hono + Bun",
5
5
  "license": "MIT",
6
6
  "type": "module",
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
 
@@ -64,5 +64,6 @@ export function defineEndpoint(input: EndpointInput) {
64
64
  returns: input.returns,
65
65
  dependsOn: input.dependsOn,
66
66
  governedBy: input.governedBy,
67
+ metadata: input.metadata,
67
68
  } satisfies EndpointDef;
68
69
  }
@@ -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(domains).flatMap(([name, domain]) =>
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(domains);
139
+ const targets = Object.entries(expandedDomains);
137
140
 
138
141
  console.log(`Seeding ${targets.length} domain(s)...`);
139
142
 
@@ -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
- });