GitHub

How to resolve an outcomes collision

You ran nwave-ai outcomes check-delta (or check) and it exited 1. The CLI flagged your candidate against an existing OUT-id. This guide walks you through the three-step triage that resolves the flag.

If you have never registered an outcome, start with the Your First Outcome tutorial instead — this guide assumes you already understand the verdict matrix.

Prerequisites

  • The CLI exited 1 with one of:
    • COLLISION: OUT-X (Tier-1 + Tier-2 <score>)
    • AMBIGUOUS: OUT-X (Tier-1 only) or (Tier-2 <score> only)
  • You have write access to docs/product/outcomes/registry.yaml.

Step 1: Inspect the colliding outcome

Look up the existing outcome the CLI is pointing at. Open docs/product/outcomes/registry.yaml and find the entry whose id matches the flag. Note three fields:

  • summary — what does the existing outcome do?
  • artifact — where is its implementation?
  • keywords — what intent did the registrant capture?

Example: the CLI flagged COLLISION: OUT-E3 (Tier-1 + Tier-2 0.67). You read the entry:

- id: OUT-E3
  summary: Every CommitmentRow non-empty across 4 columns
  keywords: [non-empty, required, cell]
  artifact: nwave_ai/feature_delta/domain/rules/e3_non_empty_rows.py

You wrote your candidate intending it to detect cherry-picks (downstream row count exceeds upstream without a ratifying DDD). The summary says non-empty cells. They are different rules with overlapping vocabulary. Move to step 2.

Step 3: Re-run the check and confirm clean

After editing the registry, re-run the original check:

nwave-ai outcomes check \
  --input-shape "FeatureModel" \
  --output-shape "tuple[Violation, ...]" \
  --keywords "<your refined keywords>"

If the verdict is now NO COLLISIONS (exit 0), proceed.

If the verdict is still COLLISION and you have already supersede'd or annotated, that is expected — the CLI does not look at related or superseded_by when deciding to flag (those are documentation, not gates). What matters is that your decision is recorded in the registry, so the next author reads the chain instead of re-litigating.

Worked example: E3 vs E3b (cherry-pick)

This is the canonical case the registry was designed to catch.

E3 (OUT-E3 in the seeded registry): "Every CommitmentRow non-empty across 4 columns." Input: FeatureDeltaModel. Output: tuple[ValidationViolation, ...]. Keywords: non-empty, required, cell.

E3b (proposed during a later DISTILL wave): "Downstream wave row count >= upstream OR DDD-authorized cherry-pick." Same input. Same output shape. Keywords: cherry-pick, row-count, ddd.

Running the check on E3b:

nwave-ai outcomes check \
  --input-shape "FeatureDeltaModel" \
  --output-shape "tuple[ValidationViolation, ...]" \
  --keywords "cherry-pick,row-count,ddd"

Output:

AMBIGUOUS: OUT-E3 (Tier-1 only)

Exit 1.

Why ambiguous? Tier-1 fires (identical shape — both rules walk the same model and emit the same violation tuple) but Tier-2 does not ({cherry-pick, row-count, ddd}{non-empty, required, cell} = ∅, Jaccard = 0).

Resolution: This is the "annotate via related" case. The two rules genuinely check different predicates — they happen to share the violation-collection contract. Register E3b normally, then add related: [OUT-E3] to its entry. The annotation says "yes, we know these collide on shape; they are intentionally separate predicates."

Six months later, when an author proposes E3c with the same shape, the chain OUT-E3 → OUT-E3b → OUT-E3c makes the rationale visible.

What this guide does not cover

  • Auto-resolution via supersede sweep — out of scope for the MVP. The registry tracks supersession; cleaning up superseded artifacts is manual.
  • Cross-feature collision dashboards — defer to v2. Currently outcomes check-delta reports one feature at a time.
  • CI gate on collisions--strict flag is reserved for v2 (D-7 in the feature-delta DISCUSS).

If your situation is none of the above, file an issue with the exact CLI invocation and verdict so we can extend the decision tree.