|
| 1 | +// Plan-template library (pure). |
| 2 | +// |
| 3 | +// Reusable plan TEMPLATES for the fixed miner lifecycle (discover -> analyze -> plan -> prepare -> create -> |
| 4 | +// manage -> repeat), emitted in the exact stateless raw-step shape the MCP `gittensory_build_plan` tool accepts |
| 5 | +// (`rawPlanStepSchema` in src/mcp/server.ts), so `build_plan` can normalize them into a validated DAG. Each builder |
| 6 | +// is deterministic and side-effect-free: it only DESCRIBES steps and their `dependsOn` ordering — it never actuates |
| 7 | +// anything. The `RawPlanStep` type below mirrors the raw-step schema so the engine package stays standalone and |
| 8 | +// does not import the app's Zod schema (the tests validate the output against the real schema to guard drift). |
| 9 | + |
| 10 | +// Mirror of `rawPlanStepSchema` (src/mcp/server.ts): the pre-normalization step shape `gittensory_build_plan` accepts. |
| 11 | +export type RawPlanStep = { |
| 12 | + id: string; |
| 13 | + title: string; |
| 14 | + actionClass?: string | undefined; |
| 15 | + dependsOn?: string[] | undefined; |
| 16 | + maxAttempts?: number | undefined; |
| 17 | +}; |
| 18 | + |
| 19 | +// The lifecycle-stage transitions this library provides a template for. |
| 20 | +export type PlanTemplateStage = "analyze" | "prepare"; |
| 21 | + |
| 22 | +// Context woven into a template's step titles so a plan reads against the opportunity it targets. |
| 23 | +export type PlanTemplateContext = { |
| 24 | + // A short human label for the issue/opportunity the plan is for (e.g. an issue title). Optional so a caller can |
| 25 | + // render a generic template; whitespace is collapsed and the value is length-bounded to keep every title valid. |
| 26 | + subject?: string | undefined; |
| 27 | +}; |
| 28 | + |
| 29 | +// Title length ceiling of `rawPlanStepSchema.title` (max 300). Titles are hard-capped to this so a long subject can |
| 30 | +// never produce an out-of-range step. |
| 31 | +const MAX_TITLE_CHARS = 300; |
| 32 | +// Keep the woven subject well under the title ceiling so the fixed prefix always survives the cap. |
| 33 | +const MAX_SUBJECT_CHARS = 200; |
| 34 | + |
| 35 | +// Collapse any run of whitespace (including newlines) to a single space and trim, so a subject yields a clean, |
| 36 | +// deterministic one-line title. |
| 37 | +function normalizeSubject(subject: string | undefined): string { |
| 38 | + return (subject ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_SUBJECT_CHARS); |
| 39 | +} |
| 40 | + |
| 41 | +// Compose a step title from a fixed prefix and the optional subject, hard-capped to the schema's title ceiling. |
| 42 | +function titleFor(prefix: string, subject: string): string { |
| 43 | + const full = subject ? `${prefix}: ${subject}` : prefix; |
| 44 | + return full.slice(0, MAX_TITLE_CHARS); |
| 45 | +} |
| 46 | + |
| 47 | +// analyze: feasibility check and repository RAG retrieval run independently, then the prompt-packet build consumes |
| 48 | +// both. Mirrors the ANALYZE-phase ordering described in the plan-template issue. |
| 49 | +export function analyzePlanTemplate(context: PlanTemplateContext = {}): RawPlanStep[] { |
| 50 | + const subject = normalizeSubject(context.subject); |
| 51 | + return [ |
| 52 | + { id: "feasibility-check", title: titleFor("Assess feasibility", subject), actionClass: "analyze", dependsOn: [], maxAttempts: 1 }, |
| 53 | + { id: "rag-retrieval", title: titleFor("Retrieve repository context", subject), actionClass: "retrieve", dependsOn: [], maxAttempts: 3 }, |
| 54 | + { id: "prompt-packet", title: titleFor("Build prompt packet", subject), actionClass: "compose", dependsOn: ["feasibility-check", "rag-retrieval"], maxAttempts: 2 }, |
| 55 | + ]; |
| 56 | +} |
| 57 | + |
| 58 | +// prepare: a strict chain — create the branch, invoke the coding agent (placeholder step; no actuation here), then |
| 59 | +// run the local tests. Mirrors the PREPARE-phase ordering described in the plan-template issue. |
| 60 | +export function preparePlanTemplate(context: PlanTemplateContext = {}): RawPlanStep[] { |
| 61 | + const subject = normalizeSubject(context.subject); |
| 62 | + return [ |
| 63 | + { id: "branch-create", title: titleFor("Create working branch", subject), actionClass: "vcs", dependsOn: [], maxAttempts: 3 }, |
| 64 | + { id: "coding-agent", title: titleFor("Invoke coding agent", subject), actionClass: "codegen", dependsOn: ["branch-create"], maxAttempts: 1 }, |
| 65 | + { id: "local-test", title: titleFor("Run local tests", subject), actionClass: "test", dependsOn: ["coding-agent"], maxAttempts: 2 }, |
| 66 | + ]; |
| 67 | +} |
| 68 | + |
| 69 | +// Registry of every stage transition to its template builder, so callers can enumerate or dispatch by stage. |
| 70 | +// Frozen so a consumer cannot mutate the shared registry and change dispatch behavior process-wide. |
| 71 | +export const PLAN_TEMPLATE_BUILDERS: Readonly<Record<PlanTemplateStage, (context?: PlanTemplateContext) => RawPlanStep[]>> = |
| 72 | + Object.freeze({ |
| 73 | + analyze: analyzePlanTemplate, |
| 74 | + prepare: preparePlanTemplate, |
| 75 | + }); |
| 76 | + |
| 77 | +// Build the raw-step template for a stage. Pure — a thin dispatcher over `PLAN_TEMPLATE_BUILDERS` that rejects an |
| 78 | +// unknown stage with a clear error rather than a generic "not a function" TypeError (guards non-TypeScript callers). |
| 79 | +export function buildPlanTemplate(stage: PlanTemplateStage, context: PlanTemplateContext = {}): RawPlanStep[] { |
| 80 | + const builder = PLAN_TEMPLATE_BUILDERS[stage]; |
| 81 | + if (!builder) throw new Error(`Unknown plan-template stage: ${String(stage)}`); |
| 82 | + return builder(context); |
| 83 | +} |
0 commit comments