/** * Shared Model Contract Layer * * 单一字段定义源(Single Source of Truth)。 * 所有 agent 通过 import { createContract } from "./model-contract.mjs" 引用, * 禁止自行定义任何字段名。 * * 提供: * - entities[] — 所有实体的字段定义(PascalCase 名 + snake_case 表名) * - auth{} — 认证配置 * - permissions[] — RBAC 权限矩阵 * - fieldMapping{} — snake_case ↔ camelCase 双向映射 * - validation{} — 字段级校验规则 */ // ═══════════════════════════════════════════════════════ // 1. Domain-Specific Entity Templates // ═══════════════════════════════════════════════════════ /** * Base entity templates for common domains. * These are the canonical field definitions for every entity. * Key: domain name → Array of entity templates. */ const DOMAIN_ENTITIES = { enterprise: [ { name: "User", table: "users", description: "系统用户", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } }, { name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true }, { name: "nickname", type: "VARCHAR(128)" }, { name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] }, { name: "phone", type: "VARCHAR(20)" }, { name: "avatar_url", type: "TEXT" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [ { type: "hasMany", entity: "contracts", via: "user_id" }, { type: "hasMany", entity: "customers", via: "user_id" }, ], }, { name: "Contract", table: "contracts", description: "合同", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" }, { name: "title", type: "VARCHAR(256)", required: true }, { name: "party_a", type: "VARCHAR(128)" }, { name: "party_b", type: "VARCHAR(128)" }, { name: "amount", type: "DECIMAL(14,2)" }, { name: "signed_at", type: "DATE" }, { name: "expires_at", type: "DATE" }, { name: "status", type: "VARCHAR(32)", defaultValue: "draft", enum: ["draft", "pending", "active", "expired", "terminated"] }, { name: "file_url", type: "TEXT" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [ { type: "belongsTo", entity: "users", via: "user_id" }, ], }, { name: "Approval", table: "approvals", description: "审批流程", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" }, { name: "entity_type", type: "VARCHAR(32)", required: true }, { name: "entity_id", type: "UUID" }, { name: "applicant_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" }, { name: "status", type: "VARCHAR(32)", defaultValue: "pending", enum: ["pending", "approved", "rejected"] }, { name: "form_data", type: "JSONB" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [ { type: "belongsTo", entity: "users", via: "user_id" }, { type: "belongsTo", entity: "users", via: "applicant_id", as: "applicant" }, ], }, { name: "Customer", table: "customers", description: "客户", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" }, { name: "name", type: "VARCHAR(128)", required: true }, { name: "email", type: "VARCHAR(256)" }, { name: "phone", type: "VARCHAR(20)" }, { name: "company", type: "VARCHAR(128)" }, { name: "source", type: "VARCHAR(64)" }, { name: "tags", type: "JSONB", defaultValue: "[]" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [ { type: "belongsTo", entity: "users", via: "user_id" }, ], }, { name: "Reminder", table: "reminders", description: "到期提醒", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" }, { name: "entity_type", type: "VARCHAR(32)", required: true }, { name: "entity_id", type: "UUID" }, { name: "remind_at", type: "TIMESTAMP", required: true }, { name: "message", type: "TEXT" }, { name: "sent", type: "BOOLEAN", defaultValue: "false" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [ { type: "belongsTo", entity: "users", via: "user_id" }, ], }, ], pet: [ { name: "User", table: "users", description: "系统用户", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } }, { name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true }, { name: "nickname", type: "VARCHAR(128)" }, { name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] }, { name: "phone", type: "VARCHAR(20)" }, { name: "avatar_url", type: "TEXT" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [], }, { name: "Pet", table: "pets", description: "宠物", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" }, { name: "name", type: "VARCHAR(64)", required: true }, { name: "species", type: "VARCHAR(32)" }, { name: "breed", type: "VARCHAR(64)" }, { name: "birth_date", type: "DATE" }, { name: "weight_kg", type: "DECIMAL(5,2)" }, { name: "avatar_url", type: "TEXT" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [ { type: "belongsTo", entity: "users", via: "user_id" }, ], }, ], ecommerce: [ { name: "User", table: "users", description: "系统用户", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } }, { name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true }, { name: "nickname", type: "VARCHAR(128)" }, { name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] }, { name: "phone", type: "VARCHAR(20)" }, { name: "avatar_url", type: "TEXT" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [], }, { name: "Product", table: "products", description: "商品", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" }, { name: "title", type: "VARCHAR(256)", required: true }, { name: "description", type: "TEXT" }, { name: "price", type: "DECIMAL(10,2)" }, { name: "stock", type: "INTEGER", defaultValue: "0" }, { name: "images", type: "JSONB", defaultValue: "[]" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [ { type: "belongsTo", entity: "users", via: "user_id" }, ], }, { name: "Order", table: "orders", description: "订单", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "user_id", type: "UUID", required: true, fkEntity: "users", fkColumn: "id" }, { name: "status", type: "VARCHAR(32)", defaultValue: "pending", enum: ["pending", "paid", "shipped", "delivered", "cancelled"] }, { name: "total_amount", type: "DECIMAL(12,2)" }, { name: "address_id", type: "UUID", fkEntity: "addresses", fkColumn: "id" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [ { type: "belongsTo", entity: "users", via: "user_id" }, ], }, ], education: [ { name: "User", table: "users", description: "系统用户", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } }, { name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true }, { name: "nickname", type: "VARCHAR(128)" }, { name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin", "instructor"] }, { name: "phone", type: "VARCHAR(20)" }, { name: "avatar_url", type: "TEXT" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [], }, ], note: [ { name: "User", table: "users", description: "系统用户", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } }, { name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true }, { name: "nickname", type: "VARCHAR(128)" }, { name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] }, { name: "phone", type: "VARCHAR(20)" }, { name: "avatar_url", type: "TEXT" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [], }, ], fitness: [ { name: "User", table: "users", description: "系统用户", fields: [ { name: "id", type: "UUID", isPrimary: true, isAuto: true }, { name: "username", type: "VARCHAR(128)", required: true, unique: true, validation: { minLength: 2, maxLength: 50 } }, { name: "password_hash", type: "VARCHAR(256)", required: true, isSecret: true }, { name: "nickname", type: "VARCHAR(128)" }, { name: "role", type: "VARCHAR(32)", defaultValue: "user", enum: ["user", "admin"] }, { name: "phone", type: "VARCHAR(20)" }, { name: "avatar_url", type: "TEXT" }, { name: "created_at", type: "TIMESTAMP", isAuto: true }, { name: "updated_at", type: "TIMESTAMP", isAuto: true }, ], relationships: [], }, ], }; // ═══════════════════════════════════════════════════════ // 2. Field Mapping Engine // ═══════════════════════════════════════════════════════ /** * Build bidirectional snake_case ↔ camelCase mappings from all contract entities. * Scanned once; cached in the returned contract. */ function buildFieldMappings(entities) { const snakeToCamel = {}; const camelToSnake = {}; // Base conversions from common SQL columns const baseMappings = { snakeToCamel: { "created_at": "createdAt", "updated_at": "updatedAt", "user_id": "userId", }, camelToSnake: { "createdAt": "created_at", "updatedAt": "updated_at", "userId": "user_id", }, }; Object.assign(snakeToCamel, baseMappings.snakeToCamel); Object.assign(camelToSnake, baseMappings.camelToSnake); for (const entity of entities) { for (const field of entity.fields) { const snake = field.name; // fields are stored in snake_case // Convert to camelCase const camel = snake.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); if (snake !== camel) { snakeToCamel[snake] = camel; camelToSnake[camel] = snake; } } } // Add common patterns const known = [ ["id", "id"], ["username", "username"], ["password_hash", "passwordHash"], ["nickname", "nickname"], ["role", "role"], ["phone", "phone"], ["avatar_url", "avatarUrl"], ["title", "title"], ["description", "description"], ["status", "status"], ["data", "data"], ["amount", "amount"], ["party_a", "partyA"], ["party_b", "partyB"], ["signed_at", "signedAt"], ["expires_at", "expiresAt"], ["file_url", "fileUrl"], ["entity_type", "entityType"], ["entity_id", "entityId"], ["applicant_id", "applicantId"], ["form_data", "formData"], ["name", "name"], ["email", "email"], ["company", "company"], ["source", "source"], ["tags", "tags"], ["breed", "breed"], ["species", "species"], ["birth_date", "birthDate"], ["weight_kg", "weightKg"], ["price", "price"], ["stock", "stock"], ["images", "images"], ["total_amount", "totalAmount"], ["address_id", "addressId"], ["remind_at", "remindAt"], ["message", "message"], ["sent", "sent"], ]; for (const [snake, camel] of known) { if (!snakeToCamel[snake]) snakeToCamel[snake] = camel; if (!camelToSnake[camel]) camelToSnake[camel] = snake; } return { snakeToCamel, camelToSnake }; } // ═══════════════════════════════════════════════════════ // 3. Auth Configuration // ═══════════════════════════════════════════════════════ function buildAuthConfig(domain) { // Enterprise domain has stricter auth const isEnterprise = domain === "enterprise"; return { registrationFields: ["username", "password", "nickname"], loginFields: ["username", "password"], jwtPayload: ["userId", "username", "role"], passwordPolicy: { minLength: isEnterprise ? 8 : 6, requireSpecial: isEnterprise, }, }; } // ═══════════════════════════════════════════════════════ // 4. Permissions Matrix // ═══════════════════════════════════════════════════════ function buildPermissions(domain) { return [ { role: "admin", allow: ["*"] }, { role: "user", allow: ["read:own", "create:*", "update:own", "delete:own"] }, ]; } // ═══════════════════════════════════════════════════════ // 5. Validation Rules // ═══════════════════════════════════════════════════════ function buildValidation(entities) { const rules = {}; for (const entity of entities) { const fieldRules = {}; for (const field of entity.fields) { if (field.validation) { fieldRules[field.name] = { ...field.validation }; } // Auto-generate from field properties if (field.required || field.unique || field.enum || field.defaultValue !== undefined) { if (!fieldRules[field.name]) fieldRules[field.name] = {}; } if (field.required) { fieldRules[field.name].required = true; } if (field.unique) { fieldRules[field.name].unique = true; } if (field.enum) { fieldRules[field.name].enum = field.enum; } if (field.defaultValue !== undefined) { fieldRules[field.name].defaultValue = field.defaultValue; } } if (Object.keys(fieldRules).length > 0) { rules[entity.name] = fieldRules; } } return rules; } // ═══════════════════════════════════════════════════════ // 6. Main Contract Builder // ═══════════════════════════════════════════════════════ /** * Create a Model Contract from PRD and Architecture. * * The contract is the SINGLE SOURCE OF TRUTH for all entity field definitions. * All generators MUST read from the contract; they MUST NOT invent field names. * * @param {object} prd — PRD from SF-01 (project-intake-agent) * @param {object} arch — Architecture from SF-02 (architecture-agent), optional * @returns {object} Contract object */ export function createContract(prd, arch) { const domain = (prd && prd.domain) || (arch && arch.domain) || "generic"; const projectName = (prd && prd.projectName) || (arch && arch.projectName) || "App"; // ── Get domain-specific entities ── let entities = DOMAIN_ENTITIES[domain] || DOMAIN_ENTITIES.enterprise; // Deep clone to avoid mutation entities = JSON.parse(JSON.stringify(entities)); // ── Augment with domain-specific entities from features/APIs ── // If architecture has derived tables, merge them into the contract if (arch && arch.databaseSchema) { const existingTables = new Set(entities.map(e => e.table)); const existingNames = new Set(entities.map(e => e.name)); for (const table of arch.databaseSchema) { if (!existingTables.has(table.table) && table.table !== "users") { // Auto-generate PascalCase entity name from snake_case table const pascalName = table.table .split("_") .map(w => w.charAt(0).toUpperCase() + w.slice(1)) .join(""); if (!existingNames.has(pascalName)) { const entity = { name: pascalName, table: table.table, description: table.description || `${pascalName}表`, fields: (table.fields || []).map(f => ({ name: f.name, type: f.type || "TEXT", required: (f.constraints || "").includes("NOT NULL") || (f.constraints || "").includes("PK"), isPrimary: (f.constraints || "").includes("PK"), isAuto: f.name === "created_at" || f.name === "updated_at" || (f.constraints || "").includes("DEFAULT"), ...((f.constraints || "").includes("UNIQUE") ? { unique: true } : {}), ...((f.constraints || "").includes("FK") ? { fkEntity: (f.name.endsWith("_id") ? f.name.replace(/_id$/, "s") : "items"), fkColumn: "id", } : {}), })), relationships: [], }; // Run FK detection on fields for (const field of entity.fields) { if (field.fkEntity) { entity.relationships.push({ type: "belongsTo", entity: field.fkEntity, via: field.name, }); } } entities.push(entity); existingTables.add(table.table); existingNames.add(pascalName); } } } } // ── Build mappings ── const fieldMapping = buildFieldMappings(entities); const auth = buildAuthConfig(domain); const permissions = buildPermissions(domain); const validation = buildValidation(entities); return { projectName, domain, entities, auth, permissions, fieldMapping, validation, }; } // ═══════════════════════════════════════════════════════ // 7. Contract Utility Functions // ═══════════════════════════════════════════════════════ /** * Look up a field in the contract by entity.table and field name (snake_case). * * @param {object} contract * @param {string} tableName — snake_case table name * @param {string} fieldName — snake_case field name * @returns {object|null} Field definition */ export function lookupField(contract, tableName, fieldName) { for (const entity of contract.entities) { if (entity.table === tableName) { return entity.fields.find(f => f.name === fieldName) || null; } } return null; } /** * Get the camelCase name for a snake_case column from the contract's fieldMapping. * * @param {object} contract * @param {string} snakeName * @returns {string} */ export function toCamel(contract, snakeName) { return contract.fieldMapping.snakeToCamel[snakeName] || snakeName; } /** * Get the snake_case name for a camelCase property from the contract's fieldMapping. * * @param {object} contract * @param {string} camelName * @returns {string} */ export function toSnake(contract, camelName) { return contract.fieldMapping.camelToSnake[camelName] || camelName; } /** * Get the TypeScript type for a contract field. * * @param {object} field — Contract field definition * @returns {string} TypeScript type string */ export function tsType(field) { const t = (field.type || "").toUpperCase(); if (t.includes("INT") || t.includes("SERIAL") || t.includes("BIGINT") || t.includes("DECIMAL") || t.includes("NUMERIC") || t.includes("FLOAT") || t.includes("DOUBLE") || t.includes("REAL")) return "number"; if (t.includes("BOOL")) return "boolean"; if (t.includes("JSONB") || t.includes("JSON")) return "Record"; return "string"; } /** * Get the SQLite column type for a contract field. * * @param {object} field — Contract field definition * @returns {string} SQLite type */ export function sqliteType(field) { const t = (field.type || "").toUpperCase(); if (t.includes("UUID") || t.includes("TEXT") || t.includes("VARCHAR") || t.includes("CHAR")) return "TEXT"; if (t.includes("INT") || t.includes("SERIAL") || t.includes("BIGINT")) return "INTEGER"; if (t.includes("DECIMAL") || t.includes("NUMERIC") || t.includes("FLOAT") || t.includes("DOUBLE") || t.includes("REAL")) return "REAL"; if (t.includes("BOOL")) return "INTEGER"; if (t.includes("DATE") || t.includes("TIME") || t.includes("TIMESTAMP")) return "TEXT"; if (t.includes("JSONB") || t.includes("JSON")) return "TEXT"; return "TEXT"; } /** * Get the non-auto fields for CreateInput (exclude id, created_at, updated_at). * * @param {object} entity — Contract entity * @returns {object[]} Fields for CreateInput */ export function createInputFields(entity) { return entity.fields.filter(f => !f.isPrimary && !f.isAuto && f.name !== "created_at" && f.name !== "updated_at" ); } /** * Get the non-identity fields for UpdateInput (exclude id, created_at, updated_at). * * @param {object} entity — Contract entity * @returns {object[]} Fields for UpdateInput */ export function updateInputFields(entity) { return createInputFields(entity); } /** * Check if an entity has the given field (by snake_case name). */ export function hasField(entity, snakeName) { return entity.fields.some(f => f.name === snakeName); } // ═══════════════════════════════════════════════════════ // 8. Export DOMAIN_ENTITIES for introspection // ═══════════════════════════════════════════════════════ export { DOMAIN_ENTITIES };