Skip to content

feat(remediate): plan-yank + compromised-release receipt fields (#98 PR 2)#129

Closed
EffortlessSteven wants to merge 2 commits into
feat/98-yank-commandfrom
feat/98-plan-yank
Closed

feat(remediate): plan-yank + compromised-release receipt fields (#98 PR 2)#129
EffortlessSteven wants to merge 2 commits into
feat/98-yank-commandfrom
feat/98-plan-yank

Conversation

@EffortlessSteven

Copy link
Copy Markdown
Member

Summary

Next step on the Remediate pillar. PR 1 (#121) landed the single-crate `shipper yank` primitive; this PR composes it into a reverse-topological containment plan derived from a prior publish receipt.

Base branch is #121, not main. Merge #121 first.

What's new

  • Receipt schema (additive, zero migration): `PackageReceipt` gains three `Option` fields, all `#[serde(default, skip_serializing_if)]`:

  • `shipper::engine::plan_yank` module:

    • `PlanYankFilter::{AllPublished, CompromisedOnly}`
    • `build_plan(receipt, filter)` — reverse-topological containment plan
    • `render_text(plan)` — copy-pasteable `shipper yank ...` command lines
    • `load_receipt_from_path` — receipt loader for arbitrary paths
  • `shipper plan-yank` CLI:
    ```
    shipper plan-yank [--from-receipt PATH] [--compromised-only]
    ```
    Honors `--format text|json`. Default `text` output is directly
    pipeable into `sh` (with some review) once you've filled in the
    `` placeholders.

Example

```
$ shipper plan-yank --from-receipt .shipper/receipt.json --compromised-only

yank plan (reverse topological) — registry=crates-io, plan_id=abc, filter=compromised_only

2 entries

  1. shipper yank --crate app --version 0.1.0 --reason # CVE-2026-0001
  2. shipper yank --crate core --version 0.1.0 --reason # CVE-2026-0001
    ```

Scope — explicitly NOT in this PR

Why reverse-topological

For A → B → C (A leaf, B depends on A, C depends on B):

  • Publish order: A, B, C
  • Yank order: C, B, A

Dependents first so downstream consumers stop resolving against the compromised chain before the root crate itself is pulled.

Tests

  • 5 `plan_yank::tests`: reverses order / excludes Failed/Skipped / compromised-only filter / empty / text-render order invariant
  • CLI `plan-yank --help` snapshot

Companion churn

Every `PackageReceipt` construction site (engine code + state/execution tests + integration tests) sets the three new Option fields to `None`. Additive schema, no runtime semantics change.

Test plan

  • `cargo test -p shipper --lib plan_yank` — 5/5 pass
  • `cargo clippy --workspace --all-targets --all-features -- -D warnings` clean
  • `cargo fmt --all --check` clean
  • `shipper plan-yank --help` renders correctly
  • CI multi-OS green (once feat(yank): shipper yank <crate>@<version> (#98 PR 1) #121 merges and this rebases on main)

…ds (#98 PR 2)

Next step on the Remediate pillar. PR 1 (#121) landed the single-crate
\`shipper yank\` primitive; this PR composes it into a reverse-topological
containment plan built from a prior publish receipt.

## What's new

- **Receipt schema** (additive): PackageReceipt gains three Option
  fields, all \`#[serde(default, skip_serializing_if = \"Option::is_none\")]\`
  so existing receipts read back unchanged:
  - \`compromised_at: Option<DateTime<Utc>>\` — when this version was
    marked compromised
  - \`compromised_by: Option<String>\` — operator-supplied reason (CVE,
    incident, free-form tag)
  - \`superseded_by: Option<String>\` — populated by \`shipper
    fix-forward\` (#98 PR 3)

- **\`shipper::engine::plan_yank\`** module:
  - \`PlanYankFilter::{AllPublished, CompromisedOnly}\` selector
  - \`YankPlan\`, \`YankEntry\` value types
  - \`build_plan(receipt, filter)\` — filter receipt.packages, reverse
    to get dependents-first order, emit entries
  - \`render_text(plan)\` — produces copy-pasteable \`shipper yank ...\`
    command lines, one per entry, with \`--reason <REASON>\` placeholders
    for the operator to fill in
  - \`load_receipt_from_path\` — receipt loader for arbitrary paths
    (default is \`<state_dir>/receipt.json\`)

- **\`shipper plan-yank\` CLI subcommand**:
  \`\`\`
  shipper plan-yank [--from-receipt PATH] [--compromised-only]
  \`\`\`
  Honors the global \`--format\` flag: \`text\` (default) prints the
  copy-pasteable yank-command list; \`json\` prints structured
  YankPlan for scripting.

## Scope — explicitly NOT in this PR

- **Plan execution** (wrapping \`shipper yank\` to run each entry) →
  #98 PR 3 along with fix-forward.
- **\`--mark-compromised\`** (populating the new receipt fields
  programmatically) → #98 PR 3. For now, operators can mark packages
  compromised by hand-editing receipt.json or via downstream tooling.
- **release_compromised state** (higher-level "this release is a
  write-off") → #98 PR 3.

## Why reverse topological, specifically

For workspace A → B → C (A is a leaf, B depends on A, C depends on B):

- Publish order: A, B, C
- **Yank order: C, B, A** — dependents first. New resolves for C will
  still pull B (if B is not yet yanked), but at least C itself is
  unresolvable. Yanking A first would be fine semantically but can
  mislead operators who read \`cargo search\` output.

Plus: reverse-topo matches the containment mental model users bring
from incident response — "pull the thing furthest from your reach first,
work back toward the root."

## Tests

5 unit tests in \`engine::plan_yank::tests\` (all green):

- reverses publish order for \`AllPublished\`
- excludes Failed/Skipped packages (only Published gets a yank entry)
- \`CompromisedOnly\` filter drops healthy packages, keeps only flagged
- empty receipt → empty plan with a helpful comment
- text-render ordering invariant: dependents appear before dependencies

Plus a CLI \`plan-yank --help\` snapshot in \`e2e_expanded\`.

## Companion churn

Mechanical: every PackageReceipt construction site (engine + state tests
+ integration tests) now sets the three new Option fields to None. The
schema is additive; no runtime semantics changed for existing code paths.
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cf9e8e9a-b317-42e6-9c9a-93c413b021d9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/98-plan-yank

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 68f4091188

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.map(|r| format!(" # {r}"))
.unwrap_or_default();
out.push_str(&format!(
"{:>3}. shipper yank --crate {} --version {} --reason <REASON>{reason}\n",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit executable shell lines in text yank plan

render_text prefixes every generated command with an ordinal like 1.. In sh/bash, that token is parsed as the command name, so shipper plan-yank ... | sh fails for any non-empty plan before shipper yank is invoked. This contradicts the command help that says text output can be piped through sh, and makes the default text plan unusable for its primary execution path.

Useful? React with 👍 / 👎.

Comment on lines +721 to +724
let receipt_path = from_receipt.unwrap_or_else(|| {
opts.state_dir
.join(shipper::state::execution_state::RECEIPT_FILE)
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve default receipt path from workspace root

When --from-receipt is omitted, plan-yank builds the path from opts.state_dir directly, unlike other commands that anchor a relative state_dir to planned.workspace_root. If the user runs from a different CWD (for example with --manifest-path /path/to/ws/Cargo.toml), this looks for .shipper/receipt.json in the wrong directory and reports a missing/unreadable receipt even though one exists under the workspace.

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented Apr 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.07595% with 33 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/shipper-cli/src/main.rs 0.00% 24 Missing ⚠️
crates/shipper/src/engine/plan_yank.rs 95.83% 6 Missing ⚠️
crates/shipper/src/engine/parallel/publish.rs 75.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

EffortlessSteven added a commit that referenced this pull request Apr 17, 2026
Close the remediation staged rollout: yank (PR 1) + plan-yank (PR 2) +
**fix-forward planning (this PR)** give operators the three building
blocks for handling a compromised release without improvising.

## What's new

### \`shipper yank --mark-compromised\`

New opt-in flag on the existing yank primitive. When set, after a
successful \`cargo yank\`, shipper also amends the matching
\`PackageReceipt\` in \`<state_dir>/receipt.json\` with:

- \`compromised_at = Utc::now()\`
- \`compromised_by = <--reason>\`

These are the fields added (additively) in PR 2 (#129). The amendment
lets downstream commands find compromised packages without scanning
events.jsonl.

Behaviour is tolerant:
- No receipt → warn + proceed (yank still succeeds)
- No matching package in receipt → warn + proceed
- Load/write errors → warn + proceed (yank already happened; a
  receipt-write failure doesn't retroactively unyank)

### \`shipper fix-forward --from-receipt <path>\`

New planning command. Reads a receipt, finds packages flagged
\`compromised_at = Some(_)\`, and prints an ordered supersession plan:

- Topological order (dependencies first, dependents last) — opposite
  of plan-yank, because fix-forward *publishes* replacements while
  plan-yank *removes* reachability.
- Suggested successor versions use a placeholder format (\`<old>-next\`)
  so the operator picks the real bump (patch / minor / major).
- Instructional preamble walks through the full remediation loop:
  bump Cargo.toml → commit → \`shipper publish\` → optional
  \`shipper plan-yank --compromised-only\` for containment.

Like plan-yank, fix-forward is **planning only**. It does not edit
Cargo.toml or invoke publish — those are operator steps.

## Tests

4 new tests in \`engine::fix_forward::tests\`:

- only compromised+Published packages produce steps (Failed-but-
  compromised packages are silently dropped — they were never shipped)
- topological order preserved from receipt.packages
- empty plan (no compromised packages) renders with guidance toward
  \`--mark-compromised\`
- text render enumerates steps with reason and points toward the
  publish + plan-yank follow-ups

Plus CLI snapshots for \`yank --help\` (gains \`--mark-compromised\`) and
\`fix-forward --help\`.

## Scope — explicitly NOT in this PR

- **Automated Cargo.toml bump.** Workspace-edit territory; needs its
  own PR with --dry-run, --apply, and git-cleanliness guards.
- **Publishing successors.** \`shipper publish\` already does this; fix-
  forward just tells the operator when to run it.
- **Chaining \`superseded_by\` back to the original receipt.** Requires
  post-publish receipt amendment from the successor run. Follow-on.
- **\`release_compromised\` top-level state.** The receipt-level marker.
  For now the per-package \`compromised_at\` fields cover the
  planning-level need.
EffortlessSteven added a commit that referenced this pull request Apr 17, 2026
Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
@EffortlessSteven EffortlessSteven deleted the branch feat/98-yank-command April 18, 2026 00:04
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Close the remediation staged rollout: yank (PR 1) + plan-yank (PR 2) +
**fix-forward planning (this PR)** give operators the three building
blocks for handling a compromised release without improvising.

## What's new

### \`shipper yank --mark-compromised\`

New opt-in flag on the existing yank primitive. When set, after a
successful \`cargo yank\`, shipper also amends the matching
\`PackageReceipt\` in \`<state_dir>/receipt.json\` with:

- \`compromised_at = Utc::now()\`
- \`compromised_by = <--reason>\`

These are the fields added (additively) in PR 2 (#129). The amendment
lets downstream commands find compromised packages without scanning
events.jsonl.

Behaviour is tolerant:
- No receipt → warn + proceed (yank still succeeds)
- No matching package in receipt → warn + proceed
- Load/write errors → warn + proceed (yank already happened; a
  receipt-write failure doesn't retroactively unyank)

### \`shipper fix-forward --from-receipt <path>\`

New planning command. Reads a receipt, finds packages flagged
\`compromised_at = Some(_)\`, and prints an ordered supersession plan:

- Topological order (dependencies first, dependents last) — opposite
  of plan-yank, because fix-forward *publishes* replacements while
  plan-yank *removes* reachability.
- Suggested successor versions use a placeholder format (\`<old>-next\`)
  so the operator picks the real bump (patch / minor / major).
- Instructional preamble walks through the full remediation loop:
  bump Cargo.toml → commit → \`shipper publish\` → optional
  \`shipper plan-yank --compromised-only\` for containment.

Like plan-yank, fix-forward is **planning only**. It does not edit
Cargo.toml or invoke publish — those are operator steps.

## Tests

4 new tests in \`engine::fix_forward::tests\`:

- only compromised+Published packages produce steps (Failed-but-
  compromised packages are silently dropped — they were never shipped)
- topological order preserved from receipt.packages
- empty plan (no compromised packages) renders with guidance toward
  \`--mark-compromised\`
- text render enumerates steps with reason and points toward the
  publish + plan-yank follow-ups

Plus CLI snapshots for \`yank --help\` (gains \`--mark-compromised\`) and
\`fix-forward --help\`.

## Scope — explicitly NOT in this PR

- **Automated Cargo.toml bump.** Workspace-edit territory; needs its
  own PR with --dry-run, --apply, and git-cleanliness guards.
- **Publishing successors.** \`shipper publish\` already does this; fix-
  forward just tells the operator when to run it.
- **Chaining \`superseded_by\` back to the original receipt.** Requires
  post-publish receipt amendment from the successor run. Follow-on.
- **\`release_compromised\` top-level state.** The receipt-level marker.
  For now the per-package \`compromised_at\` fields cover the
  planning-level need.
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Close the remediation staged rollout: yank (PR 1) + plan-yank (PR 2) +
**fix-forward planning (this PR)** give operators the three building
blocks for handling a compromised release without improvising.

## What's new

### \`shipper yank --mark-compromised\`

New opt-in flag on the existing yank primitive. When set, after a
successful \`cargo yank\`, shipper also amends the matching
\`PackageReceipt\` in \`<state_dir>/receipt.json\` with:

- \`compromised_at = Utc::now()\`
- \`compromised_by = <--reason>\`

These are the fields added (additively) in PR 2 (#129). The amendment
lets downstream commands find compromised packages without scanning
events.jsonl.

Behaviour is tolerant:
- No receipt → warn + proceed (yank still succeeds)
- No matching package in receipt → warn + proceed
- Load/write errors → warn + proceed (yank already happened; a
  receipt-write failure doesn't retroactively unyank)

### \`shipper fix-forward --from-receipt <path>\`

New planning command. Reads a receipt, finds packages flagged
\`compromised_at = Some(_)\`, and prints an ordered supersession plan:

- Topological order (dependencies first, dependents last) — opposite
  of plan-yank, because fix-forward *publishes* replacements while
  plan-yank *removes* reachability.
- Suggested successor versions use a placeholder format (\`<old>-next\`)
  so the operator picks the real bump (patch / minor / major).
- Instructional preamble walks through the full remediation loop:
  bump Cargo.toml → commit → \`shipper publish\` → optional
  \`shipper plan-yank --compromised-only\` for containment.

Like plan-yank, fix-forward is **planning only**. It does not edit
Cargo.toml or invoke publish — those are operator steps.

## Tests

4 new tests in \`engine::fix_forward::tests\`:

- only compromised+Published packages produce steps (Failed-but-
  compromised packages are silently dropped — they were never shipped)
- topological order preserved from receipt.packages
- empty plan (no compromised packages) renders with guidance toward
  \`--mark-compromised\`
- text render enumerates steps with reason and points toward the
  publish + plan-yank follow-ups

Plus CLI snapshots for \`yank --help\` (gains \`--mark-compromised\`) and
\`fix-forward --help\`.

## Scope — explicitly NOT in this PR

- **Automated Cargo.toml bump.** Workspace-edit territory; needs its
  own PR with --dry-run, --apply, and git-cleanliness guards.
- **Publishing successors.** \`shipper publish\` already does this; fix-
  forward just tells the operator when to run it.
- **Chaining \`superseded_by\` back to the original receipt.** Requires
  post-publish receipt amendment from the successor run. Follow-on.
- **\`release_compromised\` top-level state.** The receipt-level marker.
  For now the per-package \`compromised_at\` fields cover the
  planning-level need.
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Close the remediation staged rollout: yank (PR 1) + plan-yank (PR 2) +
**fix-forward planning (this PR)** give operators the three building
blocks for handling a compromised release without improvising.

## What's new

### \`shipper yank --mark-compromised\`

New opt-in flag on the existing yank primitive. When set, after a
successful \`cargo yank\`, shipper also amends the matching
\`PackageReceipt\` in \`<state_dir>/receipt.json\` with:

- \`compromised_at = Utc::now()\`
- \`compromised_by = <--reason>\`

These are the fields added (additively) in PR 2 (#129). The amendment
lets downstream commands find compromised packages without scanning
events.jsonl.

Behaviour is tolerant:
- No receipt → warn + proceed (yank still succeeds)
- No matching package in receipt → warn + proceed
- Load/write errors → warn + proceed (yank already happened; a
  receipt-write failure doesn't retroactively unyank)

### \`shipper fix-forward --from-receipt <path>\`

New planning command. Reads a receipt, finds packages flagged
\`compromised_at = Some(_)\`, and prints an ordered supersession plan:

- Topological order (dependencies first, dependents last) — opposite
  of plan-yank, because fix-forward *publishes* replacements while
  plan-yank *removes* reachability.
- Suggested successor versions use a placeholder format (\`<old>-next\`)
  so the operator picks the real bump (patch / minor / major).
- Instructional preamble walks through the full remediation loop:
  bump Cargo.toml → commit → \`shipper publish\` → optional
  \`shipper plan-yank --compromised-only\` for containment.

Like plan-yank, fix-forward is **planning only**. It does not edit
Cargo.toml or invoke publish — those are operator steps.

## Tests

4 new tests in \`engine::fix_forward::tests\`:

- only compromised+Published packages produce steps (Failed-but-
  compromised packages are silently dropped — they were never shipped)
- topological order preserved from receipt.packages
- empty plan (no compromised packages) renders with guidance toward
  \`--mark-compromised\`
- text render enumerates steps with reason and points toward the
  publish + plan-yank follow-ups

Plus CLI snapshots for \`yank --help\` (gains \`--mark-compromised\`) and
\`fix-forward --help\`.

## Scope — explicitly NOT in this PR

- **Automated Cargo.toml bump.** Workspace-edit territory; needs its
  own PR with --dry-run, --apply, and git-cleanliness guards.
- **Publishing successors.** \`shipper publish\` already does this; fix-
  forward just tells the operator when to run it.
- **Chaining \`superseded_by\` back to the original receipt.** Requires
  post-publish receipt amendment from the successor run. Follow-on.
- **\`release_compromised\` top-level state.** The receipt-level marker.
  For now the per-package \`compromised_at\` fields cover the
  planning-level need.
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
…134)

* feat(remediate): --mark-compromised + shipper fix-forward (#98 PR 3)

Close the remediation staged rollout: yank (PR 1) + plan-yank (PR 2) +
**fix-forward planning (this PR)** give operators the three building
blocks for handling a compromised release without improvising.

## What's new

### \`shipper yank --mark-compromised\`

New opt-in flag on the existing yank primitive. When set, after a
successful \`cargo yank\`, shipper also amends the matching
\`PackageReceipt\` in \`<state_dir>/receipt.json\` with:

- \`compromised_at = Utc::now()\`
- \`compromised_by = <--reason>\`

These are the fields added (additively) in PR 2 (#129). The amendment
lets downstream commands find compromised packages without scanning
events.jsonl.

Behaviour is tolerant:
- No receipt → warn + proceed (yank still succeeds)
- No matching package in receipt → warn + proceed
- Load/write errors → warn + proceed (yank already happened; a
  receipt-write failure doesn't retroactively unyank)

### \`shipper fix-forward --from-receipt <path>\`

New planning command. Reads a receipt, finds packages flagged
\`compromised_at = Some(_)\`, and prints an ordered supersession plan:

- Topological order (dependencies first, dependents last) — opposite
  of plan-yank, because fix-forward *publishes* replacements while
  plan-yank *removes* reachability.
- Suggested successor versions use a placeholder format (\`<old>-next\`)
  so the operator picks the real bump (patch / minor / major).
- Instructional preamble walks through the full remediation loop:
  bump Cargo.toml → commit → \`shipper publish\` → optional
  \`shipper plan-yank --compromised-only\` for containment.

Like plan-yank, fix-forward is **planning only**. It does not edit
Cargo.toml or invoke publish — those are operator steps.

## Tests

4 new tests in \`engine::fix_forward::tests\`:

- only compromised+Published packages produce steps (Failed-but-
  compromised packages are silently dropped — they were never shipped)
- topological order preserved from receipt.packages
- empty plan (no compromised packages) renders with guidance toward
  \`--mark-compromised\`
- text render enumerates steps with reason and points toward the
  publish + plan-yank follow-ups

Plus CLI snapshots for \`yank --help\` (gains \`--mark-compromised\`) and
\`fix-forward --help\`.

## Scope — explicitly NOT in this PR

- **Automated Cargo.toml bump.** Workspace-edit territory; needs its
  own PR with --dry-run, --apply, and git-cleanliness guards.
- **Publishing successors.** \`shipper publish\` already does this; fix-
  forward just tells the operator when to run it.
- **Chaining \`superseded_by\` back to the original receipt.** Requires
  post-publish receipt amendment from the successor run. Follow-on.
- **\`release_compromised\` top-level state.** The receipt-level marker.
  For now the per-package \`compromised_at\` fields cover the
  planning-level need.

* docs: how-to for remediating a compromised release (#98)

Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant