UNPKG

mcp-ai-agent-guidelines

Version:

A comprehensive Model Context Protocol server providing advanced tools, resources, and prompts for implementing AI agent best practices

1,491 lines (1,395 loc) 60.3 kB
/** * SpecKitStrategy - GitHub Spec-Kit output format * * Generates a complete "Spec Kit" folder structure with comprehensive * documentation for implementing a feature following GitHub's Spec-Kit methodology. * This is the premium output format - the signature output of the refactored project. * * Generates: * - README.md: Overview and navigation (primary) * - spec.md: Full specification with requirements * - plan.md: Implementation plan with phases * - tasks.md: Detailed task breakdown * - adr.md: Architecture decision record * - roadmap.md: Timeline and milestones * * @module strategies/speckit-strategy * @see {@link https://github.com/Anselmoo/mcp-ai-agent-guidelines/blob/development/plan-v0.13.x/specs/SPEC-001-output-strategy-layer.md SPEC-001} §4.5 * @see {@link https://github.com/Anselmoo/mcp-ai-agent-guidelines/blob/development/plan-v0.13.x/specs/SPEC-005-speckit-integration.md SPEC-005} */ import { validationError } from "../tools/shared/error-factory.js"; import { OutputApproach } from "./output-strategy.js"; import { createSpecValidator, } from "./speckit/spec-validator.js"; /** * SpecKitStrategy implements GitHub's Spec-Kit output format. * * Generates a complete folder structure with 6 documents: * - README.md (primary): Navigation and quick start * - spec.md: Requirements and constraints * - plan.md: Implementation phases and timeline * - tasks.md: Detailed task breakdown * - adr.md: Architecture decision record * - roadmap.md: Milestones and deliverables * * Supports rendering: * - SessionState: Complete design session into Spec-Kit format * * @implements {OutputStrategy<SessionState>} */ export class SpecKitStrategy { /** The output approach this strategy implements */ approach = OutputApproach.SPECKIT; /** Optional validator for spec validation */ validator; /** Constitution used to create the current validator (for caching) */ cachedConstitution; /** * Render a domain result to Spec-Kit artifacts. * * @param result - The SessionState to render * @param options - Optional SpecKit rendering options * @returns Output artifacts with README.md and 5 secondary documents * @throws {Error} If result is not a SessionState */ render(result, options) { if (!this.isSessionState(result)) { throw new Error("SpecKitStrategy only supports SessionState"); } // Initialize validator if constitution provided and not already cached if (options?.constitution) { // Only create a new validator if constitution has changed if (this.cachedConstitution !== options.constitution) { this.validator = createSpecValidator(options.constitution); this.cachedConstitution = options.constitution; } } else { // Clear validator if no constitution provided this.validator = undefined; this.cachedConstitution = undefined; } // Prepare spec content for validation const specContent = this.extractSpecContent(result); // Validate before rendering if requested let validationResult; if (options?.validateBeforeRender && this.validator) { validationResult = this.validator.validate(specContent); // Fail if errors and failOnValidationErrors is true if (options.failOnValidationErrors && !validationResult.valid) { throw validationError("Spec validation failed", { issues: validationResult.issues, score: validationResult.score, }); } } const title = this.extractTitle(result); const slug = this.slugify(title); return { primary: this.generateReadme(result, slug), secondary: [ this.generateSpec(result, slug, options, validationResult), this.renderPlan(result, slug), this.generateTasks(result, slug), this.renderProgress(result, slug), this.generateAdr(result, slug), this.generateRoadmap(result, slug), ], }; } /** * Check if this strategy supports rendering a specific domain type. * * @param domainType - The domain type identifier * @returns True if this strategy can render the domain type */ supports(domainType) { return ["SessionState"].includes(domainType); } /** * Generate README.md - primary navigation document. * * @param result - The session state * @param slug - The slugified folder name * @returns README document with navigation and quick start * @private */ generateReadme(result, slug) { const title = this.extractTitle(result); const overview = this.extractOverview(result); const currentDate = new Date().toISOString().split("T")[0]; const status = result.status || "Draft"; const content = `# Spec Kit: ${title} This Spec Kit contains the complete documentation for implementing this feature. ## Overview ${overview} ## Contents - [spec.md](./spec.md) - Full specification with requirements and constraints - [plan.md](./plan.md) - Implementation plan with phases and timeline - [tasks.md](./tasks.md) - Detailed task breakdown with dependencies - [progress.md](./progress.md) - Progress tracking and status updates - [adr.md](./adr.md) - Architecture decision record - [roadmap.md](./roadmap.md) - Timeline and milestones ## Quick Start 1. Read the specification (spec.md) 2. Review the ADR for design decisions 3. Follow the implementation plan 4. Execute tasks in order ## Status - **Created**: ${currentDate} - **Status**: ${status} - **Owner**: @copilot --- *Generated by design-assistant with SpecKit strategy* `; return { name: `${slug}/README.md`, content, format: "markdown", }; } /** * Generate spec.md - full specification document. * * @param result - The session state * @param slug - The slugified folder name * @param options - Optional SpecKit rendering options * @param validationResult - Optional validation result to include * @returns Specification document with requirements and constraints * @private */ generateSpec(result, slug, options, validationResult) { const title = this.extractTitle(result); const overview = this.extractOverview(result); const functionalReqs = this.extractFunctionalRequirements(result); const nonFunctionalReqs = this.extractNonFunctionalRequirements(result); const constraints = this.extractConstraints(result); const acceptanceCriteria = this.extractAcceptanceCriteria(result); const outOfScope = this.extractOutOfScope(result); const constraintRefs = this.extractConstraintReferences(result); const content = `# Specification: ${title} ## Overview ${overview} ## Objectives ${this.extractObjectives(result)} ## Requirements ### Functional Requirements ${functionalReqs} ### Non-Functional Requirements ${nonFunctionalReqs} ## Constraints ${constraints}${options?.includeConstitutionalConstraints && options.constitution ? `\n\n${this.renderConstraints(constraintRefs, options.constitution)}` : ""} ## Acceptance Criteria ${acceptanceCriteria} ## Out of Scope ${outOfScope}${validationResult ? `\n${this.renderValidationSection(validationResult)}` : ""} --- *See [plan.md](./plan.md) for implementation details* *See [adr.md](./adr.md) for architectural decisions* `; return { name: `${slug}/spec.md`, content, format: "markdown", }; } /** * Generate tasks.md - detailed task breakdown. * * @param result - The session state * @param slug - The slugified folder name * @returns Task breakdown document with checklist * @private */ generateTasks(result, slug) { // Check if we can derive tasks from a spec (has requirements or acceptance criteria) const hasSpecData = (result.config?.requirements && Array.isArray(result.config.requirements) && result.config.requirements.length > 0) || (result.context?.requirements && Array.isArray(result.context.requirements) && result.context.requirements.length > 0) || (result.context?.acceptanceCriteria && Array.isArray(result.context.acceptanceCriteria) && result.context.acceptanceCriteria.length > 0); if (hasSpecData) { // Use the new enhanced renderTasks method try { const spec = this.extractSpec(result); return this.renderTasks(result, slug, spec); } catch (error) { // Fallback to simple task list generation if extraction fails, // but surface the underlying problem in non-production environments if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") { // eslint-disable-next-line no-console -- Safe debug logging for unexpected errors console.error("SpecKitStrategy.generateTasks: enhanced task rendering failed, falling back to basic generation.", error); } } } // Fallback to simple task list generation (backward compatible) const title = this.extractTitle(result); const tasks = this.extractTaskList(result); const dependencies = this.generateTaskDependencies(result); const content = `# Tasks: ${title} ## Task List ${tasks} ## Task Dependencies ${dependencies} ## Completion Tracking - Total Tasks: ${this.countTasks(result)} - Completed: 0 - In Progress: 0 - Blocked: 0 --- *Update this document as tasks are completed* *Mark tasks with [x] when complete* `; return { name: `${slug}/tasks.md`, content, format: "markdown", }; } /** * Generate adr.md - architecture decision record. * * @param result - The session state * @param slug - The slugified folder name * @returns Architecture decision record * @private */ generateAdr(result, slug) { const title = this.extractTitle(result); const context = this.extractAdrContext(result); const decision = this.extractAdrDecision(result); const consequences = this.extractAdrConsequences(result); const currentDate = new Date().toLocaleDateString("en-US", { month: "long", year: "numeric", }); const content = `# ADR: ${title} ## Status Proposed — ${currentDate} ## Context ${context} ## Decision ${decision} ## Consequences ### Positive ${consequences.positive} ### Negative ${consequences.negative} ### Neutral ${consequences.neutral} --- *See [spec.md](./spec.md) for full requirements* *See [plan.md](./plan.md) for implementation approach* `; return { name: `${slug}/adr.md`, content, format: "markdown", }; } /** * Generate roadmap.md - timeline and milestones. * * @param result - The session state * @param slug - The slugified folder name * @returns Roadmap with milestones and deliverables * @private */ generateRoadmap(result, slug) { const title = this.extractTitle(result); const milestones = this.extractMilestones(result); const deliverables = this.extractDeliverables(result); const content = `# Roadmap: ${title} ## Overview This roadmap outlines the key milestones and deliverables for implementing this feature. ## Milestones ${milestones} ## Key Deliverables ${deliverables} ## Progress Tracking Track progress against this roadmap and update status as milestones are completed. --- *See [plan.md](./plan.md) for detailed phase breakdown* *See [tasks.md](./tasks.md) for task-level tracking* `; return { name: `${slug}/roadmap.md`, content, format: "markdown", }; } /** * Slugify a title for use as a folder name. * * Converts to lowercase, replaces non-alphanumeric with hyphens, * and truncates to 50 characters. * * @param title - The title to slugify * @returns Slugified string suitable for folder name * @private */ slugify(title) { return title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 50); } /** * Extract title from SessionState. * * @param result - The session state * @returns Title string * @private */ extractTitle(result) { if (result.config?.goal) { return result.config.goal; } if (result.context?.title && typeof result.context.title === "string") { return result.context.title; } return "Feature"; } /** * Extract overview from SessionState. * * @param result - The session state * @returns Overview text * @private */ extractOverview(result) { if (result.context?.overview && typeof result.context.overview === "string") { return result.context.overview; } if (result.config?.goal) { return result.config.goal; } return "To be defined"; } /** * Extract objectives from SessionState. * * @param result - The session state * @returns Formatted objectives list * @private */ extractObjectives(result) { if (result.context?.objectives && Array.isArray(result.context.objectives)) { return result.context.objectives .map((obj, i) => `${i + 1}. ${obj}`) .join("\n"); } if (result.config?.goal) { return `1. ${result.config.goal}`; } return "To be defined"; } /** * Extract functional requirements from SessionState. * * @param result - The session state * @returns Formatted requirements list * @private */ extractFunctionalRequirements(result) { if (result.config?.requirements && Array.isArray(result.config.requirements)) { return result.config.requirements .map((req, i) => `${i + 1}. ${req}`) .join("\n"); } if (result.context?.requirements && Array.isArray(result.context.requirements)) { return result.context.requirements .map((req, i) => `${i + 1}. ${req}`) .join("\n"); } return "To be defined"; } /** * Extract non-functional requirements from SessionState. * * @param result - The session state * @returns Formatted non-functional requirements * @private */ extractNonFunctionalRequirements(result) { if (result.context?.nonFunctionalRequirements && Array.isArray(result.context.nonFunctionalRequirements)) { return result.context.nonFunctionalRequirements .map((req, i) => `${i + 1}. ${req}`) .join("\n"); } return "To be defined"; } /** * Extract constraints from SessionState. * * @param result - The session state * @returns Formatted constraints list * @private */ extractConstraints(result) { if (result.config?.constraints && Array.isArray(result.config.constraints)) { return result.config.constraints .map((c) => `- ${c}`) .join("\n"); } if (result.context?.constraints && Array.isArray(result.context.constraints)) { return result.context.constraints .map((c) => `- ${c}`) .join("\n"); } return "None specified"; } /** * Extract acceptance criteria from SessionState. * * @param result - The session state * @returns Formatted acceptance criteria * @private */ extractAcceptanceCriteria(result) { if (result.context?.acceptanceCriteria && Array.isArray(result.context.acceptanceCriteria)) { return result.context.acceptanceCriteria .map((c) => `- [ ] ${c}`) .join("\n"); } if (result.context?.successCriteria && Array.isArray(result.context.successCriteria)) { return result.context.successCriteria .map((c) => `- [ ] ${c}`) .join("\n"); } return "To be defined"; } /** * Extract out-of-scope items from SessionState. * * @param result - The session state * @returns Formatted out-of-scope list * @private */ extractOutOfScope(result) { if (result.context?.outOfScope && Array.isArray(result.context.outOfScope)) { return result.context.outOfScope .map((item) => `- ${item}`) .join("\n"); } return "None specified"; } /** * Extract task list from SessionState. * * @param result - The session state * @returns Formatted task list * @private */ extractTaskList(result) { if (result.context?.tasks && Array.isArray(result.context.tasks)) { return result.context.tasks .map((task, i) => { const estimate = task.estimate ? ` (${task.estimate})` : ""; const priority = task.priority ? ` [${task.priority}]` : ""; return `${i + 1}. [ ] ${task.title}${estimate}${priority}`; }) .join("\n"); } // Default tasks derived from requirements if (result.config?.requirements && Array.isArray(result.config.requirements)) { return result.config.requirements .map((req, i) => `${i + 1}. [ ] Implement: ${req}`) .join("\n"); } return "1. [ ] Define detailed requirements\n2. [ ] Create implementation plan\n3. [ ] Execute development\n4. [ ] Test and validate"; } /** * Generate task dependencies description. * * @param result - The session state * @returns Task dependencies description * @private */ generateTaskDependencies(result) { if (result.context?.tasks && Array.isArray(result.context.tasks)) { const tasks = result.context.tasks; const withDeps = tasks.filter((t) => t.dependencies && t.dependencies.length > 0); if (withDeps.length > 0) { return withDeps .map((task) => { const taskId = task.id || task.title; return `- **${taskId}** depends on: ${task.dependencies?.join(", ")}`; }) .join("\n"); } } return "No explicit task dependencies defined yet"; } /** * Count total tasks. * * @param result - The session state * @returns Task count * @private */ countTasks(result) { if (result.context?.tasks && Array.isArray(result.context.tasks)) { return result.context.tasks.length; } if (result.config?.requirements && Array.isArray(result.config.requirements)) { return result.config.requirements.length; } return 4; // Default task count } /** * Extract ADR context from SessionState. * * @param result - The session state * @returns ADR context description * @private */ extractAdrContext(result) { if (result.context?.adrContext && typeof result.context.adrContext === "string") { return result.context.adrContext; } return `${this.extractOverview(result)}\n\nThis decision addresses the key architectural and design considerations for implementing this feature.`; } /** * Extract ADR decision from SessionState. * * @param result - The session state * @returns ADR decision description * @private */ extractAdrDecision(result) { if (result.context?.adrDecision && typeof result.context.adrDecision === "string") { return result.context.adrDecision; } if (result.context?.approach && typeof result.context.approach === "string") { return `We have decided to: ${result.context.approach}`; } return "To be documented based on architectural analysis"; } /** * Extract ADR consequences from SessionState. * * @param result - The session state * @returns ADR consequences (positive, negative, neutral) * @private */ extractAdrConsequences(result) { const consequences = { positive: "- To be documented", negative: "- To be documented", neutral: "- To be documented", }; if (result.context?.adrConsequences) { const adrCons = result.context.adrConsequences; if (adrCons.positive && Array.isArray(adrCons.positive)) { consequences.positive = adrCons.positive .map((c) => `- ${c}`) .join("\n"); } if (adrCons.negative && Array.isArray(adrCons.negative)) { consequences.negative = adrCons.negative .map((c) => `- ${c}`) .join("\n"); } if (adrCons.neutral && Array.isArray(adrCons.neutral)) { consequences.neutral = adrCons.neutral.map((c) => `- ${c}`).join("\n"); } } return consequences; } /** * Extract milestones from SessionState. * * @param result - The session state * @returns Formatted milestones * @private */ extractMilestones(result) { if (result.context?.milestones && Array.isArray(result.context.milestones)) { return result.context.milestones .map((milestone, i) => { const date = milestone.date || "TBD"; const deliverables = milestone.deliverables ? `\n - ${milestone.deliverables.join("\n - ")}` : ""; return `### Milestone ${i + 1}: ${milestone.name} **Target Date**: ${date}${deliverables} `; }) .join("\n"); } // Generate default milestones from phases if (result.phases) { return Object.keys(result.phases) .map((phase, i) => `### Milestone ${i + 1}: Complete ${phase} phase **Target Date**: TBD `) .join("\n"); } return `### Milestone 1: Requirements Complete **Target Date**: TBD ### Milestone 2: Design Complete **Target Date**: TBD ### Milestone 3: Implementation Complete **Target Date**: TBD ### Milestone 4: Testing Complete **Target Date**: TBD `; } /** * Extract deliverables from SessionState. * * @param result - The session state * @returns Formatted deliverables list * @private */ extractDeliverables(result) { if (result.context?.deliverables && Array.isArray(result.context.deliverables)) { return result.context.deliverables .map((d) => `- ${d}`) .join("\n"); } // Generate deliverables from requirements if (result.config?.requirements && Array.isArray(result.config.requirements)) { return result.config.requirements .map((req) => `- Implemented: ${req}`) .join("\n"); } return "- Complete specification document\n- Working implementation\n- Comprehensive test suite\n- Documentation"; } /** * Extract constraint references from SessionState. * * Looks for constraint references in the context that link to * constitutional constraints. * * @param result - The session state * @returns Array of constraint references * @private */ extractConstraintReferences(result) { if (result.context?.constraintReferences && Array.isArray(result.context.constraintReferences)) { return result.context.constraintReferences; } return []; } /** * Render constitutional constraints section. * * Generates a markdown section with constitutional constraints * referenced by this specification. Each constraint is looked up * in the constitution and rendered with its full details. * * @param constraints - Array of constraint references * @param constitution - The constitution to look up constraints from * @returns Formatted constitutional constraints section * @private */ renderConstraints(constraints, constitution) { if (constraints.length === 0) return ""; const renderedConstraints = constraints .map((c) => { const item = this.findConstitutionItem(c.constitutionId, constitution); const notes = c.notes ? `\n**Notes**: ${c.notes}` : ""; return `### ${c.constitutionId}: ${item?.title ?? "Unknown"} ${item?.description ?? ""}${notes}`; }) .join("\n\n"); return `## Constitutional Constraints ${renderedConstraints}`; } /** * Find a constitution item by ID. * * Searches through all constitution sections (principles, constraints, * architecture rules, design principles) to find an item with the * matching ID. * * @param id - The constitution item ID to find * @param constitution - The constitution to search * @returns The found constitution item or undefined * @private */ findConstitutionItem(id, constitution) { // Search principles const principle = constitution.principles.find((p) => p.id === id); if (principle) return principle; // Search constraints const constraint = constitution.constraints.find((c) => c.id === id); if (constraint) return constraint; // Search architecture rules const archRule = constitution.architectureRules.find((ar) => ar.id === id); if (archRule) return archRule; // Search design principles const designPrinciple = constitution.designPrinciples.find((dp) => dp.id === id); if (designPrinciple) return designPrinciple; return undefined; } /** * Render plan.md document with structured Plan data. * * This is the enhanced method that creates a structured Plan object * and renders it into the plan.md format as specified in P4-005. * * @param result - The session state * @param slug - The slugified folder name * @returns OutputDocument for plan.md * @private */ renderPlan(result, slug) { const plan = this.extractPlan(result); const title = this.extractTitle(result); const content = `# Implementation Plan: ${title} ## Approach ${plan.approach} ## Phases ${plan.phases .map((p) => `### ${p.id}: ${p.name} ${p.description} **Duration**: ${p.duration} **Deliverables**: ${p.deliverables.map((d) => `- ${d}`).join("\n")} `) .join("\n")} ## Dependencies | ID | Description | Owner | |----|-------------|-------| ${plan.dependencies.map((d) => `| ${d.id} | ${d.description} | ${d.owner ?? "TBD"} |`).join("\n")} ## Risks | ID | Risk | Severity | Mitigation | |----|------|----------|------------| ${plan.risks.map((r) => `| ${r.id} | ${r.description} | ${r.severity} | ${r.mitigation} |`).join("\n")} ## Timeline | Phase | Start | End | |-------|-------|-----| ${plan.timeline.map((t) => `| ${t.phase} | Week ${t.startWeek} | Week ${t.endWeek} |`).join("\n")} --- *See [tasks.md](./tasks.md) for detailed task breakdown* *See [roadmap.md](./roadmap.md) for milestones and deliverables* `; return { name: `${slug}/plan.md`, content, format: "markdown", }; } /** * Extract structured Plan data from SessionState. * * Maps the design session data to the Plan interface, * deriving phases, dependencies, risks, and timeline. * * @param result - The session state * @returns Structured Plan object * @private */ extractPlan(result) { const phases = this.derivePhases(result); return { approach: result.context?.approach ?? "Iterative implementation following Spec-Kit methodology", phases, dependencies: this.deriveDependencies(result), risks: this.deriveRisks(result), timeline: this.deriveTimeline(phases), }; } /** * Derive phases from SessionState. * * Maps design session phases to structured Phase objects * with IDs, names, descriptions, deliverables, and durations. * * @param result - The session state * @returns Array of Phase objects * @private */ derivePhases(result) { if (result.phases && Object.keys(result.phases).length > 0) { return Object.entries(result.phases).map(([phaseId, phaseData], i) => { const description = typeof phaseData === "string" ? phaseData : typeof phaseData === "object" && phaseData !== null && "description" in phaseData ? String(phaseData.description) : "In progress"; // Extract deliverables if available const deliverables = typeof phaseData === "object" && phaseData !== null && "deliverables" in phaseData && Array.isArray(phaseData.deliverables) ? phaseData.deliverables : ["Complete phase objectives"]; // Extract duration if available const duration = typeof phaseData === "object" && phaseData !== null && "duration" in phaseData ? String(phaseData.duration) : "TBD"; return { id: `PHASE-${String(i + 1).padStart(3, "0")}`, name: phaseId, description, deliverables, duration, }; }); } // Default phases if none defined return [ { id: "PHASE-001", name: "Requirements Gathering", description: "Define and document all functional and non-functional requirements.", deliverables: ["Requirements specification", "Use cases"], duration: "1 week", }, { id: "PHASE-002", name: "Design & Architecture", description: "Create architectural design and make key technical decisions.", deliverables: ["Architecture diagrams", "Technical specifications"], duration: "2 weeks", }, { id: "PHASE-003", name: "Implementation", description: "Execute development according to the plan.", deliverables: ["Working code", "Unit tests"], duration: "4 weeks", }, { id: "PHASE-004", name: "Testing & Validation", description: "Verify all acceptance criteria are met.", deliverables: ["Test reports", "Quality assurance sign-off"], duration: "1 week", }, ]; } /** * Derive dependencies from SessionState. * * Extracts project dependencies and formats them as Dependency objects. * * @param result - The session state * @returns Array of Dependency objects * @private */ deriveDependencies(result) { if (result.context?.dependencies && Array.isArray(result.context.dependencies)) { return result.context.dependencies.map((dep, i) => { if (typeof dep === "string") { return { id: `DEP-${String(i + 1).padStart(3, "0")}`, description: dep, owner: undefined, }; } return { id: dep.id ?? `DEP-${String(i + 1).padStart(3, "0")}`, description: dep.description, owner: dep.owner, }; }); } return [ { id: "DEP-001", description: "No dependencies identified", owner: undefined, }, ]; } /** * Derive risks from SessionState. * * Extracts project risks and formats them as Risk objects * with severity levels and mitigation strategies. * * @param result - The session state * @returns Array of Risk objects * @private */ deriveRisks(result) { if (result.context?.risks && Array.isArray(result.context.risks)) { return result.context.risks.map((risk, i) => { if (typeof risk === "string") { return { id: `RISK-${String(i + 1).padStart(3, "0")}`, description: risk, severity: "medium", mitigation: "To be defined", }; } const description = risk.description ?? risk.name ?? "Unnamed risk"; const severity = (risk.severity?.toLowerCase() === "high" || risk.severity?.toLowerCase() === "low" ? risk.severity.toLowerCase() : "medium"); return { id: `RISK-${String(i + 1).padStart(3, "0")}`, description, severity, mitigation: risk.mitigation ?? "To be defined", }; }); } return [ { id: "RISK-001", description: "No significant risks identified yet", severity: "low", mitigation: "Continue monitoring during implementation", }, ]; } /** * Derive timeline from Phase array. * * Generates a timeline by assigning week numbers to each phase * based on their sequence and estimated durations. * * @param phases - Array of Phase objects * @returns Array of TimelineEntry objects * @private */ deriveTimeline(phases) { let currentWeek = 1; return phases.map((phase) => { // Parse duration to estimate weeks (simple heuristic) const durationMatch = phase.duration.match(/(\d+)\s*week/i); const weekCount = durationMatch ? Number.parseInt(durationMatch[1], 10) : 1; const entry = { phase: phase.id, startWeek: currentWeek, endWeek: currentWeek + weekCount - 1, }; currentWeek += weekCount; return entry; }); } /** * Type guard for SessionState. * * @param result - The value to check * @returns True if result is a SessionState * @private */ isSessionState(result) { return (typeof result === "object" && result !== null && "id" in result && "phase" in result && "context" in result && "history" in result); } /** * Extract ParsedSpec from SessionState. * * Transforms SessionState data into a ParsedSpec structure * for task derivation. * * @param result - The session state * @returns ParsedSpec object * @private */ extractSpec(result) { const title = this.extractTitle(result); const overview = this.extractOverview(result); // Extract functional requirements const functionalReqs = []; if (result.config?.requirements && Array.isArray(result.config.requirements)) { functionalReqs.push(...result.config.requirements.map((req, i) => ({ id: `REQ-${String(i + 1).padStart(3, "0")}`, description: req, priority: "high", }))); } else if (result.context?.requirements && Array.isArray(result.context.requirements)) { functionalReqs.push(...result.context.requirements.map((req, i) => ({ id: `REQ-${String(i + 1).padStart(3, "0")}`, description: req, priority: "high", }))); } // Extract non-functional requirements const nonFunctionalReqs = []; if (result.context?.nonFunctionalRequirements && Array.isArray(result.context.nonFunctionalRequirements)) { nonFunctionalReqs.push(...result.context.nonFunctionalRequirements.map((req, i) => ({ id: `NFR-${String(i + 1).padStart(3, "0")}`, description: req, priority: "medium", }))); } // Extract acceptance criteria const acceptanceCriteria = []; if (result.context?.acceptanceCriteria && Array.isArray(result.context.acceptanceCriteria)) { acceptanceCriteria.push(...result.context.acceptanceCriteria.map((ac, i) => ({ id: `AC-${String(i + 1).padStart(3, "0")}`, description: ac, verificationMethod: "automated", }))); } else if (result.context?.successCriteria && Array.isArray(result.context.successCriteria)) { acceptanceCriteria.push(...result.context.successCriteria.map((ac, i) => ({ id: `AC-${String(i + 1).padStart(3, "0")}`, description: ac, verificationMethod: "automated", }))); } return { title, overview, objectives: [], functionalRequirements: functionalReqs, nonFunctionalRequirements: nonFunctionalReqs, constraints: this.extractConstraintReferences(result), acceptanceCriteria, outOfScope: result.context?.outOfScope ?? [], }; } /** * Render tasks.md with enhanced structure. * * Creates a comprehensive tasks document with grouped tasks, * dependency graph, and priority sections. * * @param result - The session state * @param slug - The slugified folder name * @param spec - Optional parsed spec (if available) * @returns OutputDocument for tasks.md * @private */ renderTasks(result, slug, spec) { let tasks; if (spec) { tasks = this.deriveTasksFromSpec(spec); } else { // Fallback: create basic tasks from context tasks = []; if (result.context?.tasks && Array.isArray(result.context.tasks)) { tasks = result.context.tasks.map((t, i) => ({ id: `T${String(i + 1).padStart(3, "0")}`, title: t.title, description: "", priority: "medium", estimate: "3h", acceptanceCriteria: [], })); } } const groupedTasks = this.groupTasksByPhase(tasks); const totalEstimate = tasks.length > 0 ? this.calculateTotalEstimate(tasks) : "To be determined"; // Filter out empty phases to keep output clean const nonEmptyPhases = Object.entries(groupedTasks).filter(([, phaseTasks]) => phaseTasks.length > 0); const content = `# Tasks ## Overview **Total Tasks**: ${tasks.length} **Estimated Effort**: ${totalEstimate} ## Task List ${nonEmptyPhases.length > 0 ? nonEmptyPhases .map(([phase, phaseTasks]) => `### ${phase} ${phaseTasks .map((t) => `#### ${t.id}: ${t.title} - **Priority**: ${t.priority} - **Estimate**: ${t.estimate} ${t.dependencies?.length ? `- **Dependencies**: ${t.dependencies.join(", ")}` : ""} ${t.description} **Acceptance Criteria**: ${t.acceptanceCriteria.map((ac) => `- [ ] ${ac}`).join("\n")} --- `) .join("\n")}`) .join("\n") : "No tasks defined yet.\n"} ## Dependencies Graph \`\`\`mermaid ${this.generateDependencyGraph(tasks)} \`\`\`${tasks.length === 0 ? "\n\n*No tasks defined*" : ""} ## By Priority ### High Priority ${tasks .filter((t) => t.priority === "high") .map((t) => `- [ ] ${t.id}: ${t.title}`) .join("\n") || "None"} ### Medium Priority ${tasks .filter((t) => t.priority === "medium") .map((t) => `- [ ] ${t.id}: ${t.title}`) .join("\n") || "None"} ### Low Priority ${tasks .filter((t) => t.priority === "low") .map((t) => `- [ ] ${t.id}: ${t.title}`) .join("\n") || "None"} --- *Generated by SpecKitStrategy* `; return { name: `${slug}/tasks.md`, content, format: "markdown", }; } /** * Group tasks by phase. * * Organizes tasks into phase buckets, defaulting to "Unassigned" * for tasks without a phase. * * @param tasks - Array of derived tasks * @returns Tasks grouped by phase * @private */ groupTasksByPhase(tasks) { const grouped = { "Phase 1": [], "Phase 2": [], Unassigned: [], }; for (const task of tasks) { const phase = task.phase ?? "Unassigned"; if (!grouped[phase]) grouped[phase] = []; grouped[phase].push(task); } return grouped; } /** * Generate Mermaid dependency graph. * * Creates a Mermaid graph showing task dependencies. * * @param tasks - Array of derived tasks * @returns Mermaid graph definition * @private */ generateDependencyGraph(tasks) { const lines = ["graph TD"]; for (const task of tasks) { if (task.dependencies?.length) { for (const dep of task.dependencies) { lines.push(` ${dep} --> ${task.id}`); } } else { lines.push(` ${task.id}[${task.id}]`); } } return lines.join("\n"); } /** * Calculate total estimate from tasks. * * Sums task estimates (in hours) and converts to days. * Note: Only parses estimates in "Xh" format (e.g., "3h", "8h"). * Other formats like "1d", "2 days", or "1 week" are not supported * and will be ignored in the calculation. * * @param tasks - Array of derived tasks * @returns Total estimate string (e.g., "24h (~3 days)") * @private */ calculateTotalEstimate(tasks) { let totalHours = 0; for (const task of tasks) { const match = task.estimate.match(/(\d+)h/); if (match) { totalHours += Number.parseInt(match[1], 10); } } const totalDays = Math.ceil(totalHours / 8); const dayLabel = totalDays === 1 ? "day" : "days"; return `${totalHours}h (~${totalDays} ${dayLabel})`; } /** * Derive actionable tasks from a parsed specification. * * Transforms requirements and acceptance criteria into concrete, * estimable tasks with verification tasks for each requirement. * * @param spec - The parsed specification * @returns Array of derived tasks with IDs, estimates, and dependencies * @throws {Error} If spec structure is invalid * @private */ deriveTasksFromSpec(spec) { // Validate spec structure if (!spec || typeof spec !== "object") { throw new Error("Invalid spec: spec must be an object"); } if (!Array.isArray(spec.functionalRequirements)) { throw new Error("Invalid spec: functionalRequirements must be an array"); } if (!Array.isArray(spec.acceptanceCriteria)) { throw new Error("Invalid spec: acceptanceCriteria must be an array"); } const tasks = []; let taskCounter = 1; // Derive tasks from functional requirements for (const req of spec.functionalRequirements) { const task = this.deriveTaskFromRequirement(req, taskCounter++); tasks.push(task); // Add verification task for each requirement const verifyTask = this.deriveVerificationTask(req, taskCounter++); tasks.push(verifyTask); } // Derive tasks from acceptance criteria for (const ac of spec.acceptanceCriteria) { const task = this.deriveTaskFromAcceptanceCriterion(ac, taskCounter++); tasks.push(task); } return tasks; } /** * Derive an implementation task from a requirement. * * Converts a requirement into an actionable task with estimate * based on complexity keywords in the description. * * @param req - The requirement to derive a task from * @param id - Numeric task ID * @returns Implementation task * @private */ deriveTaskFromRequirement(req, id) { // Estimate based on complexity keywords const estimate = this.estimateFromDescription(req.description); return { id: `T${String(id).padStart(3, "0")}`, title: `Implement: ${this.extractTaskTitle(req.description)}`, description: `Implement functionality to satisfy requirement ${req.id}.\n\n**Requirement**: ${req.description}`, priority: req.priority, estimate, acceptanceCriteria: [ `Requirement ${req.id} is satisfied`, "Unit tests pass", "Code review approved", ], }; } /** * Derive a verification task for a requirement. * * Creates a testing/verification task that depends on the * implementation task for the same requirement. * * @param req - The requirement to verify * @param id - Numeric task ID * @returns Verification task with dependency * @private */ deriveVerificationTask(req, id) { return { id: `T${String(id).padStart(3, "0")}`, title: `Verify: ${this.extractTaskTitle(req.description)}`, description: `Write tests to verify requirement ${req.id} is correctly implemented.`, priority: req.priority, estimate: "2h", acceptanceCriteria: [ "Tests cover happy path", "Tests cover edge cases", "Tests cover error conditions", ], dependencies: [`T${String(id - 1).padStart(3, "0")}`], }; } /** * Derive a validation task from an acceptance criterion. * * Creates a task to validate that an acceptance criterion is met, * with estimate based on verification method (automated vs manual). * * @param ac - The acceptance criterion to validate * @param id - Numeric task ID * @returns Validation task * @private */ deriveTaskFromAcceptanceCriterion(ac, id) { return { id: `T${String(id).padStart(3, "0")}`, title: `Validate: ${this.extractTaskTitle(ac.description)}`, description: `Verify acceptance criterion ${ac.id} is met.`, priority: "high", estimate: ac.verificationMethod === "automated" ? "1h" : "2h", acceptanceCriteria: [ `${ac.description} is verified`, `Verification method: ${ac.verificationMethod}`, ], }; } /** * Estimate task duration from description complexity. * * Uses keyword analysis to estimate work duration: * - "simple" or "basic" → 2h * - "complex" or "comprehensive" → 8h * - "integration" or "refactor" → 4h * - default → 3h * * @param description - Task or requirement description * @returns Estimated duration string * @private */ estimateFromDescription(description) { const lowercased = description.toLowerCase(); if (lowercased.includes("simple") || lowercased.includes("basic")) { return "2h"; } if (lowercased.includes("complex") || lowercased.includes("comprehensive")) { return "8h"; } if (lowercased.includes("integration") || lowercased.includes("refactor")) { return "4h"; } return "3h"; // Default estimate } /** * Extract a concise title from a