Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ Write `$MIGRATION_DIR/preferences.json`:
"clarify_mode": "full|fast_path",
"questions_asked": ["Q1", "Q2", ...],
"questions_defaulted": ["Q7", "Q8", ...],
"questions_skipped_not_applicable": ["Q6", "Q8", ...]
"questions_skipped_extracted": ["Q6", "Q12b", ...],
"questions_skipped_not_applicable": ["Q6", "Q8", ...],
"inventory_clarifications": {"database_ha": "plan:premium-0"}
},
"global": {
"target_region": "<Q1 value>",
Expand Down Expand Up @@ -104,9 +106,9 @@ Do **not** write a `workshop` object from Clarify. The what-if workshop

### Schema Rules

1. The `sources` object records how each question was answered: `"user"` (explicitly answered), `"default"` (system default applied, including skipped questions and "use defaults for the rest").
1. The `sources` object records how each question was answered: `"user"` (explicitly answered, or corrected on the Assumption Sheet), `"extracted"` (resolved from the inventory — Detected sheet row), `"default"` (system default applied, including skipped questions, sheet-confirmed defaults, and "use defaults for the rest").
2. `defaults_applied` is the array of question IDs that received default values.
3. `metadata.questions_skipped_not_applicable` records questions skipped because their triggering condition was not met (e.g., Q6 skipped because no Postgres).
3. `metadata.questions_skipped_not_applicable` records questions skipped because their triggering condition was not met (e.g., Q6 skipped because no Postgres). `metadata.questions_skipped_extracted` records questions resolved from the inventory (interview Step 2.5 Extraction Rules); the raw signal goes in `metadata.inventory_clarifications` (e.g. `{"database_ha": "plan:premium-0"}`).
4. Only write keys with non-null values. Omit sections/keys that are entirely null.
5. `global.fir_intent` is `null` when no Fir apps detected (Q11 not fired).
6. `network.existing_vpc_id` and `network.subnet_ids` are `null`/empty when no Private Space peering exists.
Expand Down Expand Up @@ -165,4 +167,6 @@ private-space conditionals), then emit `GATE_FAIL` (STOP; do not patch artifacts

Only after `HANDOFF_OK`, apply the phase-status update protocol (`INTERPRETER.md` § The interpreter loop) — mark `phases.clarify` completed and advance per `_advances_to` — in the **same turn** as the output message below.

Output to user: "Clarification complete. Proceeding to Phase 3: Design AWS Architecture."
Output to user: "Phase 2 of 6 complete (Clarify). Remaining: Design → Estimate → Generate (+ optional Feedback). Next artifact: aws-design.json. Proceeding to Phase 3: Design AWS Architecture."

_Emit this breadcrumb only after `HANDOFF_OK` — never on a failed gate._
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,56 @@ Before generating questions, scan the inventory to determine which questions app
| Q14 — Alerting preference | Always | Never |
| Q15 — Cost optimization | Always | Never |

### Batch Planning
### Extraction Rules (answer from the inventory before asking)

Before planning batches, resolve what `heroku-resource-inventory.json` already answers. Extracted questions are NOT asked — they appear as **Detected** rows on the Assumption Sheet (Step 2.5) and are recorded in `metadata.questions_skipped_extracted`, with the raw signal in `metadata.inventory_clarifications`.

| Q | Extraction signal | Resolves to | When NOT to extract |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Q1 — Region | Private Space `region` (e.g. `virginia` → `us-east-1`, `oregon` → `us-west-2`, `dublin` → `eu-west-1`, `frankfurt` → `eu-central-1`); Common Runtime apps: `us` → suggest `us-east-1`, `eu` → suggest `eu-west-1` | `global.target_region` — **Detected** for Private Spaces (explicit region); **Proposed default** for Common Runtime (a suggestion, not a fact) | Mixed regions across apps/spaces — ask Q1 |
| Q6 — Database HA | Heroku Postgres plan tier: `standard-*` → no HA follower (`database_ha: false` proposed); `premium-*` / `private-*` / `shield-*` → HA included (`database_ha: true` detected) | `data.database_ha` | Multiple Postgres add-ons with mixed tiers — ask Q6 with a per-add-on breakdown |
| Q7 — Redis HA | Redis plan tier: `premium-*` and above → HA (`redis_ha: true` detected); `mini`/hobby tiers → no HA (proposed `false`) | `data.redis_ha` | Mixed tiers — ask Q7 |
| Q12b — Containerization | App stack field: `container` stack → `containerization_status: "dockerfile"` detected; buildpack stacks (`heroku-22`, `heroku-24`) → `buildpack_only` detected | `compute.containerization_status` | Mixed stacks across apps — ask Q12b |

**Tier-derived HA is a strong signal, not a requirement statement:** the plan tier says what the customer HAS, not what they NEED. Present tier-derived rows on the sheet with the source shown ("your `standard-0` plan has no HA follower") so the user can correct if their target posture differs from their current one — this mirrors Q3 (availability posture), which is always asked and never extracted.

### Step 2.5: Assumption Sheet (Mandatory Gate)

**HARD GATE — do NOT ask any batch question until the user responds to this sheet.** Skip the sheet only when nothing was extracted AND no documented default applies (rare).

Present detected values and to-be-assumed defaults as one confirm-or-edit sheet:

```
### Migration assumptions — confirm or correct

**Detected from your Heroku inventory:**

| Setting | Value | Source | What it decides |
| ------- | ----- | ------ | --------------- |
| Region | us-east-1 (Private Space: virginia) | space config | All AWS resources deploy here |
| Database HA | Included (premium-0 plan) | Postgres plan tier | RDS Multi-AZ topology |
| Containerization | Buildpacks only (heroku-24) | app stack | Fargate via buildpack-to-image path |

**Assumed (documented defaults — correct anything that's wrong):**

| Setting | Assumed value | Consequence if left as-is |
| ------- | ------------- | ------------------------- |
| Migration approach | Full cutover | Single cutover event; say "interim/data-first" for phased |
| DB migration method | pg_dump/restore | Fine under ~100GB; larger needs replication tooling |
| Cost optimization | Balanced | No aggressive Spot/reservation assumptions |

Reply:
1. **Confirm all** (or "looks good") — I'll ask only the [N] remaining questions.
2. **Change a setting** — name it ("database ha: no") or describe it in plain words ("we can't take downtime") — I'll map it or ask the full question. Several fixes in one message is fine.
3. **"ask me about [setting]"** — I'll ask the full question with all options.
4. **"ask me everything"** — discard assumptions, run the full batch flow.
```

_Present these as selectable options via the structured question tool (e.g. AskUserQuestion) when the IDE provides one; otherwise the numbered list verbatim. Free-text corrections are always accepted — the menu never replaces them._

Questions resolved on the sheet (confirmed or corrected) are excluded from the batches. User corrections move the question ID from `questions_skipped_extracted`/`questions_defaulted` to `questions_asked`.

---

After determining active questions, organize them into **three progressive batches**:

Expand All @@ -144,7 +193,7 @@ After determining active questions, organize them into **three progressive batch

**Batch 3 is always active** (Q12–Q15 always fire; Q11 fires only if Fir detected).

Record the ordered list of active batches and count questions per batch after filtering.
Record the ordered list of active batches and count questions per batch after filtering. **Exclude questions resolved on the Assumption Sheet** (extracted, defaulted-and-confirmed, or corrected) — batches contain only the questions the user must actually answer.

---

Expand All @@ -156,7 +205,7 @@ For each active batch, execute steps 3a–3c:

#### 3a. Present Batch

Use a conversational tone with brief context explaining why each question matters. Number questions within each batch starting from 1.
Use a conversational tone with brief context explaining why each question matters. Number questions within each batch starting from 1. **Cap each turn at 4 questions** — when a batch has more, split it and open each part with "Batch [i] of [k]". Open the first batch with: "That leaves [N] decisions only you can make — then we're ready to design." _Use the structured question tool (e.g. AskUserQuestion) when available, identical option text otherwise; shorthand answers ("1A 2C 3 skip") are accepted in either mode._

**Batch 1 — Global / Strategic (always first):**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Output to user — build message from design contents:
- If pipeline warnings: "N pipeline(s) detected (CI/CD requires manual config)."
- VPC mode: "VPC design: [existing VPC referenced | new VPC generated with N subnets]."

Format: "Design phase complete. [artifact summaries] Next required step: Phase 4 — Estimate. Load `references/phases/estimate/estimate.md` now."
Format: "Phase 3 of 6 complete (Design). [artifact summaries] Remaining: Estimate → Generate (+ optional Feedback). Next required step: Phase 4 — Estimate. Load `references/phases/estimate/estimate.md` now."

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ After writing `estimation-infra.json`, present a concise summary to the user:
5. **Migration complexity**: tier + timeline range
6. **Monthly and annual savings** (or increase) vs Heroku per tier (if a baseline was determined)
7. **Top 2-3 optimization opportunities** with savings potential
8. **Recommendation**: `path_label` with one-line justification
8. **Recommendation**: lead with `outcome_label` (or `path_label` when `outcome` is absent). List `conditions[]` when `conditional_go`. Close with 1–3 `would_flip_if[]` bullets when present. Keep `path_label` as the execution-shape line under the verdict.

Keep under 25 lines. The user can ask for details or re-read `estimation-infra.json`.

Expand All @@ -129,14 +129,19 @@ After outer-run `HANDOFF_OK`, the summary above, and the deferred phase-status
update — offer:

```
Estimate complete. Before Generate, you can run a what-if workshop:
change region, HA, compute target, or CPU architecture (x86 vs Graviton)
and compare priced scenarios without re-discovering inventory.
Phase 4 of 6 complete (Estimate). Remaining: Generate (+ optional Feedback).
Before Generate, want to see how the numbers move if you change something?
I can reprice scenarios side by side in about a minute each, without
re-running discovery — for example: a different AWS region, single-AZ
database for staging, a different compute target, or ARM-based (Graviton)
instances.

[A] Enter what-if workshop
[B] Proceed toward Generate
```

**Data-justified scenario hint (add one line when applicable):** if a material assumption was defaulted or tier-derived rather than confirmed — most commonly `database_ha` — append: "Suggestion: we assumed [assumption]; comparing a [alternative] scenario would bound it before you commit." Suggest at most one.

- **A** → Load `references/phases/workshop/workshop.md` (sidebar) and follow it
(baseline capture if `scenarios/` missing, then the sheet). Keep
`current_phase: estimate`; set `phases.workshop` → `"in_progress"`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,27 +540,70 @@ Present 3 paths:

1. **Migrate with Optimizations (Best ROI)** — optimized service choices, projected savings
2. **Phased Migration (Lower Risk)** — app-by-app per design order, validate each before proceeding
3. **Stay on Heroku (Lowest Complexity)** — only if AWS is more expensive and costs are the sole metric
3. **Stay on Heroku** — when the evidence favors staying: AWS materially more expensive without offsetting operational benefit, team leverage from Heroku's managed simplicity with no AWS-specific need, or migration cost/risk exceeding the projected benefit for this stack. **Do not gate "stay" on "cost is the sole metric"** — any decisive factor suffices.

Include migrate/stay decision factors:

- **Migrate if:** infrastructure control matters, AWS-specific services needed, compliance requirements exceed Heroku's offerings, scaling beyond Heroku limits, long-term cost optimization (Savings Plans, Spot)
- **Stay if:** cost is the only metric and AWS is more expensive, team benefits from Heroku's managed simplicity, no need for AWS-specific services, migration risk exceeds benefit
- **Stay if:** AWS is more expensive without an offsetting benefit for this stack, team benefits from Heroku's managed simplicity, no need for AWS-specific services, migration risk exceeds benefit

### Decision outcome (write alongside `path`)

`path` says how a migration would run; `outcome` says whether to run it now. **`defer_for_evidence` is expected to be RARE** — AWS almost always has the services and the AWS-side estimate can almost always be produced; when in doubt, prefer `conditional_go` with named conditions.

**Hard triggers — any one forces `outcome: "defer_for_evidence"`:**

| # | Trigger | Evidence to name |
| - | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| 1 | GovCloud-class compliance ambiguity: compliance unknown AND signals suggest FedRAMP/government requirements — GovCloud vs commercial changes regions, service catalog, and pricing wholesale | Compliance confirmation from the user's legal/compliance owner |
| 2 | The user's **only** stated motivation is cost savings AND no spend signal exists at all (`billing_profile.available == false` AND the user declined to state Heroku spend) | Heroku invoice/billing export, or a stated monthly spend figure |

**Soft triggers — never force defer; add each to `conditions[]` (outcome becomes `conditional_go` instead of `go`) and to `would_flip_if[]`:**

| # | Trigger | Condition wording (adapt to stack) |
| - | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| 3 | `database_ha` or availability posture resolved by default/plan-tier, never user-confirmed | "Confirm the availability requirement — Multi-AZ roughly doubles the database line" |
| 4 | `migration_approach` defaulted while the stack has Postgres over ~100GB or zero-downtime signals | "Confirm cutover approach — pg_dump default may not fit this database" |
| 5 | Pricing staleness beyond the vendored rate card's accuracy band | "Refresh pricing before treating the dollar delta as decision-grade" |

**Outcome derivation:**

```
IF any hard trigger fired -> outcome: "defer_for_evidence"
ELSE IF path == "stay" -> outcome: "stay"
ELSE IF any soft trigger fired -> outcome: "conditional_go" (conditions[] = fired soft triggers)
ELSE -> outcome: "go"
```

Complexity alone selects `path: "migrate_phased"` — it never moves `outcome` away from go/conditional_go. Populate `decision_basis` from provenance: billing/inventory-extracted values → measured; defaulted → assumed; declined/unknown → unknown. Populate `would_flip_if[]` with the 1–3 changes most likely to alter the outcome.

**Presenting a defer (lead with what IS established):** open with what the assessment did determine — "AWS can host this stack; the AWS-side estimate is $X–$Y/mo" — before naming the one missing piece of evidence and how to obtain it. Never present defer as "no answer."

### Persist recommendation to estimation-infra.json

@leon1418 leon1418 Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🤖 AI review 🤖]

Optional: The PR description states: "report renders the outcome as a typography-first headline (no colored pill badges, meaning never carried by color alone) with an end-of-summary decision-basis block" — but generate-report.md is unchanged on this branch and still has no reference to outcome, outcome_label, decision_basis, or would_flip_if.

The new schema fields are written here but never consumed in report rendering. Is this intentional for a follow-up (if so, suggest adding to "Out of scope"), or was it accidentally omitted from this PR?

Severity: worth acknowledging — the report currently won't surface the new decision vocabulary to stakeholders reading the HTML output.

```json
"recommendation": {
"path": "migrate_optimized|migrate_phased|stay",
"path_label": "Migrate with Optimizations|Phased Migration|Stay on Heroku",
"outcome": "go|conditional_go|defer_for_evidence|stay",
"outcome_label": "Go|Go, with conditions|Defer — get evidence|Stay on Heroku",
"roi_justification": "<one-sentence ROI case>",
"confidence": "high|medium|low",
"migrate_if": ["<factors specific to THIS stack>"],
"stay_if": ["<factors specific to THIS stack>"],
"conditions": ["<REQUIRED non-empty when outcome is conditional_go — one per fired soft trigger>"],
"decision_basis": {
"measured": ["<evidence from billing/inventory>"],
"assumed": ["<defaulted inputs feeding this estimate>"],
"unknown": ["<unconfirmed inputs>"]
},
"would_flip_if": ["<1-3 changes most likely to alter the outcome, with direction>"],
"next_steps": ["<actionable items>"]
}
```

Readers of pre-extension artifacts must tolerate absent v2 fields (`outcome`, `conditions`, `decision_basis`, `would_flip_if`) and fall back to `path`. `outcome: "stay"` only with `path: "stay"`.

**Path selection logic:**

| Scenario | `path` value | `path_label` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ _postconditions:
_on_failure: _halt_and_inform
- _assert: "recommendation.migrate_if and recommendation.stay_if are non-empty arrays"
_on_failure: _halt_and_inform
- _assert: "recommendation.outcome is one of {go, conditional_go, defer_for_evidence, stay}; conditions is a non-empty array when outcome is conditional_go; outcome may be stay only when path is stay"
_on_failure: _halt_and_inform
- _assert: "projected_costs.aws_monthly_balanced is a positive number"
_on_failure: _halt_and_inform
- _assert: "every service in aws-design.json services[] appears in the cost breakdown, or is listed as 'unpriced' in warnings"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Only after `HANDOFF_OK`, apply the phase-status update protocol (`INTERPRETER.md
Output to user:

```
Generate phase complete.
Phase 5 of 6 complete (Generate). Optional remaining: Feedback.

Artifacts produced:
• terraform/ — [N] Terraform files for AWS infrastructure
Expand Down
Loading