Skip to content

--scope, multi-valued and writ-only, replaces the inert --target: absent means every scope defined on this platform; Home and System are well known, Windows adds ProgramFiles and ProgramData, and writ.scopes adds the rest #926

Open

Description

@David-Noble-at-work

Epic: #451 · Feature: #762 The writ lifecycle surface · Schedule: #916, lane 7, PR C · Waits on: #925 (lane 6)

The ruling (#762, 2026-08-31; this lane ruled 2026-09-23)

--scope, multi-valued, repeatable, writ-only, naming which scopes a run covers. Today writ registers a
persistent --target flag defaulting to Home (cmd/writ/writ/root.go:46) and nothing reads it: every command
sets its root from TargetHome() and deploy walks TargetOrder() regardless. The flag is dead, and its name is
the wrong word (#925).

Why it matters, profoundly, on Windows (the owner, 2026-09-23): there are two well-known scopes, Home and
System, and one should be able to add as many as needed; on Windows ProgramFiles and ProgramData are key.
Custom scopes are the writ.scopes map, name to root (#762 Requirement 8).

The behavior

Acceptance

Rewritten 2026-10-02 to the rulings recorded in this issue's comments; each tick names its evidence.

  • --scope on deploy, upgrade, reconcile and decommission, repeatable (ruled 2026-10-02, open question
    3: not on writ's root); --target gone; writ deploy --target=Home is a usage error (64). Evidence:
    8b453dc; TestRoot_ScopeOnLifecycleCommandsOnly, TestRoot_TargetIsGone; danoble-ud24-1 exit 64.
  • without --scope, deploy runs every scope defined on this platform, and upgrade, reconcile and
    decommission read the whole record (ruled 2026-10-02, open question 4); --scope=Home runs Home alone;
    --scope=Home --scope=System runs both, in ScopeOrder(). Evidence: 8b453dc, 6871349;
    TestSelectScopes_NoneSelectsEvery, TestInScopes_NoneNamedReadsEveryEntry,
    TestCompareScopes_TheModelsOrder, TestWritDeployScenario_Scopes; danoble-ud24-1.
  • on Linux, --scope=ProgramFiles errors "not defined on this platform"; a layer's ProgramFiles/ is skipped
    with no output. Evidence: TestSelectScopes_RefusesUndefinedBuiltin, TestRoot_RefusesUndefinedScope,
    TestCollectLayerSources_TheGivenScopesOnly; danoble-ud24-1 exit 64, "scope ProgramFiles is not defined on
    linux; the scopes defined here are System, Home".
  • a writ.scopes entry named for a builtin relocates it (ruled 2026-10-01, open question 1); one naming a
    builtin this platform does not define, or a scope with no root, is a configuration error (78). Evidence:
    8b453dc; TestDefinedScopes_RelocatedAndCustom, TestRefuseScopeConfiguration_UndefinedBuiltin,
    TestRefuseScopeConfiguration_NoRoot, TestRoot_RefusesUndefinedBuiltinScopeKey.
  • 10-command-line-interface.md §4.1 and the writ deploy reference say the above. Evidence: f6e1670.
  • danoble-ud24-1: deploy --scope=Home, then bare deploy. Evidence: e2a75b2 records the run. The Windows VM
    is gone; CI's Windows jobs on PR C are the Windows proof.
  • on Windows, --scope=ProgramData deploys under %ProgramData%: deferred to lane 34
    (#521), since ProgramData needs elevation (the owner,
    2026-10-02: "For now: skip tests against system"). Its root resolves to %ProgramData% in
    TestDefinedScopes_Windows.

Plan and design documents

Kind Document
Plan docs/plans/feature/762-lifecycle-scopes.md Phase 4; the lane's plan is docs/plans/task/926-scope-flag.md
Design 10-command-line-interface.md §4.1, 2.4-hermeticity-guarantees.md

Activity

  1. David-Noble-at-work commented on Oct 2, 2026

    @David-Noble-at-work
    ContributorAuthor

    Ruled 2026-10-01: a builtin key in writ.scopes overrides its root

    The owner: "it's an override if you relocate home or system (the builtins)." writ.scopes.Home and writ.scopes.System relocate those scopes, as #762 Requirement 8 ruled and lane 6 (#925) built. The acceptance line above, "a writ.scopes entry named System is a configuration error (78)", is amended: the error is a key naming a builtin this platform does not define, such as ProgramFiles on Unix. Plan: docs/plans/task/926-scope-flag.md on feature/925-scope-not-target.

  2. David-Noble-at-work commented on Oct 2, 2026

    @David-Noble-at-work
    ContributorAuthor

    Ruled 2026-10-02 (open question 2 of this lane's plan): the writ.targets refusal stops only the lifecycle commands

    Found planning this lane: lane 6's refusal of writ.targets (#925) ran before every writ command, so writ config list and writ config unset writ.targets both exited 78, and the key could not be removed with writ. Ruled "fix it the way you proposed": the refusal, and this lane's own refusal of a writ.scopes key naming a builtin this platform does not define, run for deploy, upgrade, reconcile, decommission and adopt only. writ config unset writ.targets now removes the key like any other, as git config --unset does. Fixed on feature/925-scope-not-target with TestRoot_ConfigRunsWithWritTargets; lands with PR C.

  3. David-Noble-at-work commented on Oct 2, 2026

    @David-Noble-at-work
    ContributorAuthor

    Ruled 2026-10-02 (open question 3 of this lane's plan): --scope sits on the four lifecycle commands, not on writ's root

    Found in phase 2: a --scope on writ's root collides with the shared root's workflow verify --scope, which names the one recorded scope whose definitions and traces it checks. The root's checker refuses a command that redefines a flag it inherits, since cobra would let the local one win silently (TestRoot_KeepsTheOutputConvention). The two flags name one thing: the scope a lifecycle operation ran in.

    The owner ruled (a): "(a). go on phase 2. as you know, we'll change the word workflow when we get to it on your schedule." --scope is a local flag on deploy, upgrade, reconcile and decommission, and workflow verify --scope matches the recorded scope without regard to case. The word workflow changes in lane 30 of #916 (#1017).

    This supersedes the acceptance line "--scope registered on writ's root, repeatable": read it as "--scope on the four lifecycle commands, repeatable". Landed in 8b453dc on feature/925-scope-not-target (phase 2).

  4. David-Noble-at-work commented on Oct 2, 2026

    @David-Noble-at-work
    ContributorAuthor

    Ruled 2026-10-02 (open question 4 of this lane's plan): with no --scope, reconcile, upgrade and decommission read the whole record

    Found planning phase 3: read literally, "absent: every scope defined on this platform" would filter the record too. With no --scope, reconcile, upgrade and decommission would lose the entries of a custom scope since removed from writ.scopes, and the unscoped entries of single-source mode.

    The owner ruled (a): "(a). go on phase 3". With no --scope they read the whole record, and --scope only narrows it: the record is the reference. Deploy is unaffected; with no --scope it deploys every scope defined here.

    This narrows the acceptance line "absent --scope runs every scope defined on this platform" to deploy. Phase 3 builds it as readback.Entry.InScopes, and orders every operation's scopes through one comparator, readback.CompareScopes, in place of two hard-coded maps and an alphabetical sort; it lands with phase 3's commit.

  5. David-Noble-at-work commented on Oct 2, 2026

    @David-Noble-at-work
    ContributorAuthor

    Ruled 2026-10-02 (open question 5 of this lane's plan): lifetimes are generations; writ follows git and Nix

    Found in phase 3: deploy replaces the record (#913) by minting a new lifetime on every invocation, and the record is the current lifetime's runs alone. So writ deploy --scope Home would leave a lifetime holding Home's runs only. Every other scope's entries would drop out of the record while their files stayed deployed: reconcile would stop seeing them, decommission could not reach them, and the next deploy's pre-flight would find their links foreign.

    The owner framed two alternatives, decommission before re-deploying or carry forward what is not re-deployed, and asked how package managers see it. Only GNU Stow, which keeps no record, re-deploys by tearing down; dpkg, rpm, Homebrew and Nix treat a re-install as a delta against their record. The owner: "we follow git and nix." Ruled (docs/architecture/5.1-reconciliation.md § Lifetimes are generations):

    • every operation that changes the system writes a new lifetime that references the runs before it and replicates none: a complete set, like a commit's tree, not a delta on its parent;
    • each run records its scope and, for each layer, the commit it deployed from (Stability between upgrades: a per-layer policy, pinned or live -- deployed links point at the kept snapshot or the working tree #847's pins, per run; one snapshot per layer and commit);
    • a scoped deploy replaces only its scopes;
    • decommission with nothing named removes everything and ends the lifetime, asking first when interactive; --scope and project names narrow it into a new lifetime;
    • pruning collects by reference.

    What this lane builds: every run records its scope. cli.NextLifetime replaces cli.NewLifetime: a deploy's new lifetime carries forward, by reference, the current lifetime's runs for the scopes it was not asked to deploy. A bare deploy carries none, so it still replaces everything, including the runs of lifetimes written before runs recorded a scope. A scope the deploy ran and failed keeps its previous runs. The new lifetime becomes current on its first save. TestWritDeployScenario_Scopes proves it with a custom scope, Staging, beside Home. It lands with phase 3's commit.

    The rest goes where the schedule reaches it, with a comment on each: lanes 11 (#928), 12 (#929), 13 (#930), 16 (#846) and 17 (#971). Adopt's part, which needs no deployment and requires a registered repository, is lane 31 (#1018).

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

    @David-Noble-at-work
    ContributorAuthor

    Ruled 2026-10-02 (open question 6 of this lane's plan): adopt joins the scope model in this lane

    Found answering the owner's question on adopt, two gaps between adopt and the scope model:

    Offered: (a) both in this lane, as a phase before the pages, with #761 joining PR C; (b) the case fix here and the inference in lane 31 (#1018); (c) both in lane 31. The owner ruled (a).

    The plan's Requirement 4 and phase 4: each item's scope is the one, of the scopes this platform defines, whose root is the deepest that holds the item, a tie going to scope order; the item's path is taken relative to that scope's root, from the model; the run records its scope in lower case. Phase 4 closes #761, lane 32 of #916.

  7. David-Noble-at-work commented on Oct 3, 2026

    @David-Noble-at-work
    ContributorAuthor

    2026-10-03: writ.scopes becomes writ.scope. The owner ruled that the name-keyed families take singular keys ("yes, singular for all three": scopes, segments and variables). #1024 renames writ.scopes to writ.scope and writ.segments to writ.segment; docs/architecture/10-command-line-interface.md already names the singular keys, and its status page records that the code reads the plural ones until #1024 lands. --scope keeps its meaning: it selects scopes and defines none.

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

Metadata

Metadata

Labels

Epic:WritDeploymentwrit deployment: layers, adoption, and drifttaskImplementation work item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions