From 906d63fae2a05449e720a852abf5f21727db2220 Mon Sep 17 00:00:00 2001 From: Torben Nehmer Date: Sat, 26 Sep 2026 15:19:58 +0200 Subject: [PATCH] fix: network property means any network reach, not only HTTP (#144) Files changed: - CHANGES.md - VERSION - tools/CONTRACT.md - tools/chemenu/cli_contract.py - tools/chemenu/commands/git_publish.py - tools/chemenu/commands/upstream_cmd.py - tools/chemenu/commands/version_cmd.py - tools/chemenu/tests/test_cli.py - tools/chemenu/version.py --- CHANGES.md | 26 +++++++++++++++++++++++++- VERSION | 2 +- tools/CONTRACT.md | 8 ++++---- tools/chemenu/cli_contract.py | 9 +++++++++ tools/chemenu/commands/git_publish.py | 2 ++ tools/chemenu/commands/upstream_cmd.py | 1 + tools/chemenu/commands/version_cmd.py | 22 +++++++++++----------- tools/chemenu/tests/test_cli.py | 26 ++++++++++++++++++++++++++ tools/chemenu/version.py | 14 +++++++------- 9 files changed, 86 insertions(+), 24 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1834e98..f126bf4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse. --- -## 7.1.0-beta.22 - 2026-09-26 - fail() prints the command's ON FAILURE lines on stderr +## 7.1.0-beta.23 - 2026-09-26 - network: property defined; sync, publish and upstream merge marked networked **Author:** Torben Nehmer @@ -91,6 +91,7 @@ concern - readable here, never shipped as something to parse. - Command records, Private instances group: one line per cause, examples, prohibitions - Command records, Instance health group: one bullet per check, examples - Command records: NOTES is always a tuple of bullets; every record's examples are tested +- network: property defined; sync, publish and upstream merge marked networked ### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them @@ -358,6 +359,29 @@ byte-identical to before. `cli.py`'s and `_util.py`'s lookup of the running comm `cli_contract` path is now one shared function, `cli_contract.path_of`, in place of a private copy that used to live only in `cli.py`. +### network: Eigenschaft definiert; sync, publish und upstream merge als netzwerkend markiert + +Gitea #144: `network:` blieb undefiniert und stand mit dem Code sowie mit sich selbst im +Widerspruch. `sync` und `publish` (`git_publish.py`) sowie `upstream merge` (`upstream_cmd.py`) +sprechen ein Git-Remote an (`fetch`, `ls-remote`, `push`), trugen aber `network: no`; `upstream +verify` liest nur bereits geholte Refs und bleibt zu Recht bei `no`. Gleichzeitig behauptete +`version check`s Datensatz, mit `version notes` eines von nur zwei netzwerkenden Kommandos in +`wikitool` zu sein - während `review`, `task new`/`task list`/`task close` und `doctor` seit +Gitea #121 ebenfalls `network: yes` tragen. Drei weitere Docstrings wiederholten dieselbe +Ausschließlichkeit: der Modul- und der Befehls-Docstring von `version_cmd.py` sowie +`fetch_latest`s Docstring in `version.py`, dessen Behauptung „the one place that talks to a +remote host" auch unter der bisherigen, engen Lesart falsch war, da `tasks/caldav.py` und +`tasks/superproductivity.py` ebenfalls per `urllib` sprechen. + +Entschieden (Operator, 2026-09-26): `network:` meint jeden Netzzugriff, gleich ob per HTTP aus +`wikitool` selbst oder per Git-Operation gegen ein Remote - maßgeblich ist, was möglich ist, +nicht was im Normalfall passiert, und ein Git-Aufruf, der nur lokale Refs liest, zählt nicht. +Diese Bedeutung steht jetzt im Docstring von `cli_contract.Network`. `sync`, `publish` und +`upstream merge` tragen `network: yes`; alle vier überzähligen bzw. falschen Textstellen sind +korrigiert, ohne eine neue Zahl zu behaupten. Ein neuer Test schreibt die Menge der `network: +yes`-Pfade explizit fest, damit ein künftiger Wechsel eine bewusste Teständerung ist statt +stillen Auseinanderlaufens. + --- ## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join diff --git a/VERSION b/VERSION index f367546..867ee81 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.1.0-beta.22 +7.1.0-beta.23 diff --git a/tools/CONTRACT.md b/tools/CONTRACT.md index 95de8fe..67eb548 100644 --- a/tools/CONTRACT.md +++ b/tools/CONTRACT.md @@ -1611,7 +1611,7 @@ Fetch `/` and bring the local branch up to date with it. - idempotent: yes - atomic: No - fetch, then at most one merge/rebase attempt, aborted cleanly on failure - budget: counted -- network: no +- network: yes - gates: rebase-review **EXAMPLES** @@ -1664,7 +1664,7 @@ Reconcile with `/`, then stage all changes, commit, and push. - idempotent: no - atomic: No - sequential git operations, but every gate runs before staging - budget: counted -- network: no +- network: yes - gates: mass-update, publish-remote, rebase-review **EXAMPLES** @@ -2566,7 +2566,7 @@ Ask the origin's release feed whether a newer stack exists. **NOTES** - Asks the release feed for its latest release and compares it with `VERSION`: `state` is `current`, `update`, `migration` (the step crosses a compatibility boundary) or `ahead`. -- One of the **two** commands in `wikitool` that make a network call, and the only one whose whole job it is - `version notes` is the other, and only on a distributed instance. +- The only command whose whole job is the network call - `version notes` reaches the same feed too, but only as a fallback on a distributed instance. - Never reached implicitly from another command, needs no key, and times out after `--timeout` seconds (default 10). - The feed is `--url`, else `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the built-in origin. `$WIKITOOL_UPDATE_TOKEN` is only needed if that feed is not readable anonymously. - For `update` or `migration` it prints that applying the release is a separate, manual step (`INSTALL.md` § "Eine Instanz aktualisieren"). @@ -3042,7 +3042,7 @@ Take a stack update into a private instance's branch, machinery only. - idempotent: no - atomic: **No** - can leave an open, uncommitted merge behind on refusal after fetching - budget: counted -- network: no +- network: yes **EXAMPLES** diff --git a/tools/chemenu/cli_contract.py b/tools/chemenu/cli_contract.py index 248a91d..59cd39e 100644 --- a/tools/chemenu/cli_contract.py +++ b/tools/chemenu/cli_contract.py @@ -51,6 +51,15 @@ class Budget(str, Enum): class Network(str, Enum): + """Whether at least one path through this command can reach an endpoint outside this + checkout - an HTTP call `wikitool` makes itself (release feed, task tracker), or a git + operation against a remote (`fetch`, `ls-remote`, `push`). `YES` if either kind is + possible, not only if it is the usual case: a flag that avoids it (`--offline`, + `--no-fetch`) or a configuration with no remote endpoint (a markdown tracker, a remote + pointed at a local path) does not turn the value back to `NO` - what matters is whether the + command can stall or fail on an unreachable network, not what it does on a good day. A git + call that only reads refs already on disk (`rev-parse`, `rev-list`, `remote get-url`) is not + a network access on its own.""" YES = "yes" NO = "no" diff --git a/tools/chemenu/commands/git_publish.py b/tools/chemenu/commands/git_publish.py index 208fed3..e53bd3a 100644 --- a/tools/chemenu/commands/git_publish.py +++ b/tools/chemenu/commands/git_publish.py @@ -1010,6 +1010,7 @@ def apply_reconcile(outcome: ReconcileOutcome, remote: str, branch: str, command idempotent=cli_contract.Idempotent.YES, atomic="No - fetch, then at most one merge/rebase attempt, aborted cleanly on failure", budget=cli_contract.Budget.COUNTED, + network=cli_contract.Network.YES, gates=("rebase-review",), ), notes=( @@ -1089,6 +1090,7 @@ def sync_command( idempotent=cli_contract.Idempotent.NO, atomic="No - sequential git operations, but every gate runs before staging", budget=cli_contract.Budget.COUNTED, + network=cli_contract.Network.YES, gates=("mass-update", "publish-remote", "rebase-review"), ), notes=( diff --git a/tools/chemenu/commands/upstream_cmd.py b/tools/chemenu/commands/upstream_cmd.py index 1567765..523bd7b 100644 --- a/tools/chemenu/commands/upstream_cmd.py +++ b/tools/chemenu/commands/upstream_cmd.py @@ -249,6 +249,7 @@ def _merge_success_message( idempotent=cli_contract.Idempotent.NO, atomic="**No** - can leave an open, uncommitted merge behind on refusal after fetching", budget=cli_contract.Budget.COUNTED, + network=cli_contract.Network.YES, ), notes=( "Refuses on a dirty working tree, a merge already in progress, or a remote that does " diff --git a/tools/chemenu/commands/version_cmd.py b/tools/chemenu/commands/version_cmd.py index 9f900c2..fcc7a3b 100644 --- a/tools/chemenu/commands/version_cmd.py +++ b/tools/chemenu/commands/version_cmd.py @@ -24,11 +24,11 @@ number means, and `instructions/dev/version-parts.md` for the candidate model): *distributed* instance, whose `CHANGES.md` is a stub `dist upgrade` never overwrites, it falls back to the release feed, because otherwise the command can never answer there - not today and not after any future release. -- `version check` and that fallback are the only two network calls in - `wikitool`, and neither is implicit: `check` exists for the call, `notes` - announces the URL on stderr before asking and takes `--offline`. Both need - no key, both time out, and a feed that cannot be reached is reported as an - error rather than silently answered as "up to date" or "no notes". +- `version check` and that fallback both reach the release feed, and neither + is implicit: `check` exists for the call, `notes` announces the URL on + stderr before asking and takes `--offline`. Both need no key, both time + out, and a feed that cannot be reached is reported as an error rather than + silently answered as "up to date" or "no notes". """ from __future__ import annotations @@ -157,9 +157,8 @@ def show_command( "Asks the release feed for its latest release and compares it with `VERSION`: `state` " "is `current`, `update`, `migration` (the step crosses a compatibility boundary) or " "`ahead`.", - "One of the **two** commands in `wikitool` that make a network call, and the only one " - "whose whole job it is - `version notes` is the other, and only on a distributed " - "instance.", + "The only command whose whole job is the network call - `version notes` reaches the " + "same feed too, but only as a fallback on a distributed instance.", "Never reached implicitly from another command, needs no key, and times out after " "`--timeout` seconds (default 10).", "The feed is `--url`, else `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the " @@ -205,9 +204,10 @@ def check_command( ): """Ask the origin's release feed whether a newer stack exists. - The only networked command in `wikitool`. Exits 1 if the feed cannot be - reached or does not answer with a release - an unreachable feed is not the - same answer as "up to date", and must never be reported as one.""" + The only command whose whole job is the network call. Exits 1 if the feed + cannot be reached or does not answer with a release - an unreachable feed + is not the same answer as "up to date", and must never be reported as + one.""" import os try: diff --git a/tools/chemenu/tests/test_cli.py b/tools/chemenu/tests/test_cli.py index 9a015fc..b5f362f 100644 --- a/tools/chemenu/tests/test_cli.py +++ b/tools/chemenu/tests/test_cli.py @@ -285,6 +285,32 @@ def test_every_gated_record_shows_its_re_run_after_exit_42(): assert missing == [] +def test_network_yes_is_exactly_the_commands_that_can_reach_outside_this_checkout(): + """Gitea #144: `network:` means *any* reach outside this checkout - an HTTP call + `wikitool` makes itself, or a git operation against a remote (fetch/ls-remote/push) - + not only the two `version_cmd.py` used to claim exclusivity for. Pinned as an explicit + set so a command gaining or losing that reach is a deliberate edit here, not a silent + drift between the property and what the command actually does.""" + expected = { + "sync", + "publish", + "upstream merge", + "version check", + "version notes", + "review", + "task new", + "task list", + "task close", + "doctor", + } + actual = { + path + for path, rec in cli_contract.all_records().items() + if rec.properties.network is cli_contract.Network.YES + } + assert actual == expected + + # --- fail()'s ON FAILURE hint, through two real commands (Gitea #143) --- diff --git a/tools/chemenu/version.py b/tools/chemenu/version.py index d508856..d9ee03e 100644 --- a/tools/chemenu/version.py +++ b/tools/chemenu/version.py @@ -348,13 +348,13 @@ def fetch_latest( """The version the release feed reports as latest. The network call sits behind `fetcher` so every caller above this line - - and every test - can run without a network. This is the one place in - `wikitool` that talks to a remote host, and only two commands reach it: - `version check`, whose whole job it is, and `version notes` on a - *distributed* instance, whose local `CHANGES.md` is a stub with no entry to - print (see `fetch_latest_notes`). Neither is implicit - `check` exists for - the call, and `notes` announces the URL it is asking before it asks, on - stderr, and takes `--offline` for a caller that wants none of it. + and every test - can run without a network. This is the one place that + talks to the **release feed**, and only two commands reach it: `version + check`, whose whole job it is, and `version notes` on a *distributed* + instance, whose local `CHANGES.md` is a stub with no entry to print (see + `fetch_latest_notes`). Neither is implicit - `check` exists for the call, + and `notes` announces the URL it is asking before it asks, on stderr, and + takes `--offline` for a caller that wants none of it. """ fetch = fetcher or _urlopen_fetch try: