@autobe/agent
Version:
AI backend server code generator
253 lines • 10.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildFileProseConflictMap = exports.detectProseConstraintConflicts = void 0;
const yaml_1 = __importDefault(require("yaml"));
// ─── Constants ───
const YAML_CODE_BLOCK_REGEX = /```yaml\n[\s\S]*?```/g;
const CANONICAL_FILENAME = "02-domain-model.md";
/**
* Numeric constraint patterns found in prose text. Matches: "300 characters",
* "1-50 characters", "1–150 characters", "up to 2000 characters", "maximum 500
* chars", "minimum 8 characters", "exceeds 300 characters", "at least 1
* character", "at most 200 characters".
*/
const NUMERIC_PATTERNS = [
// Range: "1-50 characters", "1–150 characters", "0–300 characters"
/(\d+)\s*[–\-]\s*(\d+)\s*(?:characters|chars?|unicode characters)/gi,
// Single number with unit: "300 characters", "2000 characters"
/(?:up to|maximum|max|at most|no more than|exceeds?|at least|minimum|min|no less than)\s+(\d+)\s*(?:characters|chars?|unicode characters)/gi,
// Plain: "N characters" (when preceded by constraint-like context)
/(?:limited to|restricted to|capped at|allow(?:s|ed)?)\s+(\d+)\s*(?:characters|chars?|unicode characters)/gi,
];
// ─── Helpers ───
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// ─── Canonical Registry ───
/**
* Build a map of Entity.attribute → canonical numeric values from
* 02-domain-model YAML blocks.
*/
function buildCanonicalNumericRegistry(canonicalFile) {
var _a, _b;
const registry = new Map();
for (const sectionsForModule of canonicalFile.sectionEvents) {
for (const sectionEvent of sectionsForModule) {
for (const section of sectionEvent.sectionSections) {
const yamlMatches = section.content.matchAll(/```yaml\n([\s\S]*?)```/g);
for (const match of yamlMatches) {
const yamlContent = (_a = match[1]) !== null && _a !== void 0 ? _a : "";
try {
const parsed = yaml_1.default.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 constraintStr = String((_b = attr.constraints) !== null && _b !== void 0 ? _b : "");
const numbers = extractAllNumbers(constraintStr);
if (numbers.length === 0)
continue;
const key = `${parsed.entity}.${attr.name}`;
registry.set(key, numbers);
}
}
catch (_c) {
// skip parse errors
}
}
}
}
}
return registry;
}
/**
* Build a reverse index: attribute name → list of Entity.attribute keys. e.g.,
* "bio" → ["User.bio"], "title" → ["Article.title", "Todo.title"]
*/
function buildAttributeNameIndex(registry) {
const index = new Map();
for (const key of registry.keys()) {
const dotIdx = key.indexOf(".");
if (dotIdx < 0)
continue;
const attrName = key.slice(dotIdx + 1);
if (!index.has(attrName))
index.set(attrName, []);
index.get(attrName).push(key);
}
return index;
}
/**
* Extract all integer numbers from a constraint string. "1-50, required" → [1,
* 50] "optional, maximum 2000 characters, may be null" → [2000]
*/
function extractAllNumbers(value) {
const nums = new Set();
const matches = value.matchAll(/\d+/g);
for (const m of matches) {
const n = parseInt(m[0], 10);
if (!isNaN(n))
nums.add(n);
}
return [...nums];
}
/**
* Value-driven prose constraint extraction.
*
* Instead of finding backtick references first, this approach:
*
* 1. Finds lines with numeric constraint patterns ("N characters", etc.)
* 2. Checks if any canonical attribute name appears on that line
* 3. Compares the numbers against canonical values
*
* This catches all patterns regardless of backtick usage:
*
* - `User.bio`: 0-300 characters
* - `bio` (0-500 chars)
* - Bio text limited to 300 characters
* - | bio | 0-300 chars |
*/
function extractProseConstraintMentions(proseContent, attrNameIndex, registry) {
const results = [];
const lines = proseContent.split("\n");
for (const line of lines) {
// Step 1: Extract constraint-like numbers from this line
const numbers = extractConstraintNumbers(line);
if (numbers.length === 0)
continue;
// Step 2: Check if any canonical attribute name appears on this line
for (const [attrName, entityAttrs] of attrNameIndex) {
const attrPattern = new RegExp(`\\b${escapeRegExp(attrName)}\\b`, "i");
if (!attrPattern.test(line))
continue;
// Step 3: Union all canonical values for all possible Entity.attr matches
const allCanonical = new Set();
for (const ea of entityAttrs) {
const vals = registry.get(ea);
if (vals)
for (const v of vals)
allCanonical.add(v);
}
// Step 4: Find numbers that don't match any canonical value
const conflicting = numbers.filter((n) => !allCanonical.has(n) && n !== 0);
if (conflicting.length === 0)
continue;
results.push({
entityAttr: entityAttrs[0],
numbers,
context: line.trim().slice(0, 200),
});
}
}
// Deduplicate: same entityAttr + same numbers → keep first
const seen = new Map();
for (const mention of results) {
const key = `${mention.entityAttr}:${mention.numbers.sort((a, b) => a - b).join(",")}`;
if (!seen.has(key))
seen.set(key, mention);
}
return [...seen.values()];
}
/**
* Extract numbers from constraint-like patterns in text. Only extracts numbers
* that appear in constraint context (near "characters", etc.).
*/
function extractConstraintNumbers(text) {
const numbers = new Set();
for (const pattern of NUMERIC_PATTERNS) {
pattern.lastIndex = 0;
const matches = text.matchAll(pattern);
for (const m of matches) {
if (m[1]) {
const n = parseInt(m[1], 10);
if (!isNaN(n))
numbers.add(n);
}
if (m[2]) {
const n = parseInt(m[2], 10);
if (!isNaN(n))
numbers.add(n);
}
}
}
return [...numbers];
}
// ─── Main Detection ───
/**
* Detect prose-level constraint value conflicts between non-canonical files and
* the canonical 02-domain-model.
*
* Uses a value-driven approach: builds a reverse index of canonical attribute
* names, then scans prose text for those names near numeric constraint
* patterns. Catches all patterns regardless of backtick usage.
*/
const detectProseConstraintConflicts = (props) => {
// Find canonical file (02-domain-model.md)
const canonicalFile = props.files.find((f) => f.file.filename === CANONICAL_FILENAME);
if (!canonicalFile)
return [];
const registry = buildCanonicalNumericRegistry(canonicalFile);
if (registry.size === 0)
return [];
const attrNameIndex = buildAttributeNameIndex(registry);
const conflicts = [];
for (const { file, sectionEvents } of props.files) {
// Skip canonical file itself
if (file.filename === CANONICAL_FILENAME)
continue;
for (const sectionsForModule of sectionEvents) {
for (const sectionEvent of sectionsForModule) {
for (const section of sectionEvent.sectionSections) {
// Strip YAML code blocks — those are handled by existing validators
const proseContent = section.content.replace(YAML_CODE_BLOCK_REGEX, "");
const mentions = extractProseConstraintMentions(proseContent, attrNameIndex, registry);
for (const mention of mentions) {
const canonicalValues = registry.get(mention.entityAttr);
if (!canonicalValues)
continue;
// Check if prose values conflict with canonical
const conflictingValues = mention.numbers.filter((n) => !canonicalValues.includes(n) && n !== 0);
if (conflictingValues.length === 0)
continue;
conflicts.push({
entityAttr: mention.entityAttr,
canonicalValues,
proseValues: mention.numbers,
file: file.filename,
sectionTitle: section.title,
context: mention.context,
});
}
}
}
}
}
return conflicts;
};
exports.detectProseConstraintConflicts = detectProseConstraintConflicts;
/**
* Build a map from filename → list of prose conflict feedback strings. Only
* non-canonical files appear in the map.
*/
const buildFileProseConflictMap = (conflicts) => {
const map = new Map();
for (const conflict of conflicts) {
const feedback = `Prose constraint conflict: ${conflict.entityAttr} — ` +
`canonical values [${conflict.canonicalValues.join(", ")}] (from ${CANONICAL_FILENAME}) vs ` +
`prose values [${conflict.proseValues.join(", ")}] in "${conflict.sectionTitle}". ` +
`Remove the restated value and use a backtick reference to ${CANONICAL_FILENAME} instead.`;
if (!map.has(conflict.file))
map.set(conflict.file, []);
map.get(conflict.file).push(feedback);
}
return map;
};
exports.buildFileProseConflictMap = buildFileProseConflictMap;
//# sourceMappingURL=detectProseConstraintConflicts.js.map