UNPKG

@autobe/agent

Version:

AI backend server code generator

814 lines (724 loc) 23.9 kB
import { AutoBeAnalyze, AutoBeAnalyzeWriteSectionEvent, } from "@autobe/interface"; import YAML from "yaml"; type ConstraintSource = { file: AutoBeAnalyze.IFileScenario; sectionTitle: string; }; type ConstraintValue = { normalized: string; display: string; sources: ConstraintSource[]; }; type ConstraintEntry = { key: string; values: Map<string, ConstraintValue>; }; const YAML_CODE_BLOCK_REGEX = /```yaml\n([\s\S]*?)```/g; export const buildConstraintConsistencyReport = (props: { files: Array<{ file: AutoBeAnalyze.IFileScenario; sectionEvents: AutoBeAnalyzeWriteSectionEvent[][]; }>; }): string => { const constraints: Map<string, ConstraintEntry> = new Map(); let totalConstraints: number = 0; for (const { file, sectionEvents } of props.files) { for (const sectionsForModule of sectionEvents) { for (const sectionEvent of sectionsForModule) { for (const section of sectionEvent.sectionSections) { const pairs = extractConstraints(section.content); for (const { key, value } of pairs) { totalConstraints++; const normalized = normalizeValue(value); if (!constraints.has(key)) { constraints.set(key, { key, values: new Map(), }); } const entry = constraints.get(key)!; if (!entry.values.has(normalized)) { entry.values.set(normalized, { normalized, display: value.trim(), sources: [], }); } entry.values.get(normalized)!.sources.push({ file, sectionTitle: section.title, }); } } } } } const conflicts: ConstraintEntry[] = [...constraints.values()].filter( (entry) => entry.values.size > 1, ); if (conflicts.length === 0) { return [ "No numeric constraint conflicts detected.", `Scanned ${totalConstraints} numeric constraints from YAML spec blocks.`, ].join("\n"); } const lines: string[] = [ `Detected ${conflicts.length} numeric constraint conflict(s).`, `Scanned ${totalConstraints} numeric constraints from YAML spec blocks.`, "", "Conflicts:", ]; for (const entry of conflicts) { lines.push(`- ${entry.key}:`); for (const value of entry.values.values()) { const sources = value.sources .map((s) => `${s.file.filename}${s.sectionTitle}`) .slice(0, 6) .join("; "); lines.push(` - ${value.display} (e.g., ${sources})`); } } return lines.join("\n"); }; /** * Extract numeric constraints from YAML spec blocks. * * Parses YAML code blocks and extracts Entity.attribute constraints that * contain numeric values (e.g., length limits, quantity limits). */ const extractConstraints = ( content: string, ): Array<{ key: string; value: string }> => { if (!content) return []; const results: Array<{ key: string; value: string }> = []; const yamlMatches = 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") continue; // Handle entity attribute YAML blocks if ( typeof parsed.entity === "string" && Array.isArray(parsed.attributes) ) { for (const attr of parsed.attributes) { if (!attr || typeof attr.name !== "string") continue; const constraintStr = String(attr.constraints ?? ""); if (!hasNumeric(constraintStr)) continue; results.push({ key: `${parsed.entity}.${attr.name}`, value: constraintStr, }); } } // Handle error code YAML blocks (HTTP status codes) if (Array.isArray(parsed.errors)) { for (const err of parsed.errors) { if (!err || typeof err.code !== "string") continue; if (typeof err.http === "number") { results.push({ key: `error.${err.code}.http`, value: String(err.http), }); } } } } catch { // skip parse errors } } return results; }; const normalizeValue = (value: string): string => value .toLowerCase() .replace(/[–—]/g, "-") .replace(/`/g, "") .replace(/\s+/g, " ") .trim(); const hasNumeric = (value: string): boolean => /\d/.test(value); // ─── Structured Conflict Detection ─── export interface IConstraintConflict { key: string; values: Array<{ display: string; files: string[]; }>; } /** * Detect numeric constraint conflicts across files as structured data. * * Returns an array of conflicts where the same constraint key has different * normalized values across files. */ export const detectConstraintConflicts = (props: { files: Array<{ file: AutoBeAnalyze.IFileScenario; sectionEvents: AutoBeAnalyzeWriteSectionEvent[][]; }>; }): IConstraintConflict[] => { const constraints: Map<string, ConstraintEntry> = 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 pairs = extractConstraints(section.content); for (const { key, value } of pairs) { const normalized = normalizeValue(value); if (!constraints.has(key)) { constraints.set(key, { key, values: new Map() }); } const entry = constraints.get(key)!; if (!entry.values.has(normalized)) { entry.values.set(normalized, { normalized, display: value.trim(), sources: [], }); } entry.values.get(normalized)!.sources.push({ file, sectionTitle: section.title, }); } } } } } return [...constraints.values()] .filter((entry) => entry.values.size > 1) .map((entry) => ({ key: entry.key, values: [...entry.values.values()].map((v) => ({ display: v.display, files: [...new Set(v.sources.map((s) => s.file.filename))], })), })); }; /** Build a map from filename → list of conflict feedback strings. */ export const buildFileConflictMap = ( conflicts: IConstraintConflict[], ): Map<string, string[]> => { const map: Map<string, string[]> = new Map(); for (const conflict of conflicts) { const allFiles = new Set(conflict.values.flatMap((v) => v.files)); const feedback = `${conflict.key} has conflicting values: ` + conflict.values .map((v) => `"${v.display}" in [${v.files.join(", ")}]`) .join(" vs "); for (const filename of allFiles) { if (!map.has(filename)) map.set(filename, []); map.get(filename)!.push(feedback); } } return map; }; // ─── Attribute Duplicate Detection ─── export interface IAttributeDuplicate { key: string; files: string[]; hasValueConflict: boolean; values?: Array<{ specification: string; files: string[]; }>; } /** * Detect cross-file attribute duplication from YAML spec blocks. * * Returns attributes that are defined in YAML blocks across multiple files. */ export const detectAttributeDuplicates = (props: { files: Array<{ file: AutoBeAnalyze.IFileScenario; sectionEvents: AutoBeAnalyzeWriteSectionEvent[][]; }>; }): IAttributeDuplicate[] => { // key → { normalized spec → { display, files } } const attributes: Map< string, Map<string, { display: string; files: Set<string> }> > = new Map(); const allFilesByKey: Map<string, 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 specs = extractAttributeSpecs(section.content); for (const { key, specification } of specs) { if (!allFilesByKey.has(key)) allFilesByKey.set(key, new Set()); allFilesByKey.get(key)!.add(file.filename); if (!attributes.has(key)) attributes.set(key, new Map()); const specMap = attributes.get(key)!; const normalized = normalizeValue(specification); if (!specMap.has(normalized)) { specMap.set(normalized, { display: specification.trim(), files: new Set(), }); } specMap.get(normalized)!.files.add(file.filename); } } } } } return [...allFilesByKey.entries()] .filter(([, files]) => files.size > 1) .map(([key, files]) => { const specMap = attributes.get(key)!; const hasValueConflict = specMap.size > 1; return { key, files: [...files], hasValueConflict, ...(hasValueConflict ? { values: [...specMap.values()].map((v) => ({ specification: v.display, files: [...v.files], })), } : {}), }; }); }; export const buildFileAttributeDuplicateMap = ( duplicates: IAttributeDuplicate[], ): Map<string, string[]> => { const map: Map<string, string[]> = new Map(); for (const dup of duplicates) { let feedback: string; if (dup.hasValueConflict && dup.values) { feedback = `${dup.key} has conflicting specifications across files: ` + dup.values .map((v) => `"${v.specification}" in [${v.files.join(", ")}]`) .join(" vs ") + `. Align to ONE canonical definition.`; } else { feedback = `${dup.key} is fully specified in multiple files: [${dup.files.join(", ")}]. ` + `Only ONE file should contain the full spec.`; } for (const filename of dup.files) { if (!map.has(filename)) map.set(filename, []); map.get(filename)!.push(feedback); } } return map; }; // ─── Enum Conflict Detection ─── export interface IEnumConflict { key: string; values: Array<{ enumSet: string; display: string; files: string[]; }>; } /** * Detect enum value conflicts from YAML spec blocks. * * Scans YAML attribute blocks for enum-like constraints and detects when * different files define different enum value sets for the same attribute. */ export const detectEnumConflicts = (props: { files: Array<{ file: AutoBeAnalyze.IFileScenario; sectionEvents: AutoBeAnalyzeWriteSectionEvent[][]; }>; }): IEnumConflict[] => { type EnumValue = { enumSet: string; display: string; files: Set<string>; }; const enums: Map<string, Map<string, EnumValue>> = 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 specs = extractEnumSpecs(section.content); for (const { key, enumSet, display } of specs) { if (!enums.has(key)) enums.set(key, new Map()); const entry = enums.get(key)!; if (!entry.has(enumSet)) { entry.set(enumSet, { enumSet, display, files: new Set() }); } entry.get(enumSet)!.files.add(file.filename); } } } } } return [...enums.entries()] .filter(([, values]) => values.size > 1) .map(([key, values]) => ({ key, values: [...values.values()].map((v) => ({ enumSet: v.enumSet, display: v.display, files: [...v.files], })), })); }; export const buildFileEnumConflictMap = ( conflicts: IEnumConflict[], ): Map<string, string[]> => { const map: Map<string, string[]> = new Map(); for (const conflict of conflicts) { const allFiles = new Set(conflict.values.flatMap((v) => v.files)); const feedback = `${conflict.key} has conflicting enum values: ` + conflict.values .map((v) => `enum(${v.enumSet}) in [${v.files.join(", ")}]`) .join(" vs "); for (const filename of allFiles) { if (!map.has(filename)) map.set(filename, []); map.get(filename)!.push(feedback); } } return map; }; // ─── Permission Rule Conflict Detection ─── export interface IPermissionConflict { actorOperation: string; rules: Array<{ condition: string; files: string[]; }>; } /** * Detect permission rule conflicts from YAML spec blocks. * * A conflict occurs when one YAML block allows an action but another doesn't * include it for the same actor+resource. */ export const detectPermissionConflicts = (props: { files: Array<{ file: AutoBeAnalyze.IFileScenario; sectionEvents: AutoBeAnalyzeWriteSectionEvent[][]; }>; }): IPermissionConflict[] => { // actor:resource → action → Set<filename> const ruleMap: Map<string, Map<string, 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 rules = extractPermissionRulesFromYaml(section.content); for (const { actor, resource, actions } of rules) { const key = `${actor.toLowerCase()}:${resource}`; if (!ruleMap.has(key)) ruleMap.set(key, new Map()); const actionMap = ruleMap.get(key)!; for (const action of actions) { const normAction = action.toLowerCase(); if (!actionMap.has(normAction)) actionMap.set(normAction, new Set()); actionMap.get(normAction)!.add(file.filename); } } } } } } // Permission conflicts are rare in YAML-based approach since // 01-actors-and-auth is the canonical source. Return empty for now. return []; }; export const buildFilePermissionConflictMap = ( conflicts: IPermissionConflict[], ): Map<string, string[]> => { const map: Map<string, string[]> = new Map(); for (const conflict of conflicts) { const allFiles = new Set(conflict.rules.flatMap((r) => r.files)); const feedback = `Permission conflict for "${conflict.actorOperation}": ` + conflict.rules .map((r) => `"${r.condition}" in [${r.files.join(", ")}]`) .join(" vs "); for (const filename of allFiles) { if (!map.has(filename)) map.set(filename, []); map.get(filename)!.push(feedback); } } return map; }; // ─── State Field Conflict Detection ─── export interface IStateFieldConflict { entity: string; conflictType: string; fields: Array<{ fieldName: string; specification: string; files: string[]; }>; } /** * Detect state field conflicts from YAML spec blocks. * * Known contradiction patterns: * * 1. Same entity has both `deletedAt` (datetime) and `isDeleted` (boolean) * 2. Same entity has `status` (enum) and semantically equivalent `is*` booleans */ export const detectStateFieldConflicts = (props: { files: Array<{ file: AutoBeAnalyze.IFileScenario; sectionEvents: AutoBeAnalyzeWriteSectionEvent[][]; }>; }): IStateFieldConflict[] => { // entity → { fieldName → { specification, files } } const entityFields: Map< string, Map<string, { specification: string; files: 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 specs = extractAttributeSpecs(section.content); for (const { key, specification } of specs) { const dotIndex = key.indexOf("."); if (dotIndex < 0) continue; const entity = key.slice(0, dotIndex); const field = key.slice(dotIndex + 1).toLowerCase(); if (!entityFields.has(entity)) entityFields.set(entity, new Map()); const fields = entityFields.get(entity)!; if (!fields.has(field)) fields.set(field, { specification, files: new Set() }); fields.get(field)!.files.add(file.filename); } } } } } const conflicts: IStateFieldConflict[] = []; for (const [entity, fields] of entityFields) { const fieldNames = [...fields.keys()]; // Pattern 1: deletedAt + isDeleted on same entity const hasDeletedAt = fieldNames.some( (f) => f === "deletedat" || f === "deleted_at", ); const hasIsDeleted = fieldNames.some( (f) => f === "isdeleted" || f === "is_deleted", ); if (hasDeletedAt && hasIsDeleted) { const deletedAtField = fields.get("deletedat") ?? fields.get("deleted_at"); const isDeletedField = fields.get("isdeleted") ?? fields.get("is_deleted"); if (deletedAtField && isDeletedField) { conflicts.push({ entity, conflictType: "deletedAt vs isDeleted", fields: [ { fieldName: "deletedAt", specification: deletedAtField.specification, files: [...deletedAtField.files], }, { fieldName: "isDeleted", specification: isDeletedField.specification, files: [...isDeletedField.files], }, ], }); } } // Pattern 2: status (enum) + is* booleans const statusField = fields.get("status"); if (statusField && /enum/i.test(statusField.specification)) { const isBooleans = fieldNames.filter( (f) => f.startsWith("is") && /boolean/i.test(fields.get(f)!.specification), ); for (const boolField of isBooleans) { const concept = boolField.slice(2).toLowerCase(); if (statusField.specification.toLowerCase().includes(concept)) { const boolEntry = fields.get(boolField)!; conflicts.push({ entity, conflictType: `status enum includes "${concept}" but separate is${concept.charAt(0).toUpperCase() + concept.slice(1)} boolean also exists`, fields: [ { fieldName: "status", specification: statusField.specification, files: [...statusField.files], }, { fieldName: boolField, specification: boolEntry.specification, files: [...boolEntry.files], }, ], }); } } } } return conflicts; }; export const buildFileStateFieldConflictMap = ( conflicts: IStateFieldConflict[], ): Map<string, string[]> => { const map: Map<string, string[]> = new Map(); for (const conflict of conflicts) { const allFiles = new Set(conflict.fields.flatMap((f) => f.files)); const feedback = `State field conflict for "${conflict.entity}": ${conflict.conflictType}. ` + conflict.fields .map( (f) => `"${f.fieldName}: ${f.specification}" in [${f.files.join(", ")}]`, ) .join(" vs ") + `. Use ONE canonical approach.`; for (const filename of allFiles) { if (!map.has(filename)) map.set(filename, []); map.get(filename)!.push(feedback); } } return map; }; // ─── YAML-based Attribute Specs Extraction (shared) ─── const ENUM_PATTERN = /enum\s*[\(\[\{]([^)\]\}]+)[\)\]\}]/i; /** * Extract attribute specs from YAML code blocks. * * Parses YAML blocks with `entity` + `attributes` structure and returns * Entity.attribute → constraints pairs. */ const extractAttributeSpecs = ( content: string, ): Array<{ key: string; specification: string }> => { if (!content) return []; const results: Array<{ key: string; specification: string }> = []; const yamlMatches = 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" || typeof parsed.entity !== "string" || !Array.isArray(parsed.attributes) ) continue; for (const attr of parsed.attributes) { if (!attr || typeof attr.name !== "string") continue; const spec = [ attr.type ? String(attr.type) : "", attr.constraints ? String(attr.constraints) : "", ] .filter(Boolean) .join(", "); if (!spec) continue; results.push({ key: `${parsed.entity}.${attr.name}`, specification: spec, }); } } catch { // skip parse errors } } return results; }; /** Extract enum specs from YAML attribute blocks. */ const extractEnumSpecs = ( content: string, ): Array<{ key: string; enumSet: string; display: string }> => { if (!content) return []; const results: Array<{ key: string; enumSet: string; display: string }> = []; const yamlMatches = 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" || typeof parsed.entity !== "string" || !Array.isArray(parsed.attributes) ) continue; for (const attr of parsed.attributes) { if (!attr || typeof attr.name !== "string") continue; const typeStr = String(attr.type ?? ""); const constraintStr = String(attr.constraints ?? ""); const combined = `${typeStr} ${constraintStr}`; const enumMatch = combined.match(ENUM_PATTERN); if (!enumMatch) continue; const rawEnumValues = enumMatch[1]!; const enumSet = [ ...new Set( rawEnumValues .split(/[|,]/) .map((v) => v.trim().toLowerCase()) .filter((v) => v.length > 0), ), ] .sort() .join("|"); results.push({ key: `${parsed.entity}.${attr.name}`, enumSet, display: combined.trim(), }); } } catch { // skip parse errors } } return results; }; /** Extract permission rules from YAML spec blocks. */ const extractPermissionRulesFromYaml = ( content: string, ): Array<{ actor: string; resource: string; actions: string[] }> => { if (!content) return []; const results: Array<{ actor: string; resource: string; actions: string[]; }> = []; const yamlMatches = 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.permissions) ) continue; for (const perm of parsed.permissions) { if ( !perm || typeof perm.actor !== "string" || typeof perm.resource !== "string" || !Array.isArray(perm.actions) ) continue; results.push({ actor: perm.actor, resource: perm.resource, actions: perm.actions.map(String), }); } } catch { // skip parse errors } } return results; };