@autobe/agent
Version:
AI backend server code generator
814 lines (724 loc) • 23.9 kB
text/typescript
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;
};