import z from "zod"; import { SCAFFOLD_CODES, ScaffoldError } from "../error-codes.js"; import type { CapabilitySelection, DatabaseEngine, IsolationTier, PromptAnswers, } from "./prompt.types.js"; /** * Strict npm-package-name validation. * * Rules (RFC 1035 + npm): all lowercase, 1-214 chars, may not start with * `.` or `_`, may contain a single optional `@scope/` prefix, the body * matches `[a-z0-9][a-z0-9._-]*`. */ const NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/; const isolationSchema: z.ZodType = z.enum([ "trusted", "isolated", "sandboxed", ]); const databaseSchema: z.ZodType = z.enum([ "postgres", "sqlite", "surrealdb", "none", ]); const capabilitySchema: z.ZodType = z.object({ rest: z.boolean(), graphql: z.boolean(), ws: z.boolean(), sse: z.boolean(), sagas: z.boolean(), twoPhaseCommit: z.boolean(), perTenantJobs: z.boolean(), }); export const PromptAnswersSchema: z.ZodType = z.object({ name: z.string().min(1).max(214), isolation: isolationSchema, database: databaseSchema, capabilities: capabilitySchema, }); /** * Validate and freeze a complete prompt-answer set. * * Throws `SCAFFOLD.INVALID_NAME` when the supplied name violates * npm-package-name rules. Throws `SCAFFOLD.UNKNOWN_ENGINE` when the * database engine falls outside the closed vocabulary (defensive — the * Zod schema catches the same case, but we want the typed error for * programmatic recovery). */ export function validateAnswers(input: PromptAnswers): PromptAnswers { const parsed = PromptAnswersSchema.parse(input); if ( !NAME_RE.test(parsed.name) || parsed.name.startsWith(".") || parsed.name.startsWith("_") ) { throw new ScaffoldError( SCAFFOLD_CODES.INVALID_NAME, `scaffold.invalid_name:${parsed.name}` ); } return Object.freeze({ ...parsed, capabilities: Object.freeze({ ...parsed.capabilities }), }); } /** * Resolve the canonical capability set for a fixture shorthand. Used * primarily by tests and the snapshot fixture set, but exported because * downstream tooling occasionally wants the same shorthand → answers * mapping (e.g. for in-process orchestration). */ export function answersForFixture( fixture: | "trusted-postgres-full" | "isolated-sqlite-rest-only" | "sandboxed-none-rest-only", name: string ): PromptAnswers { if (fixture === "trusted-postgres-full") { return validateAnswers({ name, isolation: "trusted", database: "postgres", capabilities: { rest: true, graphql: true, ws: true, sse: true, sagas: true, twoPhaseCommit: true, perTenantJobs: true, }, }); } if (fixture === "isolated-sqlite-rest-only") { return validateAnswers({ name, isolation: "isolated", database: "sqlite", capabilities: { rest: true, graphql: false, ws: false, sse: false, sagas: false, twoPhaseCommit: false, perTenantJobs: false, }, }); } return validateAnswers({ name, isolation: "sandboxed", database: "none", capabilities: { rest: true, graphql: false, ws: false, sse: false, sagas: false, twoPhaseCommit: false, perTenantJobs: false, }, }); }