diff --git a/README.md b/README.md index 3a4f26ad..5e311f17 100644 --- a/README.md +++ b/README.md @@ -696,17 +696,17 @@ link that broke. `LedgerxWallet` -> `openXwallet` -> `openWallet` is the same shape, and `openWallet` need not exist anywhere for that to be the answer. **`validate-pins.py` rechecks the referent too, not only the declaration.** -Every `neutral_product_pins:` entry's `contracts/-pin.yaml` -has its `commit`, `revision_kind` and `sorted-ls-tree-r-v1` digest recomputed -exactly the way `scaffold-project.py --pin` computed them: OFFLINE from a -local checkout when one can be found — `--pin-source [PRODUCT=]`, -`SHAPE_PIN_SOURCE_` in the environment, or a checkout sitting beside -the assembly root and named for the pinned repository — else from `gh api` -when `gh` can read it. Neither answering is a named SKIP, never a finding and -never a failure: a project with no local checkout of its referent and no -network is not thereby lying about its descent. A digest that recomputes -differently, or a pin file the declaration names but that is not there, is -reported as drift like every other pin in this file. +Each `neutral_product_pins:` entry's `contracts/-pin.yaml` +has its `commit`, `revision_kind` and `sorted-ls-tree-r-v1` digest recomputed as +`scaffold-project.py --pin` computed them: OFFLINE from a local checkout when +one is found — `--pin-source [PRODUCT=]`, `SHAPE_PIN_SOURCE_` in +the environment, or a checkout beside the assembly root named for the pinned +repository — else by `gh api`, with the operator's `gh` credentials, when `gh` +can read it. Finding neither is a named SKIP, exit 0: no checkout and no network +does not make a descent a lie. A checkout that cannot answer is the finding +`neutral-pin-source-unreadable`. `make bootstrap`, `bump-leg.py` and +`update-shape.py apply` run this validator in the operator's environment, so +each makes that call too; none takes `--pin-source`. ### A neutral product may elect the shape and be its own assembly root @@ -1025,12 +1025,12 @@ the rest is the project's own, which an upstream fix must never overwrite. ## Keeping a project's shape current -The copies are the trade: a project runs its own gate offline, and an upstream -FIX to a copied file reaches it never. On 2026-09-03 that bill came due — one -change to the assembly-root `validate.yml`, and both projects carrying the -shape were updated BY HAND: re-copy, recompute the file's `sha256` row, move -`commit` and `digests.tree_sha256`, mirror both into `project.yaml`, re-run the -validators. `update-shape.py` is that, as one command: +The copies are the trade: a project runs its own gate, which never consults this +standard (its validators' one possible network read is `validate-pins.py`'s `gh +api` recheck of a neutral product's pin, above), and an upstream FIX to a copied +file reaches it never. On 2026-09-03 that bill came due — one change to the +assembly-root `validate.yml`, and both projects with the shape were updated BY +HAND: re-copy, re-pin, re-validate. `update-shape.py` is that, as one command: ```sh ./update-shape.py check --root ../MedxEHR # writes nothing @@ -1092,9 +1092,9 @@ by every `check` until it is taken or a file arrives at that path. `owner/repo` it bare-clones itself; it defaults to the repository the pin names. `make bootstrap` prints one line when the upstream has moved past the pin AND this machine can already tell offline — `SHAPE_UPSTREAM_PATH` points at -a clone, or a `.shape-upstream-tip` file carries a commit. It adds no network -call: a bootstrap that paused to ask GitHub a question would have made the -shape a dependency again. +a clone, or a `.shape-upstream-tip` file carries a commit. That line adds no +network call: a bootstrap that paused to ask GitHub a question would have +made the shape a dependency again. (Its pins gate may ask `gh api`; see above.) ### Is this repository compliant? `openRepoShape --doctor` diff --git a/docs/cli.md b/docs/cli.md index 79cdbec8..cf6b5e7d 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -620,7 +620,9 @@ options: writes; repeatable, kept in the order given --local-remote-dir LOCAL_REMOTE_DIR resolve the leg's remote to a bare repository here - instead of its origin (the TEST path; no network) + instead of its origin (the TEST path; the leg needs no + network, but the project's own validate-pins.py may + still ask `gh api`) --dry-run print what would move and change nothing ``` diff --git a/docs/handbook.html b/docs/handbook.html index 9cf97a32..f3b51681 100644 --- a/docs/handbook.html +++ b/docs/handbook.html @@ -1110,6 +1110,20 @@

The three legs, and naming

project’s own project.yaml, so classification stays OFFLINE and never asks GitHub whether open<Product> exists. The referent may also be reached through a RECORDED chain of pins.

+

validate-pins.py rechecks the referent too, not only the declaration. Each + neutral_product_pins: entry’s + contracts/<product lowercased>-pin.yaml has its commit, + revision_kind and sorted-ls-tree-r-v1 digest recomputed as + scaffold-project.py --pin computed them: OFFLINE from a local checkout when one + is found — --pin-source [PRODUCT=]<path>, + SHAPE_PIN_SOURCE_<PRODUCT> in the environment, or a checkout beside the + assembly root named for the pinned repository — else by gh api, with the + operator’s gh credentials, when gh can read it. Finding + neither is a named SKIP, exit 0: no checkout and no network does not make a descent a lie. A + checkout that cannot answer is the finding neutral-pin-source-unreadable. + make bootstrap, bump-leg.py and update-shape.py apply + run this validator in the operator’s environment, so each makes that call too; none + takes --pin-source.

A NEUTRAL product may elect the shape and be its own assembly root as well: openDox mounting openDox-spec and openDox-code is a fact about LAYOUT and not a claim about neutrality, because electing confers nothing. @@ -1365,9 +1379,11 @@

Bootstrap is COPIED into the project, not fetched

Reference

Keeping a project’s shape current

-

The copies are the trade: a project runs its own gate offline, and an upstream FIX to a - copied file reaches it never. update-shape.py is the re-sync, as one - command.

+

The copies are the trade: a project runs its own gate, which never consults this standard + (its validators’ one possible network read is validate-pins.py’s + gh api recheck of a neutral product’s pin, + described with the legs), and an upstream FIX to a copied file reaches it + never. update-shape.py is the re-sync, as one command.

./update-shape.py check --root ../MedxEHR          # writes nothing
 ./update-shape.py apply --root ../MedxEHR --at <commit> --yes \
     --branch shape/update-<sha>                    # then open a pull request
@@ -1402,8 +1418,9 @@

Keeping a project’s shape current

bare-clones itself; it defaults to the repository the pin names. make bootstrap prints one line when the upstream has moved past the pin AND this machine can already tell offline — SHAPE_UPSTREAM_PATH points at a clone, or a - .shape-upstream-tip file carries a commit. It adds no network call: a bootstrap - that paused to ask GitHub a question would have made the shape a dependency again.

+ .shape-upstream-tip file carries a commit. That line adds no network call: a + bootstrap that paused to ask GitHub a question would have made the shape a dependency again. + (Its pins gate may ask gh api; see the legs.)

Full text — keeping the shape current
@@ -1802,9 +1819,12 @@

For AI assistants

or the leg’s pin that this user cannot open, by name, added (#383); what setup.sh --help does from a download, which clones nothing, and which commands list every flag, added (#341); bump-leg.py --dry-run running on the tracking - branch and naming the branch the real run needs, added (#477) — a page cannot name - the commit that contains it, and the blob below is the fact a test checks. - README.md blob 3a4f26ade02d2086157f1bd30c533444101710f8. Every command, flag, exit code + branch and naming the branch the real run needs, added (#477); the one network + read a project’s own validators can make, the gh api recheck of a neutral + product’s pin, said where this page and the README had said “offline”, + added (#411) — a page cannot name the commit that contains it, and the blob below is + the fact a test checks. + README.md blob 5e311f176503d43a188d9974c7988b396dd0e0e1. Every command, flag, exit code and file name on this page is quoted from that README and from AGENTS.md.

diff --git a/scripts/bump-leg.py b/scripts/bump-leg.py index 4d10ed55..101f7dac 100755 --- a/scripts/bump-leg.py +++ b/scripts/bump-leg.py @@ -120,8 +120,14 @@ MANIFEST = "project.yaml" #: The root's OWN validators, run after the rewrite and before the commit. -#: Copies, not this checkout's: a project runs its own gate offline, and the -#: only opinion that matters here is the one its pull request will report. +#: Copies, not this checkout's: a project runs its own gate, and the only +#: opinion that matters here is the one its pull request will report. +#: NOT NETWORK-FREE, and the child gets this process's environment (#411): for +#: a project declaring `neutral_product_pins:` whose product has no local +#: checkout (a sibling of the root or `SHAPE_PIN_SOURCE_`; this tool +#: takes no `--pin-source`), `validate-pins.py` asks `gh api` for the pinned +#: tree with the operator's `gh` credentials. Without `gh`, a login or a +#: network that is its `skip` line, exit 0, and the bump goes on. VALIDATORS = ("scripts/validate-pins.py", "scripts/validate-manifest.py") #: A local-path remote is a `file://` fetch, which git has refused for @@ -1531,8 +1537,9 @@ def build_parser() -> argparse.ArgumentParser: "order given") parser.add_argument("--local-remote-dir", type=Path, default=None, help="resolve the leg's remote to a bare repository " - "here instead of its origin (the TEST path; no " - "network)") + "here instead of its origin (the TEST path; the " + "leg needs no network, but the project's own " + "validate-pins.py may still ask `gh api`)") parser.add_argument("--dry-run", action="store_true", help="print what would move and change nothing") return parser diff --git a/tests/test_update_shape_pr_base.py b/tests/test_update_shape_pr_base.py index 90d309d2..c895b4b1 100644 --- a/tests/test_update_shape_pr_base.py +++ b/tests/test_update_shape_pr_base.py @@ -16,7 +16,7 @@ manifest records NO tracking branch at all — which `validate-manifest.py` accepts and which every other tool (`bootstrap.py`, `bump-leg.py`, `family.py`, `commit_on_branch`'s own refusal) reads as `main`. The remote's -HEAD is never consulted for that last case: `apply` is otherwise offline. +HEAD is never consulted for that last case: the base comes from the manifest. THE UPSTREAM IS A CLONE OF THIS REPOSITORY, the project is scaffolded out of it into bare repositories in a temporary directory, and `gh` is stubbed at the diff --git a/update-shape.py b/update-shape.py index a1ac2652..8643b3f4 100755 --- a/update-shape.py +++ b/update-shape.py @@ -331,8 +331,14 @@ def open_upstream(spec: str) -> Upstream: A PATH IS USED IN PLACE AND NOTHING IS FETCHED. That is deliberate: it is what lets the tests run with no network, and it is also the honest answer for an organisation whose fork is the upstream. `owner/repo` is bare-cloned - into a temporary directory, which is the one place this tool touches a - network, and it refuses loudly rather than degrading to a guess. + into a temporary directory, which is the one place this tool fetches the + shape, and it refuses loudly rather than degrading to a guess. It is not + the only place `apply` can touch a network (#411): the project's own + `validate-pins.py` runs in this process's environment, so for a project + declaring `neutral_product_pins:` whose product has no local checkout it + asks `gh api` for the pinned tree with the operator's `gh` credentials + (`skip` line and exit 0 without `gh`, a login or a network), and `--push` + and `--pr` push the branch and open its pull request. """ candidate = Path(spec).expanduser() if candidate.exists(): @@ -364,9 +370,9 @@ def open_upstream(spec: str) -> Upstream: "update-upstream-unresolvable", f"could not clone {url} (git exit {proc.returncode}): " f"{(proc.stderr or '').strip()}", - "Remediation: this is the one step that needs a network. Clone " - "the shape once by hand and pass `--upstream ` instead; " - "everything after it is offline.") + "Remediation: this is the one step that fetches the shape from a " + "network. Clone the shape once by hand and pass `--upstream " + "` instead; nothing after it fetches the shape again.") return Upstream(temporary / "shape.git", spec, temporary=temporary) @@ -1858,9 +1864,8 @@ def tracking_branch_of(manifest: dict | None) -> str: `main` where the manifest records none: the default `bootstrap.py`, `bump-leg.py` and `family.py` read the field with. The remote's own HEAD - is never asked: `apply` is otherwise offline, and a base taken from the - network could disagree with the one the `--branch` refusal and `--pr` - read from the manifest. + is never asked: a base taken from the network could disagree with the + one the `--branch` refusal and `--pr` read from the manifest. """ return str((manifest or {}).get("tracking_branch") or "main")