fix: network property means any network reach, not only HTTP (#144)
CI / verify (push) Successful in 1m13s
Release / release (push) Successful in 35s

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
This commit is contained in:
torben committed 2026-09-26 15:19:58 +02:00
1 parent 16c911fca5
commit 906d63fae2
9 files changed
+86 -24

No files matched your search

+25 -1
View File
@@ -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 **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, Private instances group: one line per cause, examples, prohibitions
- Command records, Instance health group: one bullet per check, examples - 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 - 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
<!-- /wikitool:bumps --> <!-- /wikitool:bumps -->
### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them ### 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 `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`. 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 ## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join
+1 -1
View File
@@ -1 +1 @@
7.1.0-beta.22 7.1.0-beta.23
+4 -4
View File
@@ -1611,7 +1611,7 @@ Fetch `<remote>/<branch>` and bring the local branch up to date with it.
- idempotent: yes - idempotent: yes
- atomic: No - fetch, then at most one merge/rebase attempt, aborted cleanly on failure - atomic: No - fetch, then at most one merge/rebase attempt, aborted cleanly on failure
- budget: counted - budget: counted
- network: no - network: yes
- gates: rebase-review - gates: rebase-review
**EXAMPLES** **EXAMPLES**
@@ -1664,7 +1664,7 @@ Reconcile with `<remote>/<branch>`, then stage all changes, commit, and push.
- idempotent: no - idempotent: no
- atomic: No - sequential git operations, but every gate runs before staging - atomic: No - sequential git operations, but every gate runs before staging
- budget: counted - budget: counted
- network: no - network: yes
- gates: mass-update, publish-remote, rebase-review - gates: mass-update, publish-remote, rebase-review
**EXAMPLES** **EXAMPLES**
@@ -2566,7 +2566,7 @@ Ask the origin's release feed whether a newer stack exists.
**NOTES** **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`. - 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). - 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. - 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"). - 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 - idempotent: no
- atomic: **No** - can leave an open, uncommitted merge behind on refusal after fetching - atomic: **No** - can leave an open, uncommitted merge behind on refusal after fetching
- budget: counted - budget: counted
- network: no - network: yes
**EXAMPLES** **EXAMPLES**
+9
View File
@@ -51,6 +51,15 @@ class Budget(str, Enum):
class Network(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" YES = "yes"
NO = "no" NO = "no"
+2
View File
@@ -1010,6 +1010,7 @@ def apply_reconcile(outcome: ReconcileOutcome, remote: str, branch: str, command
idempotent=cli_contract.Idempotent.YES, idempotent=cli_contract.Idempotent.YES,
atomic="No - fetch, then at most one merge/rebase attempt, aborted cleanly on failure", atomic="No - fetch, then at most one merge/rebase attempt, aborted cleanly on failure",
budget=cli_contract.Budget.COUNTED, budget=cli_contract.Budget.COUNTED,
network=cli_contract.Network.YES,
gates=("rebase-review",), gates=("rebase-review",),
), ),
notes=( notes=(
@@ -1089,6 +1090,7 @@ def sync_command(
idempotent=cli_contract.Idempotent.NO, idempotent=cli_contract.Idempotent.NO,
atomic="No - sequential git operations, but every gate runs before staging", atomic="No - sequential git operations, but every gate runs before staging",
budget=cli_contract.Budget.COUNTED, budget=cli_contract.Budget.COUNTED,
network=cli_contract.Network.YES,
gates=("mass-update", "publish-remote", "rebase-review"), gates=("mass-update", "publish-remote", "rebase-review"),
), ),
notes=( notes=(
+1
View File
@@ -249,6 +249,7 @@ def _merge_success_message(
idempotent=cli_contract.Idempotent.NO, idempotent=cli_contract.Idempotent.NO,
atomic="**No** - can leave an open, uncommitted merge behind on refusal after fetching", atomic="**No** - can leave an open, uncommitted merge behind on refusal after fetching",
budget=cli_contract.Budget.COUNTED, budget=cli_contract.Budget.COUNTED,
network=cli_contract.Network.YES,
), ),
notes=( notes=(
"Refuses on a dirty working tree, a merge already in progress, or a remote that does " "Refuses on a dirty working tree, a merge already in progress, or a remote that does "
+11 -11
View File
@@ -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 *distributed* instance, whose `CHANGES.md` is a stub `dist upgrade` never
overwrites, it falls back to the release feed, because otherwise the command overwrites, it falls back to the release feed, because otherwise the command
can never answer there - not today and not after any future release. can never answer there - not today and not after any future release.
- `version check` and that fallback are the only two network calls in - `version check` and that fallback both reach the release feed, and neither
`wikitool`, and neither is implicit: `check` exists for the call, `notes` is implicit: `check` exists for the call, `notes` announces the URL on
announces the URL on stderr before asking and takes `--offline`. Both need stderr before asking and takes `--offline`. Both need no key, both time
no key, both time out, and a feed that cannot be reached is reported as an out, and a feed that cannot be reached is reported as an error rather than
error rather than silently answered as "up to date" or "no notes". silently answered as "up to date" or "no notes".
""" """
from __future__ import annotations 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` " "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 " "is `current`, `update`, `migration` (the step crosses a compatibility boundary) or "
"`ahead`.", "`ahead`.",
"One of the **two** commands in `wikitool` that make a network call, and the only one " "The only command whose whole job is the network call - `version notes` reaches the "
"whose whole job it is - `version notes` is the other, and only on a distributed " "same feed too, but only as a fallback on a distributed instance.",
"instance.",
"Never reached implicitly from another command, needs no key, and times out after " "Never reached implicitly from another command, needs no key, and times out after "
"`--timeout` seconds (default 10).", "`--timeout` seconds (default 10).",
"The feed is `--url`, else `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the " "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. """Ask the origin's release feed whether a newer stack exists.
The only networked command in `wikitool`. Exits 1 if the feed cannot be The only command whose whole job is the network call. Exits 1 if the feed
reached or does not answer with a release - an unreachable feed is not the cannot be reached or does not answer with a release - an unreachable feed
same answer as "up to date", and must never be reported as one.""" is not the same answer as "up to date", and must never be reported as
one."""
import os import os
try: try:
+26
View File
@@ -285,6 +285,32 @@ def test_every_gated_record_shows_its_re_run_after_exit_42():
assert missing == [] 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) --- # --- fail()'s ON FAILURE hint, through two real commands (Gitea #143) ---
+7 -7
View File
@@ -348,13 +348,13 @@ def fetch_latest(
"""The version the release feed reports as latest. """The version the release feed reports as latest.
The network call sits behind `fetcher` so every caller above this line - 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 and every test - can run without a network. This is the one place that
`wikitool` that talks to a remote host, and only two commands reach it: talks to the **release feed**, and only two commands reach it: `version
`version check`, whose whole job it is, and `version notes` on a check`, whose whole job it is, and `version notes` on a *distributed*
*distributed* instance, whose local `CHANGES.md` is a stub with no entry to instance, whose local `CHANGES.md` is a stub with no entry to print (see
print (see `fetch_latest_notes`). Neither is implicit - `check` exists for `fetch_latest_notes`). Neither is implicit - `check` exists for the call,
the call, and `notes` announces the URL it is asking before it asks, on and `notes` announces the URL it is asking before it asks, on stderr, and
stderr, and takes `--offline` for a caller that wants none of it. takes `--offline` for a caller that wants none of it.
""" """
fetch = fetcher or _urlopen_fetch fetch = fetcher or _urlopen_fetch
try: try: