Skip to content

Tech debt: unify policy metadata extraction with graph resolution #716

Description

@trevor-vaughan

Tech debt: unify policy metadata extraction with graph resolution

Tech debt in internal/policy, introduced by PR #711 and deferred by agreement. Resolving it also closes #714 and #715 (review finding A4).

PR #711 added ExtractPolicyMetadata + extractBundleMetadata + extractSplitMetadata, which duplicate the existing ResolvePolicyGraph + resolveBundleGraph + resolveSplitGraph nearly line-for-line: same shape detection, same layer load/parse, same error strings. Only the return type differs, and every field the metadata path computes already exists on DependencyGraph.

Proposal: build the graph, project the metadata off it, delete the duplicate helpers.

Why deferred, why it still matters

Deferred because it's a refactor that also has to settle two behavioral questions (#714, #715), and PR #711 is scoped to the feature.

Still matters because the two copies sit ~100 lines apart with no compiler link. Change the shape handling or the bundle keys in one and not the other, and the graph consumers (scan, doctor) and metadata consumers (list, get) silently disagree about the same policy. Cheapest to collapse now, while the copy is new and has no other callers.

The refactor

1. Add Title to DependencyGraph and set it in both resolve*Graph (one line each; policyLayerResult.Title already exists):

graph.EvaluatorID = policyLayer.EvaluatorID
graph.Title = policyLayer.Title            // add
graph.Assessments = append(graph.Assessments, policyLayer.Assessments...)

2. Make ExtractPolicyMetadata a projection and delete the two extract* helpers:

func (r *Resolver) ExtractPolicyMetadata(policyID, version string) (PolicyMetadata, error) {
    graph, err := r.ResolvePolicyGraph(policyID, version)  // best-effort mode: see step 3
    if err != nil {
        return PolicyMetadata{}, err
    }
    meta := PolicyMetadata{
        Title:           graph.Title,
        EvaluatorID:     graph.EvaluatorID,
        AssessmentCount: len(graph.Assessments),
    }
    if len(graph.Controls) > 0 && graph.Controls[0].Parsed != nil {
        meta.ControlCount = len(graph.Controls[0].Parsed.Controls)
    }
    return meta, nil
}

3. Pick one error policy for the shared path. This is the real work: the graph path is stricter than the metadata path, so consolidating forces a decision.

Condition scan / doctor Display Owner
Catalog present but invalid fail best-effort, ControlCount = 0 A2
Catalog load fails distinguish absent vs I/O same #715
No assessment-plans product decision show title/controls anyway #714

Suggested shape: a Strict vs BestEffort mode on the graph builder, so scan/doctor stay strict and display degrades gracefully, with the leniency living in one place.

Done when

References

Duplication at resolver.go:99-221 (PR head 4cad011d); reuse :223-357. Absent on base f61e943. From the PR #711 review (finding A4).


Related issues: #714, #715

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    Priority

    Medium

    Effort

    None yet

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions