UNPKG

@autobe/agent

Version:

AI backend server code generator

305 lines (274 loc) 9.25 kB
import { AutoBeAnalyze, AutoBeAnalyzeWriteSectionEvent, } from "@autobe/interface"; import YAML from "yaml"; // ─── YAML-based Error Code Canonical Registry ─── /** * A single canonical error code entry extracted from a YAML spec block in * 04-business-rules. */ export interface IErrorCodeRegistryEntry { /** Error code, e.g. "TODO_NOT_FOUND" */ code: string; /** HTTP status code, e.g. 404 */ http: number; /** Condition description */ condition: string; } /** * A reference to an error code found via backtick pattern in non-canonical * files. */ export interface IErrorCodeReference { code: string; fileIndex: number; sectionTitle: string; } /** Result of comparing canonical error code definitions to backtick references. */ export interface IErrorCodeValidationResult { /** All canonical error codes from 04-business-rules YAML blocks */ canonical: IErrorCodeRegistryEntry[]; /** All backtick error code references in other files */ references: IErrorCodeReference[]; /** References that don't match any canonical definition */ undefinedReferences: IErrorCodeReference[]; /** YAML parse errors */ parseErrors: Array<{ fileIndex: number; sectionTitle: string; error: string; }>; } export interface IErrorCodeConflict { /** Normalized condition key */ conditionKey: string; /** Different error codes for the same condition */ codes: Array<{ errorCode: string; httpStatus: number; files: string[]; }>; } // ─── YAML Block Extraction ─── const YAML_CODE_BLOCK_REGEX = /```yaml\n([\s\S]*?)```/g; /** * Extract canonical error codes from 04-business-rules YAML spec blocks. * * Expects YAML blocks with structure: * * ```yaml * errors: * - code: TODO_NOT_FOUND * http: 404 * condition: "requested todo does not exist" * ``` */ const extractCanonicalErrorCodes = ( fileIndex: number, sectionEvents: AutoBeAnalyzeWriteSectionEvent[][], ): { entries: IErrorCodeRegistryEntry[]; errors: Array<{ fileIndex: number; sectionTitle: string; error: string }>; } => { const entries: IErrorCodeRegistryEntry[] = []; const errors: Array<{ fileIndex: number; sectionTitle: string; error: string; }> = []; for (const sectionsForModule of sectionEvents) { for (const sectionEvent of sectionsForModule) { for (const section of sectionEvent.sectionSections) { const yamlMatches = section.content.matchAll(YAML_CODE_BLOCK_REGEX); for (const match of yamlMatches) { const yamlContent = match[1] ?? ""; try { const parsed = YAML.parse(yamlContent); if ( parsed && typeof parsed === "object" && Array.isArray(parsed.errors) ) { for (const err of parsed.errors) { if (err && typeof err.code === "string") { entries.push({ code: err.code, http: typeof err.http === "number" ? err.http : 400, condition: String(err.condition ?? ""), }); } } } } catch (e) { errors.push({ fileIndex, sectionTitle: section.title, error: `YAML parse error: ${e instanceof Error ? e.message : String(e)}`, }); } } } } } return { entries, errors }; }; // ─── Backtick Reference Extraction ─── /** Match backtick `UPPER_SNAKE_CASE` patterns (error codes) */ const BACKTICK_ERROR_CODE_REGEX = /`([A-Z][A-Z0-9_]+)`/g; /** Extract backtick `ERROR_CODE` references from section content. */ const extractBacktickErrorCodeReferences = ( fileIndex: number, sectionEvents: AutoBeAnalyzeWriteSectionEvent[][], ): IErrorCodeReference[] => { const refs: IErrorCodeReference[] = []; for (const sectionsForModule of sectionEvents) { for (const sectionEvent of sectionsForModule) { for (const section of sectionEvent.sectionSections) { const matches = section.content.matchAll(BACKTICK_ERROR_CODE_REGEX); for (const match of matches) { // Skip common non-error-code patterns (HTTP methods, etc.) const code = match[1]!; if ( code === "GET" || code === "POST" || code === "PUT" || code === "PATCH" || code === "DELETE" || code === "HEAD" || code === "OPTIONS" ) continue; refs.push({ code, fileIndex, sectionTitle: section.title, }); } } } } return refs; }; // ─── Main Validation Function ─── /** * Validate error code references across files using YAML canonical definitions. * * 1. Extracts canonical error codes from 04-business-rules YAML blocks * 2. Extracts backtick `ERROR_CODE` references from 03-functional-requirements * 3. Reports undefined references */ export const validateErrorCodes = (props: { files: Array<{ file: AutoBeAnalyze.IFileScenario; sectionEvents: AutoBeAnalyzeWriteSectionEvent[][]; }>; }): IErrorCodeValidationResult => { let canonical: IErrorCodeRegistryEntry[] = []; const parseErrors: IErrorCodeValidationResult["parseErrors"] = []; // Extract canonical from 04-business-rules const businessRulesIndex = props.files.findIndex( (f) => f.file.filename === "04-business-rules.md", ); if (businessRulesIndex >= 0) { const result = extractCanonicalErrorCodes( businessRulesIndex, props.files[businessRulesIndex]!.sectionEvents, ); canonical = result.entries; parseErrors.push(...result.errors); } // Build canonical lookup set const canonicalSet = new Set(canonical.map((e) => e.code)); // Extract references from 03-functional-requirements const references: IErrorCodeReference[] = []; for (let i = 0; i < props.files.length; i++) { const filename = props.files[i]!.file.filename; if (filename === "03-functional-requirements.md") { references.push( ...extractBacktickErrorCodeReferences(i, props.files[i]!.sectionEvents), ); } } const undefinedReferences = references.filter( (ref) => !canonicalSet.has(ref.code), ); return { canonical, references, undefinedReferences, parseErrors }; }; /** * Detect error code conflicts across files. * * Now operates on YAML-extracted data. A conflict occurs when the same error * code appears with different HTTP statuses across YAML blocks. */ export const detectErrorCodeConflicts = (props: { files: Array<{ file: AutoBeAnalyze.IFileScenario; sectionEvents: AutoBeAnalyzeWriteSectionEvent[][]; }>; }): IErrorCodeConflict[] => { // code → { http → Set<filename> } const codeMap: Map<string, Map<number, Set<string>>> = new Map(); for (const { file, sectionEvents } of props.files) { for (const sectionsForModule of sectionEvents) { for (const sectionEvent of sectionsForModule) { for (const section of sectionEvent.sectionSections) { const yamlMatches = section.content.matchAll(YAML_CODE_BLOCK_REGEX); for (const match of yamlMatches) { const yamlContent = match[1] ?? ""; try { const parsed = YAML.parse(yamlContent); if ( parsed && typeof parsed === "object" && Array.isArray(parsed.errors) ) { for (const err of parsed.errors) { if (!err || typeof err.code !== "string") continue; const code = err.code; const http = typeof err.http === "number" ? err.http : 400; if (!codeMap.has(code)) codeMap.set(code, new Map()); const httpMap = codeMap.get(code)!; if (!httpMap.has(http)) httpMap.set(http, new Set()); httpMap.get(http)!.add(file.filename); } } } catch { // skip parse errors } } } } } } return [...codeMap.entries()] .filter(([, httpMap]) => httpMap.size > 1) .map(([code, httpMap]) => ({ conditionKey: code, codes: [...httpMap.entries()].map(([http, files]) => ({ errorCode: code, httpStatus: http, files: [...files], })), })); }; /** Build a map from filename → list of error code conflict feedback strings. */ export const buildFileErrorCodeConflictMap = ( conflicts: IErrorCodeConflict[], ): Map<string, string[]> => { const map: Map<string, string[]> = new Map(); for (const conflict of conflicts) { const allFiles = new Set(conflict.codes.flatMap((c) => c.files)); const feedback = `Error code conflict for "${conflict.conditionKey}": ` + conflict.codes .map((c) => `HTTP ${c.httpStatus} in [${c.files.join(", ")}]`) .join(" vs ") + `. Use ONE canonical HTTP status.`; for (const filename of allFiles) { if (!map.has(filename)) map.set(filename, []); map.get(filename)!.push(feedback); } } return map; };