Class: RubynCode::Megaplan::PlanProposer
- Inherits:
-
Object
- Object
- RubynCode::Megaplan::PlanProposer
- Defined in:
- lib/rubyn_code/megaplan/plan_proposer.rb
Overview
Proposes a multi-phase megaplan for a feature description.
Asks the LLM to produce a JSON payload that matches the extension's
plan_proposal shape — one folder per phase, three documents per
phase, vertical-slice ordering. The handler validates the response
and returns it to the IDE.
The LLM call is the slow part (~5-30s); callers should run this off the main JSON-RPC thread.
Defined Under Namespace
Classes: InvalidProposalError
Constant Summary collapse
- MAX_PHASES =
12- DEFAULT_SYSTEM_PROMPT =
"You are a senior Ruby/Rails architect breaking a feature request into a megaplan.\n\nA megaplan is a multi-phase development plan where each phase is a\nVERTICAL SLICE that can ship independently. Trunk works at every phase\nboundary. No \"scaffolding first, behavior later\" \u2014 every phase delivers\na thin, end-to-end working increment.\n\nOutput a single JSON object with this exact shape:\n\n{\n \"slug\": \"kebab-case-feature-slug\",\n \"feature\": \"Short feature description\",\n \"phases\": [\n {\n \"number\": 1,\n \"slug\": \"kebab-case-phase-slug\",\n \"name\": \"Human-readable phase name\",\n \"summary\": \"One-sentence summary of what this phase ships\",\n \"requirements_md\": \"# Phase 1 \u2014 <name>: Requirements\\\\n\\\\n...\",\n \"design_md\": \"# Phase 1 \u2014 <name>: Design\\\\n\\\\n...\",\n \"tasks_md\": \"# Phase 1 \u2014 <name>: Tasks\\\\n\\\\n## [ ] 1. ...\\\\n\\\\n- [ ] 1.1 ...\"\n }\n ]\n}\n\nConstraints:\n - 1 to 12 phases. Smaller, sharper phases beat fewer mega-phases.\n - Each phase must be a vertical slice.\n - tasks_md is a checklist with `[ ]` boxes (megaplan convention).\n - Every phase needs requirements_md, design_md, tasks_md.\n - Return ONLY the JSON. No markdown fences. No commentary.\n"
Instance Method Summary collapse
-
#initialize(llm_client: nil, system_prompt: nil, max_phases: MAX_PHASES) ⇒ PlanProposer
constructor
A new instance of PlanProposer.
-
#propose(feature) ⇒ Hash
Payload with
slug,feature,phases. -
#validate!(payload, _feature = nil) ⇒ Object
Validate a parsed plan_proposal Hash.
Constructor Details
#initialize(llm_client: nil, system_prompt: nil, max_phases: MAX_PHASES) ⇒ PlanProposer
Returns a new instance of PlanProposer.
55 56 57 58 59 |
# File 'lib/rubyn_code/megaplan/plan_proposer.rb', line 55 def initialize(llm_client: nil, system_prompt: nil, max_phases: MAX_PHASES) @llm_client = llm_client || LLM::Client.new @system_prompt = system_prompt || DEFAULT_SYSTEM_PROMPT @max_phases = max_phases end |
Instance Method Details
#propose(feature) ⇒ Hash
Returns payload with slug, feature, phases.
64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/rubyn_code/megaplan/plan_proposer.rb', line 64 def propose(feature) raise ArgumentError, 'feature is required' if feature.nil? || feature.strip.empty? response = @llm_client.chat( messages: [{ role: 'user', content: feature_prompt(feature) }], system: @system_prompt ) text = extract_text(response) payload = parse_payload(text) validate!(payload, feature) normalize(payload, feature) end |
#validate!(payload, _feature = nil) ⇒ Object
Validate a parsed plan_proposal Hash. Public so the interview path (which produces the same shape via a different LLM workflow) can reuse the rule set without reaching into a private method.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/rubyn_code/megaplan/plan_proposer.rb', line 81 def validate!(payload, _feature = nil) raise InvalidProposalError, 'payload is not an object' unless payload.is_a?(Hash) phases = payload['phases'] raise InvalidProposalError, 'phases must be an array' unless phases.is_a?(Array) raise InvalidProposalError, 'phases is empty' if phases.empty? raise InvalidProposalError, "too many phases (max #{@max_phases})" if phases.size > @max_phases phases.each_with_index do |phase, idx| %w[name summary requirements_md design_md tasks_md].each do |key| next unless phase[key].nil? || phase[key].to_s.strip.empty? raise InvalidProposalError, "phase #{idx + 1} missing #{key}" end end end |