UNPKG

@autobe/agent

Version:

AI backend server code generator

253 lines 10.7 kB
"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