(llm-first): context budget, validation, and eval harness, orchestration general-prompt

This commit is contained in:
MaKarin
2026-04-03 14:17:21 +03:00
parent 79c9589658
commit c42a88dff6
189 changed files with 15538 additions and 9109 deletions

View File

@@ -0,0 +1,301 @@
// Deterministic OpenAPI 3.0.3 generator from api-summary.json / toir.api.dsl.
//
// This script is part of the Tier 1 deterministic preprocessing layer.
// It converts the canonical api-summary (produced by tools/api-summary.mjs)
// into a valid OpenAPI 3.0.3 document.
//
// Usage:
// node tools/api-summary-to-openapi.mjs --out openapi.json
// npm run generate:openapi
//
// No LLM involvement. The output is reproducible from DSL + this script alone.
import { writeFileSync } from 'node:fs';
import path from 'node:path';
import { buildApiSummary } from './api-summary.mjs';
const rootDir = process.cwd();
// ---------------------------------------------------------------------------
// DSL scalar → OpenAPI type
// ---------------------------------------------------------------------------
function dslTypeToOpenApi(dslType) {
switch (dslType) {
case 'uuid':
return { type: 'string', format: 'uuid' };
case 'string':
return { type: 'string' };
case 'text':
return { type: 'string' };
case 'integer':
return { type: 'integer', format: 'int32' };
case 'number':
return { type: 'number' };
case 'decimal':
return { type: 'string', format: 'decimal' };
case 'date':
return { type: 'string', format: 'date-time' };
case 'boolean':
return { type: 'boolean' };
default:
// enum names and DTO references handled by caller
return null;
}
}
// ---------------------------------------------------------------------------
// Resolve a DSL field type to an OpenAPI schema reference or inline schema.
// dtoNames — Set of known DTO names in the api summary.
// enumNames — Set of known enum names (derived from type mappings table).
// ---------------------------------------------------------------------------
function resolveFieldType(dslType, dtoNames, enumNames) {
if (!dslType) return { type: 'object' };
// Array type: "DTO.Foo[]"
if (dslType.endsWith('[]')) {
const inner = dslType.slice(0, -2);
return { type: 'array', items: resolveFieldType(inner, dtoNames, enumNames) };
}
// Scalar
const scalar = dslTypeToOpenApi(dslType);
if (scalar) return scalar;
// DTO reference
if (dtoNames.has(dslType)) {
return { $ref: `#/components/schemas/${dslType.replace(/^DTO\./, '')}` };
}
// Enum reference
if (enumNames.has(dslType)) {
return { $ref: `#/components/schemas/${dslType}` };
}
// Unknown — emit as string with x-dsl-type annotation
return { type: 'string', 'x-dsl-type': dslType };
}
// ---------------------------------------------------------------------------
// Build OpenAPI schema object from a DTO definition
// ---------------------------------------------------------------------------
function buildDtoSchema(dto, dtoNames, enumNames) {
const properties = {};
const required = [];
for (const field of dto.fields) {
const schema = resolveFieldType(field.type, dtoNames, enumNames);
if (field.description && schema.$ref) {
// OpenAPI 3.0.3: $ref cannot have sibling keys — wrap with allOf
properties[field.name] = { allOf: [schema], description: field.description };
} else {
if (field.description) schema.description = field.description;
properties[field.name] = schema;
}
if (field.required) required.push(field.name);
}
const schema = {
type: 'object',
properties,
};
if (dto.description) schema.description = dto.description;
if (required.length > 0) schema.required = required;
return schema;
}
// ---------------------------------------------------------------------------
// Convert DSL HTTP method to OpenAPI method key
// ---------------------------------------------------------------------------
function methodKey(method) {
return (method ?? 'get').toLowerCase();
}
// ---------------------------------------------------------------------------
// Build OpenAPI path item for an endpoint
// ---------------------------------------------------------------------------
function buildPathOperation(endpoint, apiDescription, dtoNames, enumNames) {
const operation = {};
if (endpoint.description) operation.summary = endpoint.description;
// Security — all endpoints require bearer auth
operation.security = [{ bearerAuth: [] }];
// Tags — derive from API name or path
const tag = apiDescription ? apiDescription.replace(/^API управления\s+/i, '').replace(/ами$/, '') : undefined;
if (tag) operation.tags = [tag];
// Parameters — detect path params by matching attribute names against {param} in the path
const pathTemplate = endpoint.path ?? '';
const pathParamNames = new Set(
[...pathTemplate.matchAll(/\{(\w+)\}/g)].map((m) => m[1]),
);
const pathParams = endpoint.attributes.filter((a) => pathParamNames.has(a.name));
if (pathParams.length > 0) {
operation.parameters = pathParams.map((p) => ({
name: p.name,
in: 'path',
required: true,
schema: resolveFieldType(p.type, dtoNames, enumNames),
...(p.description ? { description: p.description } : {}),
}));
}
// Request body
const requestAttr = endpoint.attributes.find((a) => a.name === 'request');
if (requestAttr) {
operation.requestBody = {
required: true,
content: {
'application/json': {
schema: resolveFieldType(requestAttr.type, dtoNames, enumNames),
},
},
};
}
// Response
const responseAttr = endpoint.attributes.find((a) => a.name === 'response');
const responseSchema = responseAttr
? resolveFieldType(responseAttr.type, dtoNames, enumNames)
: { type: 'object' };
const successCode = endpoint.method === 'POST' && !endpoint.path?.endsWith('/page') ? '201' : '200';
operation.responses = {
[successCode]: {
description: 'Success',
content: {
'application/json': {
schema: responseSchema,
},
},
},
'401': { description: 'Unauthorized' },
'403': { description: 'Forbidden' },
};
if (endpoint.method === 'DELETE') {
operation.responses = {
'204': { description: 'No content' },
'401': { description: 'Unauthorized' },
'403': { description: 'Forbidden' },
'404': { description: 'Not found' },
};
delete operation.responses['201'];
}
return operation;
}
// ---------------------------------------------------------------------------
// Main builder
// ---------------------------------------------------------------------------
function buildOpenApi(summary) {
const dtoNames = new Set(summary.dtos.map((d) => d.name));
// Build enum map from api-summary enums block (fully declared enums with values)
const enumMap = new Map((summary.enums ?? []).map((e) => [e.name, e]));
// Also collect enum names referenced in DTO fields that are not in the declared enums
// (covers cases where enums are declared in domain.dsl but referenced in api.dsl)
const enumNames = new Set(enumMap.keys());
for (const dto of summary.dtos) {
for (const field of dto.fields) {
const t = field.type?.replace('[]', '');
if (t && !dtoNames.has(t) && !dslTypeToOpenApi(t)) {
enumNames.add(t);
}
}
}
// Schemas — one per DTO
const schemas = {};
for (const dto of summary.dtos) {
const schemaName = dto.name.replace(/^DTO\./, '');
schemas[schemaName] = buildDtoSchema(dto, dtoNames, enumNames);
}
// Enum schemas — use actual values when available, otherwise annotate as opaque string enum
for (const enumName of enumNames) {
const enumDef = enumMap.get(enumName);
if (enumDef && enumDef.values.length > 0) {
schemas[enumName] = {
type: 'string',
enum: enumDef.values.map((v) => v.name),
'x-enum-labels': Object.fromEntries(
enumDef.values.filter((v) => v.label).map((v) => [v.name, v.label]),
),
...(enumDef.description ? { description: enumDef.description } : {}),
};
} else {
schemas[enumName] = {
type: 'string',
'x-dsl-enum': enumName,
description: `Enum: ${enumName} (values defined in domain/*.api.dsl)`,
};
}
}
// Paths
const paths = {};
for (const api of summary.apis) {
for (const endpoint of api.endpoints) {
if (!endpoint.path) continue;
const pathKey = endpoint.path;
if (!paths[pathKey]) paths[pathKey] = {};
const opKey = methodKey(endpoint.method);
paths[pathKey][opKey] = buildPathOperation(endpoint, api.description, dtoNames, enumNames);
}
}
return {
openapi: '3.0.3',
info: {
title: 'KIS-TOiR API',
description:
'Equipment maintenance management system. Generated from domain/toir.api.dsl via tools/api-summary-to-openapi.mjs.',
version: '1.0.0',
},
servers: [
{
url: '/api',
description: 'Default server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
schemas,
},
paths,
};
}
// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
const outIndex = args.indexOf('--out');
const outPath = outIndex !== -1 ? args[outIndex + 1] : 'openapi.json';
const summary = buildApiSummary(rootDir);
const openApiDoc = buildOpenApi(summary);
const outputPath = path.resolve(rootDir, outPath);
writeFileSync(outputPath, `${JSON.stringify(openApiDoc, null, 2)}\n`, 'utf8');
console.log(`Generated ${path.relative(rootDir, outputPath)}`);