1
0

KISC arch init

This commit is contained in:
2026-10-11 14:36:51 +03:00
commit 8fb6c52e44
89 changed files with 10931 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
FROM node:20-alpine
WORKDIR /app
# Copy MCP server code
COPY mcp/package.json mcp/tsconfig.json /app/mcp/
COPY mcp/src /app/mcp/src
# Copy architecture data (SSOT)
COPY domains /app/domains
WORKDIR /app/mcp
RUN npm ci || npm install
RUN npm run build
ENV ARCH_ROOT=/app/domains
EXPOSE 8787
CMD ["npm", "run", "start"]

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
{
"name": "kisc-arch-mcp",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"gen:doc": "ts-node src/generateDoc.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"dotenv": "^16.6.1",
"express": "^4.21.2",
"fast-glob": "^3.3.2",
"jose": "^5.10.0",
"js-yaml": "^4.1.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.17.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}

View File

@@ -0,0 +1,285 @@
import fs from "node:fs/promises";
import path from "node:path";
import yaml from "js-yaml";
type AnyObj = Record<string, any>;
const REPO_ROOT = process.env.REPO_ROOT || path.resolve(__dirname, "../../");
const DOMAIN_DIR = process.env.DOMAIN_DIR || "domains/kultura-valoda";
const OUT_FILE = process.env.OUT_FILE || "KISC-merkarhitektura-apraksts.md";
async function readText(rel: string): Promise<string> {
const abs = path.join(REPO_ROOT, rel);
return fs.readFile(abs, "utf-8");
}
async function readYaml(rel: string): Promise<AnyObj> {
const abs = path.join(REPO_ROOT, rel);
const raw = await fs.readFile(abs, "utf-8");
const obj = yaml.load(raw);
if (!obj || typeof obj !== "object") return {};
return obj as AnyObj;
}
function mdEscape(s: any): string {
const t = (s ?? "").toString();
return t.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
}
function mdTable(headers: string[], rows: string[][]): string {
const h = `| ${headers.join(" | ")} |`;
const sep = `| ${headers.map(() => "---").join(" | ")} |`;
const body = rows.map((r) => `| ${r.join(" | ")} |`).join("\n");
return [h, sep, body].filter(Boolean).join("\n");
}
function isRegisterRefLine(line: string): string | null {
// Matches: `registers/..../file.yaml` or `registers/..../file.yml`
const m = line.match(/`(registers\/[^`]+\.(yaml|yml))`/i);
return m?.[1] ?? null;
}
function isDiagramRefLine(line: string): string | null {
const m = line.match(/`(diagrams\/[^`]+\.(mmd|png|svg|pdf))`/i);
return m?.[1] ?? null;
}
function renderAbbreviations(obj: AnyObj): string {
const items = Array.isArray(obj.items) ? obj.items : [];
const rows = items.map((it: any) => [mdEscape(it.abbr), mdEscape(it.meaning)]);
return mdTable(["Saīsinājums", "Nozīme"], rows);
}
function renderTerms(obj: AnyObj): string {
const items = Array.isArray(obj.items) ? obj.items : [];
// Accept both {term, meaning} and {name, definition}
const rows = items.map((it: any) => [
mdEscape(it.term ?? it.name ?? it.id ?? ""),
mdEscape(it.meaning ?? it.definition ?? it.description ?? ""),
]);
return mdTable(["Termins", "Skaidrojums"], rows);
}
function renderRelatedDocs(obj: AnyObj): string {
const items = Array.isArray(obj.items) ? obj.items : [];
const rows = items.map((it: any) => [
mdEscape(it.id),
mdEscape(it.title),
mdEscape(it.date ?? ""),
mdEscape(it.type ?? ""),
mdEscape(it.relevance ?? ""),
]);
return mdTable(["ID", "Nosaukums", "Datums", "Veids", "Saistība / nozīme"], rows);
}
function renderOrganizations(obj: AnyObj): string {
const items = Array.isArray(obj.items) ? obj.items : [];
const rows = items.map((it: any) => [
mdEscape(it.id),
mdEscape(it.title),
mdEscape(it.role ?? ""),
mdEscape(Array.isArray(it.responsibilities) ? it.responsibilities.join("; ") : ""),
mdEscape(it.status ?? ""),
]);
return mdTable(["ID", "Organizācija", "Loma", "Atbildība (kopsavilkums)", "Statuss"], rows);
}
function renderGenericItemsTable(obj: AnyObj, preferredCols: string[] | null = null): string {
const items = Array.isArray(obj.items) ? obj.items : [];
if (!items.length) return "_(tukšs reģistrs)_";
const allKeys = new Set<string>();
for (const it of items) {
if (it && typeof it === "object") Object.keys(it).forEach((k) => allKeys.add(k));
}
const cols = preferredCols
? preferredCols.filter((c) => allKeys.has(c)).concat([...allKeys].filter((k) => !preferredCols.includes(k)))
: [...allKeys];
const headers = cols.map((c) => c);
const rows = items.map((it: any) =>
cols.map((c) => {
const v = it?.[c];
if (Array.isArray(v)) return mdEscape(v.join(", "));
if (v && typeof v === "object") return mdEscape(JSON.stringify(v));
return mdEscape(v ?? "");
})
);
return mdTable(headers, rows);
}
function renderLegalActs(obj: AnyObj): string {
const groups = Array.isArray(obj.groups) ? obj.groups : [];
if (!groups.length) return "_(tukšs reģistrs)_";
const out: string[] = [];
for (const g of groups) {
out.push(`### ${mdEscape(g.title ?? g.id ?? "")}`.trim());
const items = Array.isArray(g.items) ? g.items : [];
if (!items.length) {
out.push("_(nav ierakstu)_");
out.push("");
continue;
}
for (const it of items) {
const ref = mdEscape(it.ref ?? "");
const notes = mdEscape(it.notes ?? "");
out.push(`- **${ref}** — ${notes}`.trim());
}
out.push("");
}
return out.join("\n").trim();
}
function renderRoadmap(obj: AnyObj): string {
const items = Array.isArray(obj.items) ? obj.items : [];
if (!items.length) return "_(tukšs reģistrs)_";
const rows = items.map((it: any) => [
mdEscape(it.id),
mdEscape(it.title),
mdEscape(it.timeframe ?? ""),
mdEscape(Array.isArray(it.owner_orgs) ? it.owner_orgs.join(", ") : ""),
mdEscape(Array.isArray(it.depends_on) ? it.depends_on.join(", ") : ""),
mdEscape(Array.isArray(it.outputs) ? it.outputs.join("; ") : ""),
]);
return mdTable(["ID", "Pasākums", "Laika posms", "Atbildīgie", "Atkarības", "Rezultāti"], rows);
}
function renderRisks(obj: AnyObj): string {
const items = Array.isArray(obj.items) ? obj.items : [];
if (!items.length) return "_(tukšs reģistrs)_";
const rows = items.map((it: any) => [
mdEscape(it.id),
mdEscape(it.risk),
mdEscape(it.likelihood ?? ""),
mdEscape(it.impact ?? ""),
mdEscape(Array.isArray(it.mitigation) ? it.mitigation.join("; ") : it.mitigation ?? ""),
mdEscape(Array.isArray(it.owner_orgs) ? it.owner_orgs.join(", ") : ""),
]);
return mdTable(["ID", "Risks", "Varbūtība", "Ietekme", "Mazināšana", "Atbildīgie"], rows);
}
function renderInteractions(obj: AnyObj): string {
const items = Array.isArray(obj.items) ? obj.items : [];
if (!items.length) return "_(tukšs reģistrs)_";
const out: string[] = [];
for (const it of items) {
out.push(`### ${mdEscape(it.title ?? it.id ?? "")}`.trim());
out.push(`- **ID:** ${mdEscape(it.id)}`);
out.push(`- **Tips:** ${mdEscape(it.interaction_type ?? "")}`);
out.push(`- **Pretējā puse:** ${mdEscape(it.counterpart ?? "")}`);
if (Array.isArray(it.components) && it.components.length) {
out.push(`- **Komponentes:** ${mdEscape(it.components.join(", "))}`);
}
if (it.why_it_matters) {
out.push(`- **Kāpēc svarīgi:** ${mdEscape(it.why_it_matters)}`);
}
if (Array.isArray(it.requirements) && it.requirements.length) {
out.push(`- **Prasības:** ${mdEscape(it.requirements.join("; "))}`);
}
out.push("");
}
return out.join("\n").trim();
}
function renderRegisterByPath(relRegisterPathFromDomain: string, domainRoot: string): Promise<string> {
const fullRel = path.posix.join(domainRoot, relRegisterPathFromDomain);
return (async () => {
const obj = await readYaml(fullRel);
// Render by filename conventions
const file = relRegisterPathFromDomain.replace(/\\/g, "/");
if (file.endsWith("abbreviations.yaml") || file.endsWith("abbreviations.yml")) return renderAbbreviations(obj);
if (file.endsWith("terms.yaml") || file.endsWith("terms.yml")) return renderTerms(obj);
if (file.endsWith("related-documents.yaml") || file.endsWith("related-documents.yml")) return renderRelatedDocs(obj);
if (file.endsWith("organizations.yaml") || file.endsWith("organizations.yml")) return renderOrganizations(obj);
if (file.endsWith("legal-acts.yaml") || file.endsWith("legal-acts.yml")) return renderLegalActs(obj);
if (file.endsWith("roadmap.yaml") || file.endsWith("roadmap.yml")) return renderRoadmap(obj);
if (file.endsWith("risks.yaml") || file.endsWith("risks.yml")) return renderRisks(obj);
if (file.endsWith("interactions.yaml") || file.endsWith("interactions.yml")) return renderInteractions(obj);
// Known item-heavy registers: functions/services/info-resources/systems: use a nicer preferred order
const preferred = (() => {
if (file.includes("/04-functions/")) return ["id", "subdomain", "name", "change_status", "change_description"];
if (file.includes("/05-services/")) return ["id", "subdomain", "name", "change_status", "change_description"];
if (file.includes("/06-information-resources/"))
return ["id", "subdomain", "name", "type", "owner", "change_status", "change_description"];
if (file.includes("/07-systems/")) return ["id", "subdomain", "name", "owner", "change_status", "change_description"];
return null;
})();
return renderGenericItemsTable(obj, preferred);
})();
}
async function build() {
const domainRoot = DOMAIN_DIR.replace(/\\/g, "/");
const manifestRel = path.posix.join(domainRoot, "manifest.yaml");
const manifest = await readYaml(manifestRel);
if (!Array.isArray(manifest.views)) {
throw new Error(`manifest.yaml missing 'views' array: ${manifestRel}`);
}
const pieces: string[] = [];
pieces.push(`# ${manifest.title ?? "Dokuments"}`);
if (manifest.version) pieces.push(`\n_Versija: ${manifest.version}_\n`);
for (const v of manifest.views) {
const viewFile = v?.file;
if (!viewFile) continue;
const viewRel = path.posix.join(domainRoot, viewFile);
let md = await readText(viewRel);
const outLines: string[] = [];
const lines = md.split(/\r?\n/);
for (const line of lines) {
const regRef = isRegisterRefLine(line);
const diagramRef = isDiagramRefLine(line);
if (regRef) {
// keep the original line, then render the register below it
outLines.push(line);
const rendered = await renderRegisterByPath(regRef, domainRoot);
outLines.push("");
outLines.push(rendered);
outLines.push("");
continue;
}
if (diagramRef) {
// keep diagram placeholder as-is (per your rule)
outLines.push(line);
continue;
}
outLines.push(line);
}
pieces.push(outLines.join("\n").trimEnd());
pieces.push(""); // spacing between views
}
const finalMd = pieces.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
const outAbs = path.join(REPO_ROOT, OUT_FILE);
await fs.writeFile(outAbs, finalMd, "utf-8");
// eslint-disable-next-line no-console
console.log(`OK: generated ${OUT_FILE}`);
}
build().catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
process.exit(1);
});

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
import { loadRepo } from "./load.js";
import { RepoIndex } from "./types.js";
let cached: RepoIndex | null = null;
export function getIndex(archRoot: string): RepoIndex {
if (!cached) cached = loadRepo(archRoot);
return cached;
}
// Optional: reload endpoint support (not exposed yet)
export function reload(archRoot: string): RepoIndex {
cached = loadRepo(archRoot);
return cached;
}

