@m6d/cortex-cli 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.
- package/README.md +135 -0
- package/bin/cortex.js +52 -0
- package/package.json +37 -0
- package/src/cli.ts +63 -0
- package/src/commands/graph.ts +253 -0
- package/src/commands/new.ts +333 -0
- package/src/commands/swagger.ts +285 -0
- package/src/config/load.ts +76 -0
- package/src/config/validate.ts +46 -0
- package/src/contracts/README.md +23 -0
- package/src/contracts/graph/embed.ts +50 -0
- package/src/contracts/graph/helpers.ts +63 -0
- package/src/contracts/graph/neo4j.ts +97 -0
- package/src/contracts/graph/schema.ts +65 -0
- package/src/contracts/graph/types.ts +131 -0
- package/src/contracts/graph.ts +36 -0
- package/src/contracts/runtime.ts +208 -0
- package/src/contracts/wire.ts +143 -0
- package/src/graph/expand-domains.ts +288 -0
- package/src/graph/generate-cypher.ts +201 -0
- package/src/graph/seed.ts +227 -0
- package/src/graph/validate.ts +78 -0
- package/src/scaffold/features.ts +189 -0
- package/src/scaffold/files.ts +568 -0
- package/src/scaffold/ports.ts +86 -0
- package/src/swagger/extract-endpoints.ts +258 -0
- package/src/swagger/generated-block.ts +74 -0
- package/src/swagger/openapi-schema.ts +149 -0
- package/src/swagger/parse-response.ts +88 -0
- package/src/swagger/types.ts +25 -0
- package/src/ui/prompts.ts +102 -0
- package/src/ui/report.ts +102 -0
- package/tsconfig.json +45 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config discovery and loading (design spec §6.2). Env needs no step of its own:
|
|
3
|
+
* discovery is cwd-only and Bun's `.env` auto-load is cwd-relative, so the two
|
|
4
|
+
* are always colocated and `process.env` is populated before the config
|
|
5
|
+
* evaluates.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { relative, resolve } from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
|
|
12
|
+
import * as report from "@/ui/report";
|
|
13
|
+
|
|
14
|
+
/** How a config path is printed once found: relative to the cwd, `./`-prefixed. */
|
|
15
|
+
export function configLabel(path: string) {
|
|
16
|
+
const relativePath = relative(process.cwd(), path);
|
|
17
|
+
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `./cortex.config.ts` in the cwd, that exact name — no upward walk and no
|
|
22
|
+
* extension fallback, so a run from the wrong directory fails loudly instead of
|
|
23
|
+
* silently picking up an ancestor's config. `--config` replaces the path
|
|
24
|
+
* outright and accepts anything Bun can import.
|
|
25
|
+
*/
|
|
26
|
+
export function findConfig(options: { path: string; explicit: boolean; command: string }) {
|
|
27
|
+
const { path, explicit, command } = options;
|
|
28
|
+
const absolute = resolve(process.cwd(), path);
|
|
29
|
+
if (existsSync(absolute)) return absolute;
|
|
30
|
+
|
|
31
|
+
if (explicit) {
|
|
32
|
+
report.fail({
|
|
33
|
+
what: `No config file at ${path}`,
|
|
34
|
+
blocks: [
|
|
35
|
+
"--config takes a path to a file Bun can import, resolved from the\ndirectory you run in.",
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
report.fail({
|
|
41
|
+
what: `No cortex.config.ts in ${process.cwd()}`,
|
|
42
|
+
blocks: [
|
|
43
|
+
"The CLI looks for exactly ./cortex.config.ts in the directory you run in.\nIt does not search parent directories.",
|
|
44
|
+
"Run it from the project root, or point at the file:",
|
|
45
|
+
` ${command} --config ./server/cortex.config.ts`,
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The whole loader: one import, one default export, no unwrap branch. */
|
|
51
|
+
export async function loadConfig(path: string, verbose: boolean) {
|
|
52
|
+
try {
|
|
53
|
+
const module = (await import(pathToFileURL(path).href)) as { default?: unknown };
|
|
54
|
+
const config = module.default;
|
|
55
|
+
|
|
56
|
+
if (config === undefined) throw new Error("The module has no default export.");
|
|
57
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
58
|
+
throw new Error("The default export is not an object.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return config as Record<string, unknown>;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
report.fail({
|
|
64
|
+
what: `Could not load ${configLabel(path)}`,
|
|
65
|
+
error,
|
|
66
|
+
blocks: [
|
|
67
|
+
// The line above this one is the underlying error, and for the most
|
|
68
|
+
// common failure it reads `Cannot find package '@m6d/cortex-server'` —
|
|
69
|
+
// the config imports the server for `defineAgent`. So the install is
|
|
70
|
+
// named here rather than left to be inferred.
|
|
71
|
+
"Importing the file has to run cleanly on its own: it default-exports a\nplain CortexConfig object and nothing else, and every package it imports\nhas to be installed — `bun add @m6d/cortex-server` if that is what is\nmissing above.",
|
|
72
|
+
],
|
|
73
|
+
verbose,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One generic rule (design spec §6.3): a key present with an `undefined` value
|
|
3
|
+
* is a failure, listed by full path. It is essentially always the
|
|
4
|
+
* `process.env["X"]!` pattern, where `!` silences the type error and `undefined`
|
|
5
|
+
* flows on to fail far from its cause. Absent keys are fine — that is how
|
|
6
|
+
* optional sections are expressed.
|
|
7
|
+
*
|
|
8
|
+
* The error names config paths only. Annotating each with the env var behind it
|
|
9
|
+
* is not implementable: the CLI imports an already-evaluated object, so the name
|
|
10
|
+
* is gone. Point at `.env` and stop.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { configLabel } from "@/config/load";
|
|
14
|
+
import * as report from "@/ui/report";
|
|
15
|
+
|
|
16
|
+
export function validateConfig(config: Record<string, unknown>, path: string) {
|
|
17
|
+
const paths = findUndefined(config, "", new WeakSet());
|
|
18
|
+
if (!paths.length) return;
|
|
19
|
+
|
|
20
|
+
report.fail({
|
|
21
|
+
what: `${configLabel(path)} has ${paths.length} undefined value${paths.length === 1 ? "" : "s"}`,
|
|
22
|
+
blocks: [
|
|
23
|
+
paths.map((entry) => ` ${entry}`).join("\n"),
|
|
24
|
+
"These keys are present but evaluate to undefined — usually a missing\nenvironment variable. Bun loads .env from the directory you run in; check\nthat ./.env exists and is complete.",
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// `seen` guards the domain graph, where concepts and endpoints reference each
|
|
30
|
+
// other in cycles.
|
|
31
|
+
function findUndefined(value: object, prefix: string, seen: WeakSet<object>): string[] {
|
|
32
|
+
if (seen.has(value)) return [];
|
|
33
|
+
seen.add(value);
|
|
34
|
+
|
|
35
|
+
return (Object.entries(value) as [string, unknown][]).flatMap(function ([key, child]) {
|
|
36
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
37
|
+
if (child === undefined) return [path];
|
|
38
|
+
if (typeof child !== "object" || child === null) return [];
|
|
39
|
+
// Domain data is built by the server's `define*` helpers, which write
|
|
40
|
+
// every optional key whether or not it was passed. `undefined` under a
|
|
41
|
+
// `__brand` means "this endpoint mutates nothing", never a missing env
|
|
42
|
+
// var, so the rule stops at the seed data it would otherwise drown in.
|
|
43
|
+
if ("__brand" in child) return [];
|
|
44
|
+
return findUndefined(child, path, seen);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @cortex/contracts
|
|
2
|
+
|
|
3
|
+
The shapes both sides of a cortex boundary must agree on. Private on purpose —
|
|
4
|
+
it never publishes; each publishable package carries its own copy:
|
|
5
|
+
|
|
6
|
+
- `@m6d/cortex-server` and `@m6d/cortex-cli` publish raw source, so their
|
|
7
|
+
`prepack` vendors a copy of this directory into the tarball (`contracts/` and
|
|
8
|
+
`src/contracts/` respectively), resolved there by their shipped tsconfig paths.
|
|
9
|
+
- `@m6d/cortex-angular` and `@m6d/cortex-react` compile the parts they import
|
|
10
|
+
into their build artifacts.
|
|
11
|
+
|
|
12
|
+
Three boundaries, one subpath each:
|
|
13
|
+
|
|
14
|
+
| import | boundary |
|
|
15
|
+
| --------------------------- | ------------------------------------------------------------- |
|
|
16
|
+
| `@cortex/contracts/wire` | chat client ↔ server (HTTP/WebSocket types) |
|
|
17
|
+
| `@cortex/contracts/runtime` | cortex-cc console ↔ server runtime API (zod schemas) |
|
|
18
|
+
| `@cortex/contracts/graph` | graph authoring (cli) ↔ server (vocabulary, helpers, clients) |
|
|
19
|
+
|
|
20
|
+
Everything is consumed as TypeScript source — no build, no dist. Nothing here
|
|
21
|
+
has a runtime dependency beyond zod (for `/runtime`), and nothing here may grow
|
|
22
|
+
one: these files are compiled into an Angular library, a React library, a CLI
|
|
23
|
+
and a Bun server alike.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeddings, which both halves of the graph need: `@m6d/cortex-cli` embeds every
|
|
3
|
+
* concept description as it seeds, `@m6d/cortex-server` embeds every prompt as it
|
|
4
|
+
* resolves. Two embedders would mean two vector spaces, so there is one.
|
|
5
|
+
*
|
|
6
|
+
* TanStack AI removed embeddings, and the graph only ever needed the plain
|
|
7
|
+
* OpenAI-compatible `/embeddings` call cortex is already configured for — so this
|
|
8
|
+
* is a bare `fetch`, and this package keeps its zero dependencies.
|
|
9
|
+
*
|
|
10
|
+
* Everything under `./graph` is written to the oldest target any consumer builds
|
|
11
|
+
* against — an Angular library compiles these sources directly — and imports
|
|
12
|
+
* siblings by their `.js` specifier so one emit serves both `dist` and source.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Everything cortex needs to reach one OpenAI-compatible embeddings endpoint. */
|
|
16
|
+
export type EmbeddingProviderConfig = {
|
|
17
|
+
baseURL: string;
|
|
18
|
+
apiKey: string;
|
|
19
|
+
modelName: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Turns a batch of strings into their vectors, in the order given. */
|
|
23
|
+
export type EmbedFn = (values: string[]) => Promise<number[][]>;
|
|
24
|
+
|
|
25
|
+
export function createEmbedder(config: EmbeddingProviderConfig) {
|
|
26
|
+
const url = `${config.baseURL.replace(/\/+$/, "")}/embeddings`;
|
|
27
|
+
|
|
28
|
+
return async function embed(values: string[]) {
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({ model: config.modelName, input: values }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Embedding request failed (${response.status}): ${await response.text()}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The API is free to return entries out of order; `index` is authoritative.
|
|
45
|
+
const body = (await response.json()) as { data: { index: number; embedding: number[] }[] };
|
|
46
|
+
return [...body.data]
|
|
47
|
+
.sort((left, right) => left.index - right.index)
|
|
48
|
+
.map((entry) => entry.embedding);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for building knowledge graph seed data. Projects author
|
|
3
|
+
* domains with these; `@m6d/cortex-server` re-exports them so authoring needs
|
|
4
|
+
* exactly one import path.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ConceptDef,
|
|
9
|
+
DomainDef,
|
|
10
|
+
EndpointDef,
|
|
11
|
+
EndpointInput,
|
|
12
|
+
RuleDef,
|
|
13
|
+
ServiceDef,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
export function defineConcept(def: Omit<ConceptDef, "__brand">) {
|
|
17
|
+
return { __brand: "concept" as const, ...def };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function defineRule(def: Omit<RuleDef, "__brand">) {
|
|
21
|
+
return { __brand: "rule" as const, ...def };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function defineService(def: Omit<ServiceDef, "__brand">) {
|
|
25
|
+
return { __brand: "service" as const, ...def };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function defineDomain(def: Omit<DomainDef, "__brand">) {
|
|
29
|
+
return { __brand: "domain" as const, ...def };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function defineEndpoint(input: EndpointInput) {
|
|
33
|
+
const propertiesDescriptions = Object.fromEntries(
|
|
34
|
+
Object.entries({
|
|
35
|
+
...(input.propertiesDescriptions ?? {}),
|
|
36
|
+
...(input.paramDescriptions ?? {}),
|
|
37
|
+
...(input.responseDescriptions ?? {}),
|
|
38
|
+
}).filter(([, value]) => value != null),
|
|
39
|
+
) as Record<string, string>;
|
|
40
|
+
|
|
41
|
+
const normalized = input.autoGenerated;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
__brand: "endpoint" as const,
|
|
45
|
+
name: input.name,
|
|
46
|
+
description: input.description ?? input.name,
|
|
47
|
+
path: input.path,
|
|
48
|
+
method: input.method,
|
|
49
|
+
propertiesDescriptions,
|
|
50
|
+
params: normalized ? [...normalized.params] : [],
|
|
51
|
+
body: normalized ? [...normalized.body] : [],
|
|
52
|
+
response: normalized ? [...normalized.response] : [],
|
|
53
|
+
successStatus: normalized?.successStatus ?? 200,
|
|
54
|
+
errorStatuses: normalized ? [...normalized.errorStatuses] : [],
|
|
55
|
+
responseKind: normalized?.responseKind ?? "object",
|
|
56
|
+
queries: input.queries,
|
|
57
|
+
mutates: input.mutates,
|
|
58
|
+
returns: input.returns,
|
|
59
|
+
dependsOn: input.dependsOn,
|
|
60
|
+
governedBy: input.governedBy,
|
|
61
|
+
metadata: input.metadata,
|
|
62
|
+
} satisfies EndpointDef;
|
|
63
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configurable Neo4j HTTP client, shared by the graph's writer and its reader.
|
|
3
|
+
*
|
|
4
|
+
* All configuration is passed explicitly — no environment variables are read —
|
|
5
|
+
* and the transport is `fetch`, so this package stays dependency-free.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EmbedFn } from "./embed";
|
|
9
|
+
|
|
10
|
+
export type Neo4jConfig = {
|
|
11
|
+
url: string;
|
|
12
|
+
user: string;
|
|
13
|
+
password: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type Neo4jClient = {
|
|
17
|
+
query(cypher: string, parameters?: Record<string, unknown>): Promise<string>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function createNeo4jClient(config: Neo4jConfig, embed?: EmbedFn) {
|
|
21
|
+
async function query(cypher: string, parameters?: Record<string, unknown>) {
|
|
22
|
+
// Auto-embed parameters whose keys start with '#'
|
|
23
|
+
const embeddedParams = Object.keys(parameters ?? {}).filter((x) => x.startsWith("#"));
|
|
24
|
+
|
|
25
|
+
if (embeddedParams.length) {
|
|
26
|
+
if (!embed) {
|
|
27
|
+
throw new Error("An embedder is required when using # parameters");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const embeddings = await embed(embeddedParams.map((k) => parameters![k] as string));
|
|
31
|
+
|
|
32
|
+
embeddedParams.forEach((k, idx) => {
|
|
33
|
+
delete parameters![k];
|
|
34
|
+
parameters![k.slice(1)] = embeddings[idx]!;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const response = await fetch(`${config.url}/db/neo4j/tx/commit`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Authorization: `Basic ${btoa(`${config.user}:${config.password}`)}`,
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
statements: [
|
|
46
|
+
{
|
|
47
|
+
statement: cypher,
|
|
48
|
+
parameters: parameters ?? {},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
error: true,
|
|
57
|
+
status: response.status,
|
|
58
|
+
message: `Neo4j request failed with status ${response.status}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = (await response.json()) as {
|
|
63
|
+
errors?: { code?: string; message: string }[];
|
|
64
|
+
results: { columns: string[]; data: { row: unknown[] }[] }[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (result.errors && result.errors.length > 0) {
|
|
68
|
+
// `code` rides alongside the message rather than inside it: it is the
|
|
69
|
+
// one-line identifier a report shows, and existing readers of
|
|
70
|
+
// `message` keep seeing exactly what they saw before.
|
|
71
|
+
return JSON.stringify({
|
|
72
|
+
error: true,
|
|
73
|
+
code: result.errors[0]!.code,
|
|
74
|
+
message: result.errors.map((e) => e.message).join("; "),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Transform Neo4j response into a clean array of objects
|
|
79
|
+
const queryResult = result.results[0];
|
|
80
|
+
if (!queryResult) {
|
|
81
|
+
return JSON.stringify([]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { columns, data } = queryResult;
|
|
85
|
+
const rows = data.map((entry) => {
|
|
86
|
+
const obj: Record<string, unknown> = {};
|
|
87
|
+
columns.forEach((col, i) => {
|
|
88
|
+
obj[col] = entry.row[i];
|
|
89
|
+
});
|
|
90
|
+
return obj;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return JSON.stringify(rows);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { query } satisfies Neo4jClient;
|
|
97
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between the graph's writer and its reader.
|
|
3
|
+
*
|
|
4
|
+
* Seeding lives in `@m6d/cortex-cli` and resolution lives in `@m6d/cortex-server`,
|
|
5
|
+
* two packages a project installs and upgrades separately. The labels, relationship
|
|
6
|
+
* types, property names and index name they agree on are this module, here, so
|
|
7
|
+
* neither side owns the vocabulary the other has to match.
|
|
8
|
+
*
|
|
9
|
+
* A mismatch between the two is invisible at runtime: the resolver's Cypher
|
|
10
|
+
* matches nothing and retrieval comes back empty rather than failing. That is what
|
|
11
|
+
* `GRAPH_SCHEMA_VERSION` is for — bump it on any change to the shape below. The
|
|
12
|
+
* CLI stamps the version it wrote onto the graph and the server checks that stamp
|
|
13
|
+
* on its first resolve, so skew surfaces as an error instead of as silence.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const GRAPH_SCHEMA_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
export const GRAPH_SCHEMA = {
|
|
19
|
+
label: {
|
|
20
|
+
domain: "Domain",
|
|
21
|
+
concept: "Concept",
|
|
22
|
+
endpoint: "Endpoint",
|
|
23
|
+
service: "Service",
|
|
24
|
+
rule: "Rule",
|
|
25
|
+
/** Stamped with the version above, so a stale graph is detectable later. */
|
|
26
|
+
marker: "CortexGraph",
|
|
27
|
+
},
|
|
28
|
+
rel: {
|
|
29
|
+
belongsTo: "BELONGS_TO",
|
|
30
|
+
specializes: "SPECIALIZES",
|
|
31
|
+
queriedVia: "QUERIED_VIA",
|
|
32
|
+
mutatedVia: "MUTATED_VIA",
|
|
33
|
+
returns: "RETURNS",
|
|
34
|
+
dependsOn: "DEPENDS_ON",
|
|
35
|
+
governs: "GOVERNS",
|
|
36
|
+
requestedVia: "REQUESTED_VIA",
|
|
37
|
+
},
|
|
38
|
+
prop: {
|
|
39
|
+
name: "name",
|
|
40
|
+
description: "description",
|
|
41
|
+
aliases: "aliases",
|
|
42
|
+
metadata: "metadata",
|
|
43
|
+
path: "path",
|
|
44
|
+
method: "method",
|
|
45
|
+
params: "params",
|
|
46
|
+
body: "body",
|
|
47
|
+
response: "response",
|
|
48
|
+
propertiesDescriptions: "propertiesDescriptions",
|
|
49
|
+
successStatus: "successStatus",
|
|
50
|
+
errorStatuses: "errorStatuses",
|
|
51
|
+
builtInId: "builtInId",
|
|
52
|
+
embedding: "embedding",
|
|
53
|
+
/** On a RETURNS edge: which part of the response carries the concept. */
|
|
54
|
+
field: "field",
|
|
55
|
+
/** On a DEPENDS_ON edge. */
|
|
56
|
+
paramName: "paramName",
|
|
57
|
+
fromField: "fromField",
|
|
58
|
+
/** On the marker node. */
|
|
59
|
+
version: "version",
|
|
60
|
+
},
|
|
61
|
+
/** Created by the CLI's seeder, queried by name in the resolver's first hop. */
|
|
62
|
+
conceptEmbeddingsIndex: "concept_embeddings",
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
export type GraphSchema = typeof GRAPH_SCHEMA;
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
export type EndpointScalarType =
|
|
11
|
+
| "uuid"
|
|
12
|
+
| "number"
|
|
13
|
+
| "date"
|
|
14
|
+
| "datetime"
|
|
15
|
+
| "string"
|
|
16
|
+
| "boolean"
|
|
17
|
+
| "any"
|
|
18
|
+
| "object";
|
|
19
|
+
|
|
20
|
+
export type EndpointProperty = {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly required: boolean;
|
|
23
|
+
// NonNullable<unknown> keeps the literal union visible to autocomplete.
|
|
24
|
+
readonly type: EndpointScalarType | (string & NonNullable<unknown>);
|
|
25
|
+
readonly isArray?: boolean;
|
|
26
|
+
readonly properties?: readonly EndpointProperty[];
|
|
27
|
+
readonly description?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ResponseKind = "object" | "array" | "paginated" | "file" | "none";
|
|
31
|
+
|
|
32
|
+
export type AutoGenerated = {
|
|
33
|
+
readonly params: readonly EndpointProperty[];
|
|
34
|
+
readonly body: readonly EndpointProperty[];
|
|
35
|
+
readonly response: readonly EndpointProperty[];
|
|
36
|
+
readonly successStatus: number;
|
|
37
|
+
readonly errorStatuses: readonly number[];
|
|
38
|
+
readonly responseKind?: ResponseKind;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ConceptDef = {
|
|
42
|
+
readonly __brand: "concept";
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
aliases?: string[];
|
|
46
|
+
parentConcept?: ConceptDef;
|
|
47
|
+
governedBy?: RuleDef[];
|
|
48
|
+
metadata?: Record<string, unknown>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type EndpointDef = {
|
|
52
|
+
readonly __brand: "endpoint";
|
|
53
|
+
name: string;
|
|
54
|
+
description: string;
|
|
55
|
+
path: string;
|
|
56
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
57
|
+
propertiesDescriptions: Record<string, string>;
|
|
58
|
+
|
|
59
|
+
params: EndpointProperty[];
|
|
60
|
+
body: EndpointProperty[];
|
|
61
|
+
response: EndpointProperty[];
|
|
62
|
+
successStatus: number;
|
|
63
|
+
errorStatuses: number[];
|
|
64
|
+
responseKind: ResponseKind;
|
|
65
|
+
|
|
66
|
+
queries?: ConceptDef[];
|
|
67
|
+
mutates?: ConceptDef[];
|
|
68
|
+
|
|
69
|
+
returns?: { concept: ConceptDef; field?: string }[];
|
|
70
|
+
dependsOn?: {
|
|
71
|
+
endpoint: EndpointDef;
|
|
72
|
+
paramName: string;
|
|
73
|
+
fromField: string;
|
|
74
|
+
}[];
|
|
75
|
+
|
|
76
|
+
governedBy?: RuleDef[];
|
|
77
|
+
metadata?: Record<string, unknown>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type EndpointInput = {
|
|
81
|
+
name: string;
|
|
82
|
+
path: string;
|
|
83
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
84
|
+
description?: string;
|
|
85
|
+
autoGenerated?: AutoGenerated;
|
|
86
|
+
|
|
87
|
+
propertiesDescriptions?: Record<string, string>;
|
|
88
|
+
|
|
89
|
+
queries?: ConceptDef[];
|
|
90
|
+
mutates?: ConceptDef[];
|
|
91
|
+
paramDescriptions?: Partial<Record<string, string>>;
|
|
92
|
+
responseDescriptions?: Partial<Record<string, string>>;
|
|
93
|
+
|
|
94
|
+
returns?: { concept: ConceptDef; field?: string }[];
|
|
95
|
+
dependsOn?: {
|
|
96
|
+
endpoint: EndpointDef;
|
|
97
|
+
paramName: string;
|
|
98
|
+
fromField: string;
|
|
99
|
+
}[];
|
|
100
|
+
|
|
101
|
+
governedBy?: RuleDef[];
|
|
102
|
+
metadata?: Record<string, unknown>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type ServiceDef = {
|
|
106
|
+
readonly __brand: "service";
|
|
107
|
+
name: string;
|
|
108
|
+
description: string;
|
|
109
|
+
builtInId: string;
|
|
110
|
+
belongsTo: ConceptDef;
|
|
111
|
+
governedBy?: RuleDef[];
|
|
112
|
+
metadata?: Record<string, unknown>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type RuleDef = {
|
|
116
|
+
readonly __brand: "rule";
|
|
117
|
+
name: string;
|
|
118
|
+
description: string;
|
|
119
|
+
metadata?: Record<string, unknown>;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type DomainDef = {
|
|
123
|
+
readonly __brand: "domain";
|
|
124
|
+
name: string;
|
|
125
|
+
description: string;
|
|
126
|
+
concepts?: ConceptDef[];
|
|
127
|
+
endpoints?: EndpointDef[];
|
|
128
|
+
services?: ServiceDef[];
|
|
129
|
+
rules?: RuleDef[];
|
|
130
|
+
metadata?: Record<string, unknown>;
|
|
131
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The graph contract — the knowledge-graph vocabulary, authoring types and
|
|
3
|
+
* clients that `@m6d/cortex-cli` writes with and `@m6d/cortex-server` reads
|
|
4
|
+
* with. `@m6d/cortex-server` re-exports all of it, so a project authoring
|
|
5
|
+
* domains still has exactly one import path.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type { GraphSchema } from "./graph/schema";
|
|
9
|
+
export { GRAPH_SCHEMA, GRAPH_SCHEMA_VERSION } from "./graph/schema";
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
EndpointScalarType,
|
|
13
|
+
EndpointProperty,
|
|
14
|
+
ResponseKind,
|
|
15
|
+
AutoGenerated,
|
|
16
|
+
ConceptDef,
|
|
17
|
+
EndpointDef,
|
|
18
|
+
EndpointInput,
|
|
19
|
+
ServiceDef,
|
|
20
|
+
RuleDef,
|
|
21
|
+
DomainDef,
|
|
22
|
+
} from "./graph/types";
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
defineConcept,
|
|
26
|
+
defineRule,
|
|
27
|
+
defineService,
|
|
28
|
+
defineDomain,
|
|
29
|
+
defineEndpoint,
|
|
30
|
+
} from "./graph/helpers";
|
|
31
|
+
|
|
32
|
+
export type { Neo4jConfig, Neo4jClient } from "./graph/neo4j";
|
|
33
|
+
export { createNeo4jClient } from "./graph/neo4j";
|
|
34
|
+
|
|
35
|
+
export type { EmbedFn, EmbeddingProviderConfig } from "./graph/embed";
|
|
36
|
+
export { createEmbedder } from "./graph/embed";
|