fix(gcp-to-aws): read compliance from design_constraints, not a top-level key - #319
herosjourney wants to merge 3 commits into
Conversation
…evel key gcp-to-aws baseline Generate and infra Estimate gated the Config/Security Hub compliance controls (and the security_baseline_compliance cost line) on `preferences.json.compliance` — a TOP-LEVEL key. Clarify never writes that key: it writes the declared frameworks to `design_constraints.compliance.value` (the same canonical shape `design-ai.md`, `design-billing.md`, `migration-complexity.md`, `generate-artifacts-ai.md`, and `networking.md` already read). So a founder who declared soc2/pci/hipaa/fedramp always hit the empty-compliance branch: baseline.tf omitted the Config/Security Hub section and the estimate omitted its compliance cost line — silently dropping controls the user asked for. Align the two outlier readers to the canonical location, matching the five sibling readers that already use it: - generate-artifacts-infra.md: new Step 0 normalizes `design_constraints.compliance.value` (absent / none / unknown -> []), and the baseline.tf descriptions reference the same shape; every downstream "compliance contains …" resolves against the normalized array. - estimate-infra.md: the security_baseline_compliance gate reads the same location. Scope: exactly the two infra readers were wrong (verified — every other GCP compliance reader already uses design_constraints.compliance; no GCP fixture sets a compliance value, and no top-level `compliance` key exists in any fixture's preferences.json, which is why the bug never tripped a golden). This is a consistency fix to the established pattern, not a new mechanism. Byte-identical across both plugin trees; drift, fmt, lint:md, lint:frontmatter, lint:types, fixtures:assert, and bandit all green. Follow-up: with GCP controls now firing, migrate/README.md's GCP compliance assurance (qualified out in awslabs#311 because it wasn't true) can be restored.
ayn-builds
left a comment
There was a problem hiding this comment.
Review: align GCP compliance reads to design_constraints
Diagnosis is right and the fix is correct. Verified independently:
schema-preferences.mdmandates the{value, chosen_by, prompt, design_consequence}wrapper for everydesign_constraintskey, and Clarify writesdesign_constraints.compliance(full-flow Q2 inclarify-global.md:73-84, AI-only Q1.5 inclarify-ai-only.md:141/:359). Nothing writes a top-levelcompliance.- Post-fix grep over the repo: zero remaining top-level reads. The only hits are the new "do NOT read" warnings.
.valueis the correct leaf, andgenerate-artifacts-report.md:283already usescompliance.value, so the new text is stricter than the sibling readers cited in the description (those use the looserdesign_constraints.complianceshorthand), not divergent from them.- The new Step 0 tracks
heroku-to-aws/.../generate-terraform.md:145almost word for word, and its none/unknown/absent semantics matchdesign-ai.md:81and Clarify's["unknown"]contract. - Both trees byte-identical for both files. Heroku unaffected (
preferences.json.global.compliance, scalar-or-array). Fixture claim holds: only heroku fixtures carry compliance (global.compliance: "none"), no GCP fixture declares a framework, so no golden churn.
Worth strengthening in the Problem section
The miss was visible in the output, not only silent. generate-artifacts-report.md:283 fires the Section 4 compliance-controls note, and Appendix G renders "Compliance-conditional (only when SOC 2/PCI/HIPAA/FedRAMP declared in preferences)" listing AWS Config and Security Hub + FSBP with monthly costs (:312-316). Both are gated on the declared preference, which reads the correct shape. So pre-fix a SOC 2 run shipped a report documenting Config and Security Hub as part of the baseline while baseline.tf contained neither and the estimate omitted the cost line. That is a documented-vs-generated contradiction, a stronger statement of impact than "silently does not."
On the coverage gap
Agreed a GCP compliance golden is out of scope here. The cheap durable guard is a grep invariant in mise.toml next to drift:check that fails on any top-level preferences.json.compliance read (excluding the "do NOT read" warning line), in the same spirit as the reviewer-workflow-invariants check. Follow-up alongside the migrate/README.md restore.
Three inline notes below, all nits - nothing blocking.
|
|
||
| 1. **Compute retention.** Read `preferences.json.compliance` (array of strings; may be absent or empty). Compute `cloudtrail_retention_days` using this mapping, taking `max()` across all declared values (use 90 if the array is empty or absent): | ||
| - absent / `[]` → 90 | ||
| 0. **Normalize compliance.** Read `preferences.json` → `design_constraints.compliance.value` — the canonical location Clarify writes (full-flow Q2 / AI-only Q1.5), the same shape `design-ai.md`, `design-billing.md`, and `migration-complexity.md` already read. It is an array of framework strings, or absent. Normalize to an array: absent, `["none"]`, or `["unknown"]` → `[]` (an absent or unconfirmed answer is not a framework — `unknown` is a defaulted, never-user-confirmed value that behaves like `none` for control selection); otherwise lowercase the entries, dropping any `none`/`unknown`. Every reference to `compliance` below means this normalized array. (Do NOT read a top-level `preferences.json.compliance` key — Clarify never writes one, so that read always yields empty and silently skips the controls the user asked for.) |
There was a problem hiding this comment.
Step 3.0's caller context at line 297 is the one read in this file still pointing at the raw location:
compliance— the value ofpreferences.json→design_constraints.compliance(array;
may be empty/absent).
Two things: per schema-preferences.md that path resolves to the {value, chosen_by, prompt, design_consequence} wrapper, not an array, and it bypasses this Step 0, so ["unknown"]/["none"] enter the set the skill is handed.
Not behavioral - tf-best-practices/references/security-posture-rules.md:280-292 gates emissions on membership of soc2/pci/hipaa/fedramp and treats a non-gating set like empty, so nothing wrong gets emitted. But heroku's counterpart is already precise (heroku-to-aws/.../generate-terraform.md:82: "the normalized compliance array (see Step 1.5 item 0 ...)"), and Step 3.0 runs after Step 1.5, so this can just say the same thing:
- **compliance** — the normalized compliance array from Step 1.5 item 0 (empty ⇒ the skill emits no compliance-conditional hardening ...)
Same line in the migrate/ copy.
There was a problem hiding this comment.
Done in cee210a4 — Step 3.0's caller-context compliance bullet now reads "the normalized compliance array from Step 1.5 item 0 (read from design_constraints.compliance.value, with none/unknown/absent normalized to [])", matching heroku's precise wording so ["unknown"]/["none"] no longer enter the set. Both copies.
| 1. **Compute retention.** Read `preferences.json.compliance` (array of strings; may be absent or empty). Compute `cloudtrail_retention_days` using this mapping, taking `max()` across all declared values (use 90 if the array is empty or absent): | ||
| - absent / `[]` → 90 | ||
| 0. **Normalize compliance.** Read `preferences.json` → `design_constraints.compliance.value` — the canonical location Clarify writes (full-flow Q2 / AI-only Q1.5), the same shape `design-ai.md`, `design-billing.md`, and `migration-complexity.md` already read. It is an array of framework strings, or absent. Normalize to an array: absent, `["none"]`, or `["unknown"]` → `[]` (an absent or unconfirmed answer is not a framework — `unknown` is a defaulted, never-user-confirmed value that behaves like `none` for control selection); otherwise lowercase the entries, dropping any `none`/`unknown`. Every reference to `compliance` below means this normalized array. (Do NOT read a top-level `preferences.json.compliance` key — Clarify never writes one, so that read always yields empty and silently skips the controls the user asked for.) | ||
| 1. **Compute retention.** From the normalized `compliance` array, compute `cloudtrail_retention_days` using this mapping, taking `max()` across all declared values (use 90 when the array is empty): |
There was a problem hiding this comment.
(Anchored here; the line this is about is 418, outside the diff.)
Self-check at line 418, now that this gate actually fires:
- If
complianceis empty, absent, or contains only gdpr,baseline.tfdoes NOT contain anyaws_config_*oraws_securityhub_*resources.
Two small drifts against the new Step 0. "absent" is no longer reachable (Step 0 normalizes absent to []), and ccpa is missing: Q2 option 7 writes ["ccpa"] (clarify-global.md:78), which is non-gating, so a ccpa-only run correctly emits no controls but is not covered by this assertion.
Suggest: "If compliance is empty or contains only non-gating frameworks (gdpr, ccpa), ...". Same line in the migrate/ copy.
There was a problem hiding this comment.
[🤖 AI review 🤖] [P2] Define retention for the CCPA value now reaching the lookup
The CCPA case also exposes a gap before the self-check: Q2 option 7 writes design_constraints.compliance.value = ["ccpa"], which the new normalization preserves. Step 1.5 item 1 then requires max() across the declared frameworks, but its table has no ccpa entry; the 90-day fallback applies only to an empty array. A valid CCPA-only selection therefore has no defined cloudtrail_retention_days, and mixed selections containing CCPA need unspecified handling too. Before this patch, the absent top-level key took the 90-day fallback.
Please define the CCPA mapping or an explicit non-gating fallback in both generator copies. Verify CCPA-only resolves to a positive integer without Config/Security Hub, and CCPA + HIPAA preserves the HIPAA maximum. This concerns the generator's lookup, not a statutory CCPA retention requirement. Verified from the exact producer/table and independent source-derived probes; no deployment was run.
There was a problem hiding this comment.
Fixed in cee210a4. Added a ccpa → 365 row to the Step 1.5 item 1 retention table and generalized the 90-day fallback so it applies to any array that yields no mapping, not only []. So:
- CCPA-only (
["ccpa"]) →cloudtrail_retention_days = 365, and — since ccpa is non-gating — no Config/Security Hub controls, matching the self-check. - CCPA + HIPAA →
max(365, 2190) = 2190, HIPAA maximum preserved. - Any lone non-gating framework not in the table still floors to 90 rather than being undefined.
Applied in both the advisor/ and migrate/ copies (byte-identical).
There was a problem hiding this comment.
Done in cee210a4. Self-check now reads "If compliance is empty or contains only non-gating frameworks (gdpr, ccpa), ..." — dropped the now-unreachable "absent" (Step 0 normalizes it to []) and added ccpa so the ccpa-only case (which correctly emits no controls) is covered by the assertion. Both copies.
There was a problem hiding this comment.
[🤖 AI review 🤖] Verified fixed at 2cd2fdc2efeb5a5d2394f76c420788cae50b1ee9 in both plugin copies. CCPA-only resolves to 365 days without Config/Security Hub; CCPA + HIPAA retains 2190 days; empty or unmapped sets fall back to 90. The normalized authoring handoff and non-gating self-check are also explicit now. Source-derived checks cover all 64 combinations of the six supported frameworks plus default/boundary cases. This addresses the retention finding; no cloud deployment was performed.
| ## Part 2: Calculate Projected AWS Costs | ||
|
|
||
| **Security baseline coverage (always required):** Add a `security_baseline` entry to `projected_costs.breakdown` with `service: "AWS Security Baseline (Tier 1)"`, low/mid/high estimates of $3/$15/$30 per month, `accuracy: "±25%"`, and a `components` sub-object breaking down CloudTrail S3 storage (~$1.50/mo mid), GuardDuty (~~$13/mo mid after free trial), AWS Budgets ($0), and the free controls. If `preferences.json.compliance` contains any of `soc2`, `pci`, `hipaa`, `fedramp`, also add a sibling `security_baseline_compliance` entry with low/mid/high estimates of $3/$14/$25 per month, `accuracy: "±25%"`, `emission_reason` field citing the declared compliance values, and a `components` sub-object breaking down AWS Config (~$6/mo mid continuous), Config S3 storage (~~ $0.50/mo mid), Security Hub + FSBP (~$7/mo mid after free trial), and extra standards (free). Per-unit rates are grounded in the AWS Pricing API for us-east-1 as of 2026-05-04 (Config pricing effective 2025-09-01, Security Hub effective 2026-03-01). Cite source as `references/shared/pricing-cache.md § Security Baseline`. Both line items are added as flat additives to each tier total (Premium/Balanced/Optimized) rather than being tier-dependent. | ||
| **Security baseline coverage (always required):** Add a `security_baseline` entry to `projected_costs.breakdown` with `service: "AWS Security Baseline (Tier 1)"`, low/mid/high estimates of $3/$15/$30 per month, `accuracy: "±25%"`, and a `components` sub-object breaking down CloudTrail S3 storage (~$1.50/mo mid), GuardDuty (~~$13/mo mid after free trial), AWS Budgets ($0), and the free controls. If `preferences.json` → `design_constraints.compliance.value` (the canonical location Clarify writes — NOT a top-level `compliance` key, which Clarify never writes) contains any of `soc2`, `pci`, `hipaa`, `fedramp` (treat absent / `none` / `unknown` as empty), also add a sibling `security_baseline_compliance` entry with low/mid/high estimates of $3/$14/$25 per month, `accuracy: "±25%"`, `emission_reason` field citing the declared compliance values, and a `components` sub-object breaking down AWS Config (~$6/mo mid continuous), Config S3 storage (~~ $0.50/mo mid), Security Hub + FSBP (~$7/mo mid after free trial), and extra standards (free). Per-unit rates are grounded in the AWS Pricing API for us-east-1 as of 2026-05-04 (Config pricing effective 2025-09-01, Security Hub effective 2026-03-01). Cite source as `references/shared/pricing-cache.md § Security Baseline`. Both line items are added as flat additives to each tier total (Premium/Balanced/Optimized) rather than being tier-dependent. |
There was a problem hiding this comment.
Worth noting this now states the none/unknown/absent rule inline, which makes three copies of it (here, generate-artifacts-infra.md Step 1.5 item 0, and heroku's generate-terraform.md:145).
Estimate runs before Generate so it cannot cite Generate's Step 0, and I would not restructure for it in this PR. But if the semantics ever change, this copy is the easiest to miss - one option is to state the normalization once in schema-preferences.md under the compliance row and have all three cite it.
leon1418
left a comment
There was a problem hiding this comment.
[🤖 AI review 🤖]
Reviewed 64046abbd9fe8eac4dc9121108b1ce6ec52c36ce with OCR delegation, an independent general reviewer, repository contract tracing, and a completed evidence gate. The canonical .value read is correct. Two P2 corrections remain in generate-artifacts-infra.md, in both plugin copies:
- Define retention for CCPA-only and mixed declarations (Step 1.5 item 1). Details are in the existing CCPA thread.
- Derive the newly enabled Config managed-policy ARN from the target partition so FedRAMP/GovCloud output uses
aws-us-gov(Step 1.5 item 6).
Validation: source-derived framework/default/combination probes, exact mirror comparison, drift check, formatting, Markdown lint, git diff --check, and both fixture-asserter runners passed. The probes expose the two contract gaps; existing fixtures do not exercise compliance generation. No model-driven Terraform generation or AWS deployment was performed. CI is green at this head; GitHub still reports the branch behind, with approval and conversation requirements outstanding.
|
|
||
| 1. **Compute retention.** Read `preferences.json.compliance` (array of strings; may be absent or empty). Compute `cloudtrail_retention_days` using this mapping, taking `max()` across all declared values (use 90 if the array is empty or absent): | ||
| - absent / `[]` → 90 | ||
| 0. **Normalize compliance.** Read `preferences.json` → `design_constraints.compliance.value` — the canonical location Clarify writes (full-flow Q2 / AI-only Q1.5), the same shape `design-ai.md`, `design-billing.md`, and `migration-complexity.md` already read. It is an array of framework strings, or absent. Normalize to an array: absent, `["none"]`, or `["unknown"]` → `[]` (an absent or unconfirmed answer is not a framework — `unknown` is a defaulted, never-user-confirmed value that behaves like `none` for control selection); otherwise lowercase the entries, dropping any `none`/`unknown`. Every reference to `compliance` below means this normalized array. (Do NOT read a top-level `preferences.json.compliance` key — Clarify never writes one, so that read always yields empty and silently skips the controls the user asked for.) |
There was a problem hiding this comment.
[🤖 AI review 🤖] [P2] Use the target partition for the newly enabled Config policy
Recognizing fedramp here now enables Step 1.5 item 6, which specifies arn:aws:iam::aws:policy/service-role/AWS_ConfigRole. Q2 requires GovCloud for FedRAMP, whose ARNs use aws-us-gov (AWS ARN contract). The generated role attachment therefore targets a commercial-partition policy instead of the GovCloud policy and cannot apply as specified.
Derive this policy ARN from the provider partition, including the referenced partition data source, in both advisor/ and migrate/ copies. Check that commercial regions retain arn:aws:... and both GovCloud regions use arn:aws-us-gov:.... The literal predates this PR, but this corrected input read newly activates it for normal FedRAMP preferences.
There was a problem hiding this comment.
Fixed in cee210a4. Item 6 no longer hard-codes the aws partition — it now instructs adding a data "aws_partition" "current" {} source and setting policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWS_ConfigRole". Commercial regions resolve to arn:aws:... and GovCloud (which Q2 pins FedRAMP to) resolves to arn:aws-us-gov:..., so the role attachment applies as specified. Applied in both advisor/ and migrate/ copies.
There was a problem hiding this comment.
[🤖 AI review 🤖] Verified fixed at 2cd2fdc2efeb5a5d2394f76c420788cae50b1ee9 in both plugin copies. Item 6 declares data "aws_partition" "current" {} when needed and uses its partition attribute in the Config role policy ARN. Source-derived rendering for commercial regions and both GovCloud regions matches the frozen HashiCorp/AWS contracts and preserves AWS_ConfigRole. This addresses the partition finding; validation did not include Terraform provider execution or an AWS deployment.
…y partition Address review P2s on generate-artifacts-infra.md Step 1.5 (both plugin copies): - CCPA retention: Q2 option 7 writes design_constraints.compliance.value = ["ccpa"], which the normalization preserves, but the item-1 retention table had no ccpa row and the 90-day fallback only covered an empty array — leaving ccpa-only (and ccpa-mixed) runs with no defined cloudtrail_retention_days. Add ccpa -> 365 (non-gating: sets retention, emits no Config/Security Hub) and generalize the 90-day fallback to any array that yields no mapping. ccpa+hipaa still resolves to the hipaa max() as before. - FedRAMP Config policy ARN: item 6 hard-coded arn:aws:...:policy/service-role/AWS_ConfigRole. Q2 pins fedramp to GovCloud, whose ARNs use the aws-us-gov partition, so the attachment could not apply as specified. Derive the ARN from data.aws_partition.current.partition so commercial keeps arn:aws and GovCloud gets arn:aws-us-gov. Also address doc-consistency nits: - Step 3.0 caller-context: point compliance at the normalized array from Step 1.5 item 0 (design_constraints.compliance.value), matching heroku's wording. - Self-check: 'empty or contains only non-gating frameworks (gdpr, ccpa)' — drop the now-unreachable 'absent' (Step 0 normalizes it to []) and cover ccpa.
leon1418
left a comment
There was a problem hiding this comment.
[🤖 AI review 🤖]
Verified 2cd2fdc2efeb5a5d2394f76c420788cae50b1ee9 with OCR delegation, one fresh independent general reviewer, parent contract review, and the completed convergence-v1 evidence gate.
Both earlier findings are fixed: CCPA retention is defined, and the Config policy ARN uses the target partition. I replied in their original threads. The normalized Terraform authoring handoff and non-gating self-check are also corrected.
One P2 remains: a saved decision estimated before the canonical compliance read can resume into the new generator without refreshing its estimate, so generated controls and the reported costs/budget disagree. I missed this existing resume path in the previous review. The consolidated correction is a retained-estimate compatibility guard in both generate-artifacts-infra.md copies, with no unnecessary repricing of compatible runs; details are inline.
Validation: source-derived framework/partition probes; a separate four-revision resume trace; actual report-validator positive/negative controls; drift, shared-file, formatting, Markdown, GCP frontmatter, both fixture suites, and whitespace checks. No model-driven generation/resume or AWS deployment was run. Approval and any change to the blocking review state remain for the user.
|
|
||
| 1. **Compute retention.** Read `preferences.json.compliance` (array of strings; may be absent or empty). Compute `cloudtrail_retention_days` using this mapping, taking `max()` across all declared values (use 90 if the array is empty or absent): | ||
| - absent / `[]` → 90 | ||
| 0. **Normalize compliance.** Read `preferences.json` → `design_constraints.compliance.value` — the canonical location Clarify writes (full-flow Q2 / AI-only Q1.5), the same shape `design-ai.md`, `design-billing.md`, and `migration-complexity.md` already read. It is an array of framework strings, or absent. Normalize to an array: absent, `["none"]`, or `["unknown"]` → `[]` (an absent or unconfirmed answer is not a framework — `unknown` is a defaulted, never-user-confirmed value that behaves like `none` for control selection); otherwise lowercase the entries, dropping any `none`/`unknown`. Every reference to `compliance` below means this normalized array. (Do NOT read a top-level `preferences.json.compliance` key — Clarify never writes one, so that read always yields empty and silently skips the controls the user asked for.) |
There was a problem hiding this comment.
[🤖 AI review 🤖] [P2] Check retained estimate coverage before enabling controls on resume
A run can finish Estimate under the old top-level reader with design_constraints.compliance.value = ["soc2"], then stop at the supported decide-complete state. After updating the skill, accepting Generate loads this corrected reader while SKILL.md explicitly preserves the old estimate. Config/Security Hub are now emitted, but estimation-infra.json still lacks security_baseline_compliance; the budget and final report reuse those understated totals. Neither Generate's entry checks nor the pre-report gate catches that mismatch.
Add a compatibility check before conditional emission in both generator copies. If the saved estimate lacks the required compliance entry, stop with a targeted request to refresh Estimate through the authorized flow. Compatible estimates should resume without recomputation or double-counting, and non-gating inputs should remain unaffected.
This path already existed at 64046ab; I missed it in the previous review. The latest CCPA and partition corrections are verified fixed. This finding is supported by the explicit resume/source contracts and labeled simulations, not a live generator run.
|
Heads up: the The plugin now lives in the Agent Toolkit for AWS, merged to We are beginning plans to deprecate this repository, so that's the copy to build on going forward — changes landed here won't reach customers once distribution repoints. Please re-open this PR against Porting your diff: paths move from
Happy to help with the move if anything doesn't map cleanly. |
|
Ported to aws/agent-toolkit-for-aws#341 per the move to the new plugin home. Paths remapped from |
…aws#341) * fix(aws-startup-advisor): read GCP compliance from design_constraints, not a top-level key Ported from awslabs/startups#319. gcp-to-aws Clarify writes declared frameworks to preferences.json -> design_constraints.compliance.value, but generate-artifacts-infra.md and estimate-infra.md read a top-level preferences.json.compliance key that Clarify never writes. Declared SOC 2 / PCI / HIPAA / FedRAMP answers therefore silently landed in the empty-compliance branch: no Config/Security Hub section in baseline.tf and no compliance cost line in the estimate. Align both readers to design_constraints.compliance.value (the shape the other GCP compliance readers already use), normalize none/unknown/absent to [], add a ccpa -> 365 retention row with a generalized 90-day fallback, and derive the Config role policy ARN from aws_partition so FedRAMP (GovCloud) runs resolve the correct partition. * docs(aws-startup-advisor): document the security-baseline breakdown entries in the estimate schema Schema-conformance sweep: this PR emits `security_baseline` (always) and `security_baseline_compliance` (when a gating framework is declared) entries in projected_costs.breakdown[], but schema-estimate-infra.md documented neither — the entries were shape-conformant (they follow the observability-entry pattern) but undocumented in the contract other consumers read. Added a "Security Baseline Entries" subsection with both shapes + component keys + validation, and listed the security baseline in the breakdown-coverage assertion. Verified: markdownlint 0 issues, mise run build exit 0. --------- Co-authored-by: Logan Kleier <lkleier@amazon.com>
Problem
When a founder migrating from GCP says they have a compliance requirement (SOC 2, PCI, HIPAA, or FedRAMP), the tool is supposed to add the matching security controls — AWS Config and Security Hub — to their generated infrastructure, and include their cost in the estimate. Today it silently doesn't. The founder answers the compliance question, but the generated
baseline.tfcomes out without those controls and the estimate omits their cost, with no warning. So someone who asked for a compliance-ready setup quietly gets one that isn't.The cause is a plumbing mismatch: the step that records the founder's answer saves it in one place, and the steps that generate the Terraform and the estimate look for it somewhere else — so they always see "no compliance requirement" and skip the controls. This is the underlying bug behind the README claim that PR #311 had to walk back (the README promised GCP compliance controls that never actually shipped).
In code, for reviewers
Clarify writes the declared frameworks to
preferences.json→design_constraints.compliance.value. Butgenerate-artifacts-infra.md(Step 1.5 retention + the compliance-conditionalbaseline.tfsection) andestimate-infra.md(thesecurity_baseline_compliancecost line) read a top-levelpreferences.json.compliancekey that Clarify never writes. A normal declared-framework answer therefore lands in the empty-compliance branch: the Config/Security Hub section is not emitted and the compliance cost line is dropped.Solution
Align the two readers to
design_constraints.compliance.value— the canonical shape the other five GCP compliance readers already use (design-ai.md,design-billing.md,migration-complexity.md,generate-artifacts-ai.md,networking.md):generate-artifacts-infra.md: a new Step 0 normalizesdesign_constraints.compliance.value(absent /none/unknown→[]); thebaseline.tfdescriptions reference the same shape; every downstream "compliancecontains …" resolves against the normalized array.estimate-infra.md: thesecurity_baseline_compliancegate reads the same location.Blast radius verified: I traced every GCP compliance reader — only these two used the top-level shape; all others already read
design_constraints.compliance. So this is a consistency fix bringing two outliers in line with the established pattern, not a new mechanism.heroku-to-awsis unaffected (it readspreferences.json.global.compliance, which its Clarify writes).Validation
preferences.jsoncarries a top-levelcompliancekey; compliance lives underdesign_constraints— which is why the bug never tripped a golden (no GCP fixture declares a framework).migrateandadvisortrees. Green locally:drift:check,fmt:check,lint:md,lint:frontmatter,lint:types,fixtures:assert(22 ok),security:bandit.Follow-up: with GCP controls now firing,
migrate/README.md's GCP Config/Security Hub assurance — qualified out in #311 — can be restored.Type of Change
Team Folder
advisor/migrate/solution-architecture/Checklist
mise run buildlocally and it passesBoth plugin folders are required to keep the distributed skill copies aligned.