View File

@@ -0,0 +1,195 @@
import fg from "fast-glob";
import fs from "node:fs";
import path from "node:path";
import yaml from "js-yaml";
import { Entity, Edge, RepoIndex } from "./types.js";
function toKindFromId(id: string): Entity["_kind"] {
if (id.startsWith("goal.")) return "goal";
if (id.startsWith("func.")) return "function";
if (
id.startsWith("svc.kultura.") ||
id.startsWith("svc.valoda.") ||
id.startsWith("svc.")
)
return "service";
if (
id.startsWith("ir.kultura.") ||
id.startsWith("ir.valoda.") ||
id.startsWith("ir.")
)
return "information_resource";
if (
id.startsWith("sys.kultura.") ||
id.startsWith("sys.valoda.") ||
id.startsWith("sys.")
)
return "system";
if (id.startsWith("principle.")) return "principle";
if (id.startsWith("org.")) return "org";
if (id.startsWith("domain.")) return "domain";
if (id.startsWith("subdomain.")) return "subdomain";
if (id.startsWith("rm.")) return "roadmap";
if (id.startsWith("risk.")) return "risk";
if (id.startsWith("int.")) return "interaction";
if (id.startsWith("doc.")) return "document";
if (id.startsWith("legal.")) return "legal";
return "unknown";
}
function safeString(x: unknown): string {
if (x === null || x === undefined) return "";
if (typeof x === "string") return x;
if (typeof x === "number" || typeof x === "boolean") return String(x);
try {
return JSON.stringify(x);
} catch {
return "";
}
}
function indexEntity(idx: RepoIndex, e: Entity) {
idx.entitiesById.set(e.id, e);
const hay = [
e.id,
e._kind,
e._domain ?? "",
e._subdomain ?? "",
safeString(e.title),
safeString(e.name),
safeString(e.description),
safeString(e.change_description),
safeString(e.status),
]
.join(" ")
.toLowerCase();
idx.textIndex.push({
id: e.id,
haystack: hay,
kind: e._kind,
domain: e._domain,
subdomain: e._subdomain,
});
}
function readYaml(filePath: string): any {
const raw = fs.readFileSync(filePath, "utf-8");
return yaml.load(raw);
}
export function loadRepo(archRoot: string): RepoIndex {
const idx: RepoIndex = {
entitiesById: new Map(),
edges: [],
textIndex: [],
resources: [],
};
// Register resource URIs for views and manifest and raw registers.
const domainManifests = fg.sync(["**/manifest.yaml"], {
cwd: archRoot,
dot: false,
absolute: true,
});
for (const mf of domainManifests) {
const mfObj: any = readYaml(mf);
const domainFolder = path.dirname(mf);
const domainSlug = path.basename(domainFolder);
idx.resources.push({
uri: `manifest://${domainSlug}`,
title: `Manifest: ${domainSlug}`,
mimeType: "application/yaml",
});
// Views resources
const views = (mfObj?.views ?? []) as Array<{
file: string;
title?: string;
id?: string | number;
}>;
for (const v of views) {
const name = path.basename(v.file, path.extname(v.file));
idx.resources.push({
uri: `views://${domainSlug}/${name}`,
title: `View: ${domainSlug} / ${v.title ?? name}`,
mimeType: "text/markdown",
});
}
// Register raw YAML registers as resources
const regFiles = fg.sync(["registers/**/*.yaml"], {
cwd: domainFolder,
absolute: true,
});
for (const rf of regFiles) {
const rel = path.relative(domainFolder, rf).replaceAll("\\", "/");
idx.resources.push({
uri: `register://${domainSlug}/${rel}`,
title: `Register: ${domainSlug}/${rel}`,
mimeType: "application/yaml",
});
}
// Load domain entity (if present)
const domainYaml = path.join(domainFolder, "registers/00-meta/domain.yaml");
if (fs.existsSync(domainYaml)) {
const d = readYaml(domainYaml);
const ent: Entity = {
...(d ?? {}),
id: d?.id ?? `domain.${domainSlug}`,
_kind: "domain",
_domain: `domain.${domainSlug}`,
_source_path: path.relative(archRoot, domainYaml).replaceAll("\\", "/"),
};
indexEntity(idx, ent);
}
// Load YAML registers with "items"
const regList = fg.sync(["registers/**/*.yaml"], {
cwd: domainFolder,
absolute: true,
});
for (const file of regList) {
const obj = readYaml(file);
// edges file
if (file.endsWith("registers/99-relations/edges.yaml") && obj?.edges) {
const edges = obj.edges as Edge[];
idx.edges.push(...edges);
continue;
}
// Single-entity YAML (has id but no items)
if (obj?.id && !obj?.items) {
const ent: Entity = {
...(obj ?? {}),
_kind: toKindFromId(obj.id),
_domain: `domain.${domainSlug}`,
_subdomain: obj?.subdomain ?? undefined,
_source_path: path.relative(archRoot, file).replaceAll("\\", "/"),
};
indexEntity(idx, ent);
continue;
}
// List YAML (items)
if (Array.isArray(obj?.items)) {
for (const it of obj.items) {
if (!it?.id) continue;
const ent: Entity = {
...(it ?? {}),
_kind: toKindFromId(it.id),
_domain: `domain.${domainSlug}`,
_subdomain: it?.subdomain ?? undefined,
_source_path: path.relative(archRoot, file).replaceAll("\\", "/"),
};
indexEntity(idx, ent);
}
}
}
}
return idx;
}

View File

@@ -0,0 +1,44 @@
export type DomainId = string;
export type Edge = {
from: string;
type: string;
to: string;
};
export type Entity = {
id: string;
_kind:
| "goal"
| "function"
| "service"
| "information_resource"
| "system"
| "principle"
| "org"
| "domain"
| "subdomain"
| "roadmap"
| "risk"
| "interaction"
| "document"
| "legal"
| "unknown";
_domain?: DomainId;
_subdomain?: string;
_source_path?: string;
[k: string]: any;
};
export type RepoIndex = {
entitiesById: Map<string, Entity>;
edges: Edge[];
textIndex: Array<{
id: string;
haystack: string;
kind: string;
domain?: string;
subdomain?: string;
}>;
resources: Array<{ uri: string; title: string; mimeType: string }>;
};

View File

@@ -0,0 +1,43 @@
import fs from "node:fs";
import path from "node:path";
import fg from "fast-glob";
function readFileSafe(filePath: string): string {
return fs.readFileSync(filePath, "utf-8");
}
export function getResource(archRoot: string, uri: string) {
// manifest://<domain>
if (uri.startsWith("manifest://")) {
const domain = uri.replace("manifest://", "");
const mf = path.join(archRoot, domain, "manifest.yaml");
if (!fs.existsSync(mf)) return { ok: false, error: `Not found: ${uri}` };
return { ok: true, uri, mimeType: "application/yaml", content: readFileSafe(mf) };
}
// views://<domain>/<viewfile-without-ext>
if (uri.startsWith("views://")) {
const rest = uri.replace("views://", "");
const [domain, viewName] = rest.split("/", 2);
const domainDir = path.join(archRoot, domain);
const matches = fg.sync([`views/${viewName}.md`], { cwd: domainDir, absolute: true });
if (!matches.length) return { ok: false, error: `Not found: ${uri}` };
return {
ok: true,
uri,
mimeType: "text/markdown",
content: readFileSafe(matches[0]!),
};
}
// register://<domain>/<relative-path>
if (uri.startsWith("register://")) {
const rest = uri.replace("register://", "");
const [domain, rel] = rest.split("/", 2);
const filePath = path.join(archRoot, domain, rel);
if (!fs.existsSync(filePath)) return { ok: false, error: `Not found: ${uri}` };
return { ok: true, uri, mimeType: "application/yaml", content: readFileSafe(filePath) };
}
return { ok: false, error: `Unsupported uri: ${uri}` };
}

View File

@@ -0,0 +1,8 @@
import { RepoIndex } from "../repo/types.js";
export function listResources(idx: RepoIndex) {
return {
ok: true,
resources: idx.resources,
};
}

View File

@@ -0,0 +1,11 @@
import { RepoIndex } from "../repo/types.js";
export function getEntityTool(idx: RepoIndex, args: any) {
const id = String(args?.id ?? "").trim();
if (!id) return { ok: false, error: "Missing args.id" };
const ent = idx.entitiesById.get(id);
if (!ent) return { ok: false, error: `Entity not found: ${id}` };
return { ok: true, entity: ent };
}

View File

@@ -0,0 +1,16 @@
import { RepoIndex } from "../repo/types.js";
export function listRelationsTool(idx: RepoIndex, args: any) {
const id = String(args?.id ?? "").trim();
if (!id) return { ok: false, error: "Missing args.id" };
const direction = String(args?.direction ?? "both"); // in|out|both
const edges = idx.edges.filter((e) => {
if (direction === "out") return e.from === id;
if (direction === "in") return e.to === id;
return e.from === id || e.to === id;
});
return { ok: true, edges };
}

View File

@@ -0,0 +1,29 @@
import { RepoIndex } from "../repo/types.js";
export function searchTool(idx: RepoIndex, args: any) {
const q = String(args?.query ?? "").toLowerCase().trim();
if (!q) return { ok: false, error: "Missing args.query" };
const kind = args?.kind ? String(args.kind) : null;
const subdomain = args?.subdomain ? String(args.subdomain) : null;
const hits: Array<{ id: string; score: number; kind: string; subdomain?: string }> = [];
for (const row of idx.textIndex) {
if (kind && row.kind !== kind) continue;
if (subdomain && row.subdomain !== subdomain) continue;
// Very simple scoring: count substring occurrences
const hay = row.haystack;
let score = 0;
let pos = hay.indexOf(q);
while (pos !== -1) {
score++;
pos = hay.indexOf(q, pos + q.length);
}
if (score > 0) hits.push({ id: row.id, score, kind: row.kind, subdomain: row.subdomain });
}
hits.sort((a, b) => b.score - a.score);
return { ok: true, results: hits.slice(0, 50) };
}

View File

@@ -0,0 +1,42 @@
import { RepoIndex } from "../repo/types.js";
export function subgraphTool(idx: RepoIndex, args: any) {
const seeds = Array.isArray(args?.seed_ids)
? args.seed_ids.map((x: any) => String(x))
: [];
const depth = Number.isFinite(args?.depth) ? Number(args.depth) : 1;
if (!seeds.length) return { ok: false, error: "Missing args.seed_ids[]" };
if (depth < 0 || depth > 5) return { ok: false, error: "depth must be 0..5" };
const nodes = new Set<string>(seeds);
const edgesOut: any[] = [];
let frontier = new Set<string>(seeds);
for (let d = 0; d < depth; d++) {
const next = new Set<string>();
for (const e of idx.edges) {
if (frontier.has(e.from)) {
edgesOut.push(e);
if (!nodes.has(e.to)) next.add(e.to);
nodes.add(e.to);
}
if (frontier.has(e.to)) {
edgesOut.push(e);
if (!nodes.has(e.from)) next.add(e.from);
nodes.add(e.from);
}
}
frontier = next;
if (!frontier.size) break;
}
const nodeObjs = Array.from(nodes).map(
(id) => idx.entitiesById.get(id) ?? { id, _kind: "unknown" }
);
return { ok: true, nodes: nodeObjs, edges: edgesOut };
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}