Summary
Phase B of #27 — clean up the abstraction now that Phase A has shipped two
policy engines side-by-side.
Phase A (#27, landed in feat: add Cedar as a second policy engine,
db5b843) deliberately duplicated structure to stay backward-compatible:
Finding::rego_stub and Finding::cedar_stub are parallel optional fields
extract_rego / extract_cedar are parallel functions in
src/commands/extract.rs with engine-specific dispatch from
match args.engine { ... }
- MCP
suggest_policy and validate_policy match engine { Rego => ..., Cedar => ... } over each branch of the call
[rule.rego_template] and [rule.cedar_template] are parallel TOML blocks
That's the right shape for two engines. It will not scale to N engines without
becoming a nuisance.
Scope
1. Extract a PolicyGenerator trait
pub trait PolicyGenerator {
fn engine(&self) -> PolicyEngine;
fn validate(&self, policy: &str) -> ValidationResult;
fn wrap_by_confidence(&self, body: &str, confidence: Confidence) -> String;
fn group_and_generate(&self, findings: &[Finding], opts: &ExtractOpts) -> Vec<PolicyFile>;
}
RegoGenerator and CedarGenerator implement it. extract::execute becomes
generic over dyn PolicyGenerator, dispatched off the --engine flag at one
point of entry. Same refactor for the MCP tools (suggest_policy /
validate_policy collapse onto the trait).
2. Collapse parallel *_stub fields into policy_outputs
Replace Finding::rego_stub and Finding::cedar_stub with:
pub struct PolicyOutput {
pub engine: PolicyEngine,
pub content: String,
}
pub struct Finding {
// ...
pub policy_outputs: Vec<PolicyOutput>,
}
Backward compatibility is part of this work item, not a separate gate.
Ship a serde deserialization shim that reads legacy rego_stub /
cedar_stub fields and folds them into policy_outputs so existing
findings files keep loading. Output continues to populate policy_outputs
only — old fields are write-removed but read-tolerated.
3. Same treatment for the rule TOML format
Replace [rule.rego_template] and [rule.cedar_template] with a single
[[rule.policy_templates]] array:
[[rule.policy_templates]]
engine = "rego"
template = """..."""
[[rule.policy_templates]]
engine = "cedar"
template = """..."""
Same migration story — accept both shapes during parse, fold the legacy
keys into the new structure.
Out of scope
- New engines (Phase B is a refactor, not a feature add)
- Auto-translation between engines
- Cedar runtime / enforcement
- Engine-selection heuristics
Risks
- MCP tool-name churn.
suggest_rego / validate_rego aliases stay; this
refactor is internal.
- Output-shape change for downstream JSON consumers. Anything reading
rego_stub / cedar_stub off zift's stdout will need to read
policy_outputs instead. Document explicitly in the changelog. The input
shim handles persisted files; it does not paper over consumer code.
References
Summary
Phase B of #27 — clean up the abstraction now that Phase A has shipped two
policy engines side-by-side.
Phase A (#27, landed in
feat: add Cedar as a second policy engine,db5b843) deliberately duplicated structure to stay backward-compatible:
Finding::rego_stubandFinding::cedar_stubare parallel optional fieldsextract_rego/extract_cedarare parallel functions insrc/commands/extract.rswith engine-specific dispatch frommatch args.engine { ... }suggest_policyandvalidate_policymatch engine { Rego => ..., Cedar => ... }over each branch of the call[rule.rego_template]and[rule.cedar_template]are parallel TOML blocksThat's the right shape for two engines. It will not scale to N engines without
becoming a nuisance.
Scope
1. Extract a
PolicyGeneratortraitRegoGeneratorandCedarGeneratorimplement it.extract::executebecomesgeneric over
dyn PolicyGenerator, dispatched off the--engineflag at onepoint of entry. Same refactor for the MCP tools (
suggest_policy/validate_policycollapse onto the trait).2. Collapse parallel
*_stubfields intopolicy_outputsReplace
Finding::rego_stubandFinding::cedar_stubwith:Backward compatibility is part of this work item, not a separate gate.
Ship a
serdedeserialization shim that reads legacyrego_stub/cedar_stubfields and folds them intopolicy_outputsso existingfindings files keep loading. Output continues to populate
policy_outputsonly — old fields are write-removed but read-tolerated.
3. Same treatment for the rule TOML format
Replace
[rule.rego_template]and[rule.cedar_template]with a single[[rule.policy_templates]]array:Same migration story — accept both shapes during parse, fold the legacy
keys into the new structure.
Out of scope
Risks
suggest_rego/validate_regoaliases stay; thisrefactor is internal.
rego_stub/cedar_stuboff zift's stdout will need to readpolicy_outputsinstead. Document explicitly in the changelog. The inputshim handles persisted files; it does not paper over consumer code.
References
docs/CEDAR_SUPPORT.md§ "Phase B — clean up the abstraction"