Skip to content

writ deploy narrates every conflict it detects, whatever the policy does about it; today skip is silent and skipped targets count as links #842

Open

Description

@David-Noble-at-work

writ deploy --conflict=skip left three occupied targets untouched and said nothing about it, then
reported every planned link as done.

Observed, 2026-09-06

$ writ deploy --allow-dirty --conflict=skip common
[writ] [△] Planning against dirty layers (uncommitted changes): [base personal]
[writ] [✔] Deployed 217 files (217 links) [home]

$ writ reconcile -o json | jq '.entries | group_by(.state) | map({(.[0].state): length})'
linked: 214
conflict: 3

The three are dangling symlinks in ~/.Personal-secrets/ whose sources left the personal layer;
writ correctly treated them as occupied and, under skip, correctly left them. What it did not do
is say so. Compare stop, which lists every occupant before refusing. skip is the policy that
continues past a problem, which is exactly when the reader most needs to be told what was passed
over.

And the summary is wrong: 217 is the plan, 214 is what happened. report.go (cmd/writ/writ/deploy/report.go:99)
counts links from the plan, not the outcome.

Wanted

  • Under skip, each skipped target is narrated — [△] skipped (occupied): ~/.Personal-secrets/…
    — and the summary reads Deployed 214 files (214 links, 3 skipped) [home].
  • The summary counts what was done. A skipped, failed or refused entry is never counted as a link.
  • stop already lists occupants; replace should narrate each archive-and-overwrite the same way.
    One rule: every policy names the targets it acted on differently from the plan.

Skip branch: cmd/writ/writ/deploy/deploy.go:56. Observed under noblefactor-ops#140's first base deploy.


Epic: #447


Feature: #463

Activity

  1. David-Noble-at-work commented on Sep 6, 2026

    @David-Noble-at-work
    ContributorAuthor

    Precise evidence, from the same deploy.

    The three were in the plan, sourced from files that exist:

    personal  Home/common/.Personal-secrets/com.amazon.developer.David-Noble.yaml  -> ~/.Personal-secrets/com.amazon.developer.David-Noble.yaml
    personal  Home/common/.Personal-secrets/com.apple.developer.David-Noble.yaml   -> ~/.Personal-secrets/com.apple.developer.David-Noble.yaml
    personal  Home/common/.Personal-secrets/com.discogs.yaml                       -> ~/.Personal-secrets/com.discogs.yaml
    

    The targets were already symlinks — to …/repos/personal/Home/noblefactor.Unix/.Personal-secrets/…, a path the layer no longer has. Dangling, so not "writ's own unmodified output"; correctly classed as occupied. Under stop the earlier run listed them and refused. Under skip this run left them, said nothing, and formatSummary (report.go:39, completed(file.Link)) counted them among the 217. After the deploy the three links still point at the old path; reconcile reports them as conflict: symlink cannot be resolved.

    So two defects, not one: the skip is silent, and a skipped operation is counted as a completed link. The right outcome for these three was replace — archive the stale link, relink to the source that now exists — and the reader had no way to know that from the deploy's output.

  2. David-Noble-at-work commented on Sep 6, 2026

    @David-Noble-at-work
    ContributorAuthor

    Ruled 2026-09-06: every conflict writ detects is narrated, regardless of the action taken — replacing, skipping, or stopping because of it.

    That splits the work in two, which is the right split:

    1. Detection narrates. The pre-flight already finds every occupied target and classifies it (writ's own unmodified output; foreign; locally modified; dangling). At that point, before any policy applies, each one is narrated with its classification — one line per target, the way stop already lists them.
    2. Policy decides, and says what it decided. stop refuses and says so (as today); skip says skipped per target; replace says archived to <recovery site>, replaced per target. The summary counts outcomes — Deployed 214 files (214 links, 3 skipped) — never the plan.

    The consequence for the reader: the same three lines appear whether they chose stop, skip, or replace; only the verb after each changes. Yesterday's stop run and today's skip run would have told the same story.

  3. changed the title [-]writ deploy --conflict=skip is silent about what it skipped, and the summary counts skipped targets as links[/-] [+]writ deploy narrates every conflict it detects, whatever the policy does about it; today skip is silent and skipped targets count as links[/+] on Sep 6, 2026
  4. David-Noble-at-work commented on Sep 8, 2026

    @David-Noble-at-work
    ContributorAuthor

    One more case for the narration rule, from #864 (closed 2026-09-08 as not a bug — greenfield, the old store is tossed on a shape change): when the readback inventory drops documents it cannot read, the stop pre-flight refuses every target they covered as "not recognized as writ's own unmodified output" and suggests --conflict=replace, which would archive-and-overwrite writ's own correct links — 270 of them on 2026-09-08. The refusal should say what reconcile's health already knows: "N targets unknown because M store documents are unreadable; run writ reconcile", and suggest skip, not replace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Epic:WritDeploymentwrit deployment: layers, adoption, and driftThread:WritDeploydeploying converges the machinebugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions