Skip to content

SSRF root precedence is positional (argv), not declared in the data #5

Description

@emuehlstein

Summary

The README states that inputs are "merged deterministically", and they are — but
precedence is carried entirely by the order of --ssrf-root CLI arguments.
No root declares its own position in the merge, so correctness lives in the
invocation rather than in the data.

This works today because there is exactly one overlay and the documented command
happens to be correct. It becomes a real hazard as soon as there are two
overlays, or a second person runs the build.

Reproduction

Documented order (authoritative first, overlay last) — the asg_wx1 patch applies:

$ codeplugger-profile profiles/baofeng_dm32/reference.yml \
    --ssrf-root ../ssrf-lite/ssrf \
    --ssrf-root ../chioff-ssrf-private/ssrf
...
  notes: Synthetic note applied by the public reference overlay.

Reversed:

$ codeplugger-profile profiles/baofeng_dm32/reference.yml \
    --ssrf-root ../chioff-ssrf-private/ssrf \
    --ssrf-root ../ssrf-lite/ssrf

error: ../chioff-ssrf-private/ssrf/overrides/reference_weather.yml:
       unknown assignments override target 'asg_wx1'

What is already good

The failure above is correct behavior, and worth preserving. In
ssrf/overlays.py, override targets are filtered to strictly earlier roots:

targets = [
    target
    for target in index.get((collection, entity_id), [])
    if target[2] < root_index
]

So an overlay cannot patch a record that has not been loaded yet, and ambiguous
targets raise rather than silently picking one. That is the right model. The gap
is not the merge semantics — it is that nothing binds a root to its intended
precedence
.

The gap

Two failure modes that the current design cannot catch:

  1. Silent wrong-order resolution between overlays. If two overlays both patch
    the same ID, whichever is passed later wins. Both orders resolve without error
    and produce different codeplugs. There is no signal that anything is wrong.

  2. Additive-only overlays reorder silently. The root_index guard only fires
    for overrides: blocks. An overlay that only adds entities (like
    reference_simplex.yml) resolves fine in any position, so a mis-ordered
    invocation is not necessarily caught by the existing check.

The practical risk: a generated codeplug is a build artifact, and the README is
explicit that it must not become an authoritative source. But if the artifact
depends on argv ordering, then reproducing a build requires knowing the exact
command someone typed, which is not recorded anywhere in the data.

Proposed direction

Let each root declare its own precedence, and have the resolver sort rather than
trust argv. Rough shape — naming open for discussion:

# ssrf/_root.yml (or a top-level key in existing documents)
ssrf_root:
  id: "chioff_reference_overlay"
  precedence: 100        # higher wins; authoritative data is 0

Then:

  • resolve_ssrf_roots() sorts by declared precedence, ignoring argument order.
  • Duplicate precedence values across roots raise, rather than falling back to argv.
  • A root with no declaration keeps today's positional behavior, so this stays
    backward compatible.

Alternative if a manifest feels heavy: keep argv ordering but require roots to be
named (--ssrf-root authoritative=../ssrf-lite/ssrf) and have profiles declare
which named roots they expect. That moves the contract into the profile instead
of the root.

Either way the goal is the same: a mis-ordered build should be impossible, not
merely detectable in one direction.

Scope note

This is a ssrf-lite change more than a codeplugger one — resolve_ssrf_roots
lives in ssrf/overlays.py. Filing here because codeplugger is the consumer
that exposes the ordering to users via CLI flags, and because the fix should be
designed against how profiles actually get built. Happy to move it if you would
rather track it upstream.

Low urgency while there is one overlay. Worth settling before the pattern
propagates to real user repos, since username-ssrf-private plus a club or
regional overlay is exactly the two-overlay case above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions