diff --git a/luria/cli.py b/luria/cli.py index 2b9b7214..8f6ee826 100644 --- a/luria/cli.py +++ b/luria/cli.py @@ -16,9 +16,6 @@ luria init scaffold the record into a project that has none luria config write a starting luria.toml and stop, for editing before anything is scaffolded - luria upgrade carry a record across a version boundary; every - command under it is temporary and says what has to - be true before it is deleted Two more exist for CI, which is their only regular caller: @@ -41,7 +38,7 @@ import fire from . import (adr_index, collect, concretize, init, link_refs, lint, migrate, - new, remotes, repair, reports, site, upgrade) + new, remotes, repair, reports, site) COMMANDS = { "lint": lint.run, @@ -57,7 +54,6 @@ "config": init.config_run, "reports": reports.run, "collect": collect.run, - "upgrade": upgrade.run, } # Run by CI on every push; runnable by hand, but nothing in the contributor diff --git a/luria/lint.py b/luria/lint.py index fe2c651a..cbbecd81 100644 --- a/luria/lint.py +++ b/luria/lint.py @@ -124,8 +124,8 @@ def check_frontmatter(errors: list[str]) -> None: f"{rel}: status {status.value!r} is unchecked — the " f"{scheme.prefix} scheme declares no `status` " f"vocabulary, so any word passes and the absence looks " - f"exactly like a clean check " - f"(`luria upgrade statuses` writes one)") + f"exactly like a clean check — declare it with " + f"`[luria.schemes.{scheme.prefix}.fields.status]`") # "Superseded names its successor" used to be a branch here. # It is a `required_when` on the built-in field now, checked with # every other obligation in `check_contracts` (ADR-071 stated @@ -376,31 +376,10 @@ def scan_one(path) -> list[str]: "source-mismatch", "source-unchecked", "legacy-spellings", "narrow-titles", "stale-directives", "template-drift", "broken-chains", - "one-sided-relations", "spent-upgrades", + "one-sided-relations", "pending-documents", "unlinted-files", "workflow-temp-codes") -def spent_upgrades() -> list[str]: - """Upgrades this record has already run. - - A one-shot upgrade is temporary by construction, and the thing that - makes it *stay* temporary is being asked about. Once every record has - run one it is dead code that still has to be read, tested and - explained — so a record that no longer needs it says so, the way - `stale-directives` reports a directive that no longer suppresses - anything. One user's record saying it is not proof every record has, - which is why the row says "once every record has" rather than "now".""" - from . import upgrade - out = [] - for name, entry in upgrade.SUNSET.items(): - writes, lines, _ = upgrade._plan(current().root) - if not writes and not lines: - out.append(f"`luria upgrade {name}` has nothing left to do here " - f"— delete it once every record has run it " - f"({entry.sunset})") - return out - - def status_sections() -> list[tuple[str, str, list[str]]]: """Every status finding, as (class, headline, detail lines) — computed once, so the warning path and the `fail_on` path cannot disagree.""" @@ -585,13 +564,6 @@ def status_sections() -> list[tuple[str, str, list[str]]]: "contract (a document copied from the form starts in the wrong " "shape)", drift)) - spent = spent_upgrades() - if spent: - sections.append(( - "spent-upgrades", - f"{len(spent)} one-shot upgrade(s) this record no longer needs", - spent)) - # A directive that silently does nothing is worse than no directive. stale = ref_status.stale_annotations(result, docs) + stale_urls \ + stale_targets + stale_sources + pins.flag_problems() diff --git a/luria/upgrade.py b/luria/upgrade.py deleted file mode 100644 index e921b3a7..00000000 --- a/luria/upgrade.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python3 -"""`luria upgrade` — carry a record across a version boundary (#181). - - luria upgrade statuses # write what the new version requires - luria upgrade statuses --dry-run # print what it would write - -**Every command in here is temporary by construction.** An upgrade exists to -move records that predate a change onto it, and once they have moved it is -dead code that still has to be read, tested and explained. `SUNSET` below -says, for each one, what has to be true before it is deleted — and `luria -lint` reports an upgrade this record no longer needs, so the question comes -up on its own rather than waiting to be remembered. That is the same posture -`stale-directives` takes: a guard that no longer guards anything is worse -than no guard, because it reads like one. - -Nothing here goes through `config.load()`. The config an upgrade repairs is -the config the new version refuses to load, so a command that needed it -would be unrunnable in exactly the situation it exists for. -""" - -from __future__ import annotations - -import tomllib -from dataclasses import dataclass -from pathlib import Path - -from .config import CONFIG_NAME, find_root -from .statuses import DEFAULT_STATUSES - -_BLURBS = { - "Active": "in force — the current answer, and what a citation should " - "normally point at", - "Proposed": "not in force yet — an open question, so citing it as settled " - "is what the reference report catches", - "Deferred": "not in force and not being worked on; the question is real " - "and the answer waits on something", - "Superseded": "no longer in force because something replaced it; the " - "successor is named in the field, not in the prose", - "Rejected": "no longer in force and nothing replaced it — kept because a " - "rejection is worth being able to point at", -} - - -@dataclass(frozen=True) -class Upgrade: - """One version boundary, and what has to be true before it is deleted.""" - summary: str - sunset: str - - -SUNSET = { - "statuses": Upgrade( - summary="declare `status:` as the controlled vocabulary it is", - sunset="every record that predates #181 has run it. Luria has one " - "user today, so: once luria's own record and the anthology " - "are both on the release that carries this.", - ), -} - - -def _statuses_yaml() -> str: - lines = [ - "# The words this scheme's documents may use, and what each means.", - "#", - "# `status:` is a controlled vocabulary like any other, and this is", - "# the vocabulary: `[luria.schemes.X.fields.status]` in luria.toml is", - "# the wiring. Rename these, drop what you do not want, add your own.", - "# The one rule is that the scheme's `active` word has to appear here", - "# — it is how everything decides what is in force.", - "", - ] - for word in DEFAULT_STATUSES: - lines += [f"{word}:", f" blurb: {_BLURBS[word]}"] - return "\n".join(lines) + "\n" - - -def _schemes(raw: dict) -> dict[str, dict]: - return (raw.get("luria", raw) or {}).get("schemes", {}) or {} - - -def _plan(root: Path) -> tuple[list[tuple[Path, str]], list[str], list[str]]: - """(files to write, config lines to append, notes) — nothing written.""" - config = root / CONFIG_NAME - if not config.exists(): - return [], [], [f"no {CONFIG_NAME} at {root} — nothing to upgrade"] - raw = tomllib.loads(config.read_text(encoding="utf-8")) - schemes = _schemes(raw) - if not schemes: - return [], [], [ - f"{CONFIG_NAME} declares no schemes, so this record runs on the " - f"shipped default — `luria init --config` writes it out first"] - writes: list[tuple[Path, str]] = [] - lines: list[str] = [] - notes: list[str] = [] - for prefix, spec in schemes.items(): - if "status" in (spec.get("fields") or {}): - notes.append(f"{prefix}: already declares `status` — left alone") - continue - lines += [f"[luria.schemes.{prefix}.fields.status]", - 'vocabulary = "statuses"', ""] - values = root / str(spec.get("dir", "")) / "statuses.yaml" - if not values.exists(): - writes.append((values, _statuses_yaml())) - return writes, lines, notes - - -def run(name: str = "", *, dry_run: bool = False, root: str = "") -> None: - """Write what a new version requires into a record that predates it. - - NAME is the upgrade to run; with none, the available ones are listed - with what each is waiting on before it can be deleted.""" - where = Path(root).resolve() if root else find_root() - if not name: - print("temporary — each is deleted once every record has run it:\n") - for key, up in SUNSET.items(): - print(f" luria upgrade {key}\n {up.summary}" - f"\n remove when: {up.sunset}\n") - return - if name not in SUNSET: - raise SystemExit(f"luria upgrade: no upgrade named {name!r} " - f"(have: {', '.join(SUNSET) or 'none'})") - writes, lines, notes = _plan(where) - for note in notes: - print(f" {note}") - if not writes and not lines: - print(f"{name}: nothing to do — this record is already upgraded, and " - f"`luria upgrade {name}` can be deleted once every other one is") - return - if dry_run: - for path, _ in writes: - print(f" would write {path.relative_to(where)}") - if lines: - print(f" would append {len(lines) // 3} declaration(s) to " - f"{CONFIG_NAME}") - return - for path, text in writes: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(text, encoding="utf-8") - print(f" wrote {path.relative_to(where)}") - if lines: - config = where / CONFIG_NAME - body = config.read_text(encoding="utf-8").rstrip("\n") - config.write_text( - body + "\n\n# `status:` declared as the controlled vocabulary it " - "is (#181),\n# written by `luria upgrade statuses`.\n" - + "\n".join(lines).rstrip("\n") + "\n", encoding="utf-8") - print(f" declared `status` for {len(lines) // 3} scheme(s) " - f"in {CONFIG_NAME}") - - -if __name__ == "__main__": - import fire - fire.Fire(run) diff --git a/record/changelog.d/20260906-230545.md b/record/changelog.d/20260906-230545.md new file mode 100644 index 00000000..f7c3a6fc --- /dev/null +++ b/record/changelog.d/20260906-230545.md @@ -0,0 +1,13 @@ +### Removed + +- **`luria upgrade`, and the `statuses` upgrade under it.** It existed to + carry records across [#181](https://github.com/dmarx/luria/issues/181), and every record that predated [#181](https://github.com/dmarx/luria/issues/181) has run + it. A one-shot upgrade kept past its purpose is dead code that still has + to be read, tested and explained. +- Lint class `spent-upgrades`, which existed to ask this question. It asked + it, and this is the answer. + +### Changed + +- The finding for a scheme declaring no `status` vocabulary names the + declaration to write rather than a command that no longer exists. diff --git a/tests/test_cli.py b/tests/test_cli.py index 030efdd6..ed70fbb0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -13,7 +13,7 @@ import sys from luria import (adr_index, cli, collect, concretize, init, link_refs, - lint, migrate, new, remotes, repair, reports, site, upgrade) + lint, migrate, new, remotes, repair, reports, site) def test_every_command_is_registered(): @@ -24,7 +24,6 @@ def test_every_command_is_registered(): "migrate": migrate.run, "remotes": remotes.run, "site": site.run, "init": init.run, "reports": reports.run, "collect": collect.run, "config": init.config_run, - "upgrade": upgrade.run, } diff --git a/tests/test_statuses.py b/tests/test_statuses.py index ec4b7e6e..7259b5d2 100644 --- a/tests/test_statuses.py +++ b/tests/test_statuses.py @@ -739,46 +739,5 @@ def test_a_scheme_without_a_status_vocabulary_fails_the_lint( _value(tmp_path, 1) errors: list[str] = [] lint.check_frontmatter(errors) - assert any("luria upgrade statuses" in e for e in errors), errors + assert any("declare it with" in e for e in errors), errors - -def test_the_upgrade_satisfies_the_requirement(tmp_path, monkeypatch): - """The fix ships with the break, and is the whole remedy: after it, the - record lints clean without anyone hand-editing config.""" - from luria import upgrade - _project(tmp_path, monkeypatch) - _value(tmp_path, 1) - upgrade.run("statuses", root=str(tmp_path)) - config.reset() - errors: list[str] = [] - lint.check_frontmatter(errors) - lint.check_contracts(errors) - assert errors == [], errors - - -def test_the_upgrade_does_not_load_the_config_it_repairs(tmp_path, monkeypatch): - """It has to run against a record the new version refuses to load, or it - is unrunnable in exactly the situation it exists for.""" - from luria import upgrade - _project(tmp_path, monkeypatch) - (tmp_path / "luria.toml").write_text( - (tmp_path / "luria.toml").read_text() - + '[luria.schemes.VP.fields.bogus]\nvocabulary = "nothing"\n') - config.reset() - with pytest.raises(ValueError): - config.current() - upgrade.run("statuses", root=str(tmp_path)) # must not raise - assert (tmp_path / "record" / "values.d" / "statuses.yaml").exists() - - -def test_a_spent_upgrade_says_it_can_be_deleted(tmp_path, monkeypatch): - """The marker. An upgrade that has nothing left to do anywhere is dead - code that still has to be read and tested, so the lint raises the - question rather than waiting for someone to remember it — the posture - `stale-directives` already takes.""" - from luria import upgrade - _project(tmp_path, monkeypatch) - upgrade.run("statuses", root=str(tmp_path)) - config.reset() - rows = lint.spent_upgrades() - assert any("statuses" in r and "delete" in r for r in rows), rows