diff --git a/docs/release-notes/v1.0.0.md b/docs/release-notes/v1.0.0.md new file mode 100644 index 00000000..3f6e7ee1 --- /dev/null +++ b/docs/release-notes/v1.0.0.md @@ -0,0 +1,548 @@ +# openRepoShape v1.0.0 + +openRepoShape is the convention, as data and as running code, for a project +that spans several repositories: what they are called, how they are pinned to +each other, how one clone brings the whole project down, and what an AI +assistant does when asked to scaffold one. Its preferred shape, the Triad, is an +assembly root that mounts a spec leg and a code leg. The shape is elective and +confers nothing. Version 1.0 promises that every procedure in AGENTS.md works +end to end for ordinary repositories on Linux, macOS and Windows. On Windows, +`make park` and `make resume` run under WSL2, as the README says: native +Windows parks nothing. The release is tagged only when every in-scope issue is +closed and the full test suite passes on all three systems on the release +commit. Edge cases that only unusual input reaches +are listed below as known limits. + +## What 1.0 contains + +- **New project:** `openRepoShape --org `, or `./setup.sh`, or + `py setup-project.py` on Windows. It runs a preflight, checks the names, + shows the plan and asks for one typed `yes`. +- **Direct scaffold:** `scaffold-project.py`. Add `--pin open@` + for a declared descendant. +- **Adopt in place:** `adopt-project.py plan`, then `check`, then `execute`. +- **Compliance check:** `openRepoShape --doctor []` or + `shape-doctor.py --root `. +- **Stay current:** `update-shape.py check`, then `apply`. +- **Advance a leg:** `scripts/bump-leg.py --root --leg spec|code --to <40-hex>`. +- **Families:** `scripts/family.py init|add|bump|remove|siblings`. +- **Park and resume:** `make park` and `make resume`. +- **Each project's own gate:** `make bootstrap` and `make validate` run pinned + copies of the scripts, so the gate never needs this standard. + +## Requirements and platforms + +- `git` and Python 3.9 or newer. The tools use the standard library only. +- `gh`, logged in, wherever a tool creates repositories or pull requests on + GitHub. `git-filter-repo` is needed for adoption only. `make` is optional. +- `openRepoShape --preflight` checks the machine and creates nothing. On + Windows, run `py setup-project.py --preflight`. It names what is missing, + and offers to install `gh`, log it in and set up its credential helper, + each behind a typed `yes`. `setup-project.py --preflight` also offers `git`. +- **Linux** and **macOS** are supported, and the stock macOS `/bin/bash` 3.2 is + enough. **Native Windows** uses `setup-project.py`, after + `git config --global core.autocrlf false`. **WSL2** runs the bash entry + points. Park and resume do not work on native Windows; run them under + WSL2. +- CI runs the full suite on `ubuntu-latest`, `macos-latest` and + `windows-latest`. + +## How to pin 1.0.0 + +A project pins the standard in `contracts/shape-pin.yaml`, which always +records a 40-hex commit. Name `` itself rather than the tag: a +tag can be moved and a commit cannot, as the README and AGENTS.md say of every +pin. + +- **New project:** run + `OPENREPOSHAPE_REF= openRepoShape --org `. + The command forwards `--shape-ref ` to `setup.sh`. Outside a + checkout, `setup.sh` and `setup-project.py` take `--shape-ref` directly. +- **Existing project or family holder:** run these from a checkout of the + standard. `check` lists the files that change. `apply` says how many it + will re-copy and asks `Type yes to proceed:` before it writes anything: + + ```sh + ./update-shape.py check --root --at + ./update-shape.py apply --root --at \ + --branch shape/update- + git -C push -u origin shape/update- + ``` + + Then open a pull request from that branch. + +- **Confirm:** run `./shape-doctor.py --root ` from a checkout at + ``. `COMPLIANT`, exit 0, means the pin names that commit. + +## What changed on the way to 1.0 + +### adopt-project.py + +- **In-place adoption.** The repository becomes the assembly root and keeps + its name and history. The legs are extracted with `git filter-repo`, and the + split lands as one pull request. Every source path is verified by blob + sha, a kept root `.gitattributes` as the source's bytes plus the block + below. + The plan asks about any path it cannot place, an empty leg is seeded with + `--allow-empty-leg`, and the `xf-project-` topic is set + (#5, #16, #17, #24). +- **Branches and pins.** The tracking branch defaults to the source's default + branch, and `check` and `execute` refuse one that is no branch of the + source (`plan-tracking-branch-missing`). The topic is set before the pull + request, and NEXT names `scripts/bootstrap.py` when the source kept its own + Makefile (#493). `plan --pin` takes `@`, and `execute` + writes the pin file the scaffold writes, so the adopted root's + `validate-pins.py` passes; a `--pin-source` that cannot answer is + `pin-source-unreadable` (#508). A pinned product such as `007` is listed + in `project.yaml` as text, not as the number 7 (#515). +- **The source's own name.** `plan` refuses an `--org` and `--project` that + name another repository than the source's, compared without case + (`adopt-not-the-source`). `check` and `execute` refuse a plan edited to + name one (`plan-not-the-source`), and a source whose own name the naming + policy refuses is told to rename it first (#513). +- **Keywords as typed.** `plan` writes a value bare only where the project's + own YAML loader reads it back as that text, so `--spec-path null`, + `--id null` and `--tracking-branch true`, on a source with a branch `true`, + adopt as typed. Before, the first mounted the leg at `spec`, the second made + the id `northwind`, and the third was refused about a value nobody typed. An + explicit null leg path in a plan (`spec_path: null`) is a + `plan-bad-leg-path` finding, not the default `spec`. A backstop + (`adopt-manifest-misread`, `plan-manifest-misread`) refuses, before any leg + exists, a value the loader would read back as something else from the line + `execute` writes. The shipped templates quote those lines, so it names none + today (#518). +- **Refused before any leg exists.** `execute` refuses everything `check` + finds before it creates a leg. That covers bad mount paths, root paths that + clash with the shape's files, the source's own `project.yaml` kept in the + root, a symlink with an empty target, values no file can hold + (`adopt-unwritable-value`), and paths it cannot extract, which `plan` now + asks about. A split branch an earlier run left, or a branch in its way, is + `adopt-branch-exists` or `plan-split-branch-taken`, and a hand-edited key + of the wrong type, or a date the tools cannot read, is a finding, not a + traceback + (#170, #187, #198, #231, #270, #313, #353, #356, #377, #382, #387, #435). +- **Symlinks the root keeps.** `check` follows each kept symlink that a + path the shape writes runs through. One the shape cannot write through, + because it dangles, leads out of the tree, enters a leg or a submodule, + loops, or carries the shape's scripts out of their directory, is a + `plan-root-symlink` finding, and `execute` refuses it before any leg + exists. Where git checks a symlink out as a file, `execute` also refuses + one the shape writes beneath (`adopt-symlinks-off`). Such links used to + break the run after both legs were pushed, or put the shape's files + outside the assembly. A link at a shape path to something that is there + still gets the copy beside it (#504). +- **A source's own `.gitattributes`.** A source that keeps one at its root + gets one marked block of `/ text eol=lf` lines appended to it in + the split, a line for each pinned copy outside `shape/`, so a Windows + clone writes those copies with the bytes the pin records. The + verification counts that file as the source's bytes followed by exactly + that block, and `update-shape.py apply --add` extends the block (#511). +- **Submodules.** `.gitmodules` stays in one leg with its submodules. An + unreadable one is a `plan-gitmodules-unreadable` finding, not a crash, and + relative URLs are checked from both sides. Verification accepts no root + gitlink but the two leg mounts, each at the commit pushed to its leg + (`adopt-root-submodule`, `adopt-mount-commit` and others). `execute` + records in `project.yaml` the leg each kept submodule went to, and the + doctor holds it there + (#162, #173, #194, #226, #240, #245, #268, #394). +- **Ignore rules and odd names.** Shape files are committed even when the + source's `.gitignore` matches them, and leg mounts use `--force`. Findings + spell out control characters and bytes that are not UTF-8, in a + hand-edited `question:` and a submodule's name too, follow-up searches take + paths literally, and a cp1252 console gets escaped names + (#182, #200, #223, #228, #246, #247, #378, #384). +- **Late failures.** After a leg exists, a failure prints `REFUSED .` + and what the run left behind (#445). Exit 1 means the verification failed. + Exit 2 with `IS pushed` is finished by hand with the command it prints, + never by re-running (#371). + +### update-shape.py and bump-leg.py + +- **Re-syncing copies.** `check` gives each pinned file a verdict + (`unchanged`, `upstream-changed`, `locally-modified`, `both` and others) + and names files the standard has added (`upstream-added`). It exits 1 when + there is work to do. `apply` re-copies and re-pins, and rolls back if the + project's validators fail. `--add` and `--accept-local` each name one file + and can be repeated. Family holders, and their README's `Shape:` line, update the same + way, and the commit and refusals name the holder's own manifest + (#10, #29, #47, #52, #85, #152, #159). +- **Local edits and merged copies.** No NEXT line, `check`'s or the + doctor's, passes `--accept-local`, so a locally edited copy meets + `update-local-drift`, which leaves the choice to a person. A `shape/` + collision copy merged into its original and removed is `merged-away`, and + `apply` drops its pin row (#512). +- **Landing.** `apply --branch` takes `--trailer` lines, and a non-ASCII + trailer is committed as typed under any locale. Before it writes, + `apply --branch` refuses a pin, manifest or copy that differs from HEAD + (`update-branch-uncommitted`). The `gh pr create` it prints targets the + root's own branch and repository, and every NEXT line quotes its paths for + the shell. A failed push keeps the commit and prints a NEXT line. A later + `--pr` run opens the pull request, unless the tree has changed since + (`update-rerun-uncommitted`) + (#112, #218, #227, #374, #376, #379, #474). +- **Clean rollback.** A failure before the commit restores everything. + Additions through links or under files are refused (`update-add-symlink`, + `update-add-not-a-directory`), and so is a rewrite of a copy, the pin or + the manifest kept as a symlink (`update-rewrite-symlink`; `check` calls + such a copy `copy-is-link`). A shape pin whose `files:` is not a list is + refused `update-pin-row`, and the project's validators report + `shape-pin-row`, where all of them ended in a traceback. Pin rewrites keep + layout and comments, and non-ASCII project names work under cp1252 + (#224, #233, #269, #281, #343, #346, #347, #380). +- **bump-leg.py.** One commit moves a leg's gitlink, pin and workflow + references, and `--dry-run` shows `old -> new` and the trailers first. The + dry run also runs on the tracking branch, and names the `git switch -c` the + real run needs. Untracked workflows are refused + (`bump-leg-workflow-untracked`), and unreadable files are refused by name. + Case-only renames are followed, and any failure rolls back. After a bump, + `make bootstrap` moves the leg's tracking branch up to the new pin when + that branch is behind it + (#35, #151, #222, #264, #344, #352, #403, #484, #490). + +### setup, scaffold, family and the shared helpers + +- **Front door.** `setup.sh` self-bootstraps from upstream with an explicit + `--org`, so no fork is needed. It is a shim over `setup-project.py`, the + native Windows way in. `openRepoShape` installs as a command. The preflight + offers installs behind a typed `yes`. `--local-remote-dir` rehearses a run + with nothing created on GitHub, and `--family` lands the clone in a family + folder. `setup.sh --help` outside a checkout answers without cloning + anything. Every question is asked at the person's own terminal, even under + `curl … | bash`; with no terminal at all, the run refuses and names that + terminal as the way on. A clone path that is already taken is refused + before anything is created + (#2, #3, #14, #37, #53, #56, #57, #60, #62, #65, #75, #78, #452, #491, #509). +- **Naming and descent.** Names are classified offline against six families. + A descendant must pin its `open`, directly or through a recorded + chain. The default `reference:` follows the election date. The Triad + advisory speaks once and never blocks, and `single-repository.yaml` silences + it. A `--link-source` value is `PRODUCT=PATH` only when its key names a + chain link, and a link source that cannot be entered is a warning, not a + traceback (#4, #6, #19, #27, #42, #45, #86, #164, #453). +- **Scaffold refusals before anything exists,** dry runs included: + - colliding leg paths (`scaffold-leg-path-collides`); + - pins the shape cannot write, or two products that share one + `SHAPE_PIN_SOURCE_` variable (`scaffold-pin-collides`); + - bad chain links; + - manifest values the project would misread, and text no file can hold; + - a `--tracking-branch` git would not take as a branch name, here and in + `family.py init`. + + Shape files and leg mounts get past the operator's own ignore rules, and + templates are filled in one pass. They quote every text value they write, + so `--id 007` or `--tracking-branch 2026` reads back as typed, and a leg + path any template line would misread is refused. A `--pin` product such + as `007` is listed as text, so the root's own `validate-pins.py` passes + (#185, #201, #234, #235, #256, #266, #271, #314, #316, #351, #355, #395, #409, #517). +- **Ruleset refusals.** `--reuse-empty-repo` reuses a leg that is empty or + holds only the scaffold's own seed. The ruleset hint offers that one re-run, + by an operator holding the bypass right, and names the run's tracking + branch, not `main` (#373, #506). +- **Each project's own gate.** `make bootstrap` places each leg at its pin, + and `.gitattributes` keeps LF line endings. `AGENTS-shape.md` carries the + rules for assistants, and a `-wip` template ships too. The `validate` + workflow reads private legs with a GitHub App or `SHAPE_LEGS_TOKEN`, and + carries on with a warning without one. `validate-pins.py` rechecks + neutral-product pins from a local checkout where it can, else through + `gh api` with the operator's credentials, as the docs now say. It refuses + `git-not-runnable` (exit 2), and a workflow file it cannot read as UTF-8 + is one `workflow-unreadable` refusal naming every such file. + `make bootstrap` escapes what the console cannot print, so a `name` such as + `日本` under `PYTHONIOENCODING=ascii` no longer ends it in a traceback + (#1, #7, #8, #12, #13, #15, #18, #25, #30, #58, #154, #312, #419, #446, #456, #465). +- **Families.** A holder pins member roots under `members/`, and + `make siblings` clones working copies beside it without moving anything. + `init` asks the holder's own reader and validator about `family.yaml` + before it creates anything. `add`, `bump` and `remove` have `--dry-run`. + They check before they write, and restore the holder on failure. `add` + refuses duplicates by name or id (`member-already-present`, + `member-duplicate-id`), and `bump --to` an annotated tag, a tree or a blob + is refused `member-to-not-a-commit`. A repeated `bump` says `nothing to do` + and exits 1. `make siblings`, `park` and `resume` refuse by name when + `scripts/siblings.py` is missing. `init` refuses its default landing + inside a checkout of the standard (`family-landing-in-standard`), and + `add`, `bump` and `remove` refuse the holder's tracking branch + (`family-on-tracking-branch`) and end with a quoted NEXT that pushes + their own branch + (#16, #79, #219, #221, #236, #308, #315, #317, #318, #345, #350, #381, #397, #399, #400, #406, #416, #466, #503, #507). +- **Park and resume.** `make park` and `make resume` hand off to the Speckit + git extension. The installed commands moved to opensoft/openRepoTools + (#80, #83, #93, #94). +- **Shared helpers.** The YAML reader refuses a file it cannot open or + decode as UTF-8 (`yaml-unreadable`) and text it cannot parse + (`yaml-unparsable`), and several flow and quoting forms it used to misread + are now read exactly or refused. Output from git and `gh` is read as UTF-8 + under any locale. "Same repository" now has one definition + (#156, #160, #193, #230, #244, #257, #265, #284, #390). A refusal built + without a remedy of its own prints its `REFUSED` line alone. Before, each + tool printed the assembly root's lockstep remedy under it, in a family + holder too, and the doctor could offer it as a NEXT. The root's + `validate-pins.py` still prints it. `family.py add` refuses a + `.gitmodules` git cannot read as `gitmodules-unreadable`, as the holder's + gate does (#494). + +### shape-doctor.py + +- **One verdict.** `openRepoShape --doctor` fetches nothing into the + repository, and writes nothing there but a `--placement-plan` file you + name. Run outside a checkout of the standard, it clones the standard + into a temporary directory and keeps it, naming it on the report's last + line, so a NEXT that names a file in it still runs after the command + exits. `--json` and `--placement-plan ` pass through to + `shape-doctor.py` (#510). It prints a row per check, then one verdict: + - `COMPLIANT`: exit 0; + - `COMPLIANT, SHAPE BEHIND`, `DRIFTED`, `MISPLACED` or `INVALID`: exit 1; + - `NOT A SHAPE ROOT`: exit 2; + - `CANNOT ANSWER`: exit 3. + + Only a `FINDING` moves the verdict. The machine check is now `--preflight` + (#96). +- **Placement.** The `placement` row checks each leg's paths against the + adoption policy. `--placement-plan` writes a plan for a person to resolve + (#100, #186). +- **NEXT commands you can paste.** A NEXT is the command alone; the sentence + that explains it is a `WHY` line of its own, and `why` in `--json`. They + are quoted for sh or PowerShell, use `Copy-Item -LiteralPath` on Windows, + and set the `PYTHONPATH` a validator needs. A gitlink committed by hand is + offered both repairs, `bump-leg.py --to` the gitlink's commit or the pin's + (#102, #106, #107, #108, #110, #287, #311, #354, #402, #495). +- **More accurate verdicts.** An unloadable manifest or shape pin is now + `INVALID`, where it was `NOT A SHAPE ROOT` or `CANNOT ANSWER`, and a root + in the middle of a merge is `INVALID (merge)`, exit 1, where it could read + `COMPLIANT`. A misplaced `family.yaml` is a `manifest kinds` finding, and a + crashed validator says so. A pin that does not load is put back from git, + where the NEXT said to remove it, and a conflicted shape pin is taken + alone, its conflicted copies left to a person to merge. A fork beside a + holder is no longer taken for the member + (#147, #220, #232, #267, #357, #358, #375, #404, #468, #496). + +### The test suite, CI and the docs + +- **Three operating systems.** Every pull request runs the full suite on + Linux, macOS and Windows, and the Windows job installs `git-filter-repo`, + so the adoption suites run there too. Every push to `main` runs it on + Linux. actionlint checks this repository's workflow and both shipped ones + (#9, #11, #53, #57, #124, #129, #336, #511). +- **Hermetic tests.** No test reaches the real `gh` (#274, #349), and the + suite scaffolds into local bare repositories, never GitHub. Tests no longer + depend on the host's paths, pin-source variables or git identity, and a + failing assert names what broke. The two session fixtures that clone a + scaffold, and `setup-project.py --local-remote-dir`, clone a just-pushed + local remote through git, so git's background maintenance no longer breaks + the clone + (#64, #116, #120, #128, #131, #310, #335, #467). +- **Docs.** The README has a quick start for each platform, and + `docs/handbook.html` is a one-page reading of it. `docs/cli.md` is generated + from every tool's `--help`. A test ties each refusal code AGENTS.md names to + the tool that prints it, and the docs name `$AGENT_PROTOCOL_ROOT` rather + than a fixed path. `adopt-project.py plan --help` says what `--spec-path` + and `--code-path` are, their defaults, and the two refusals `plan` makes on + them (`adopt-bad-leg-path`, `adopt-leg-path-collides`). An accuracy pass + corrected 28 statements in the README and AGENTS.md + (#23, #46, #70, #71, #73, #88, #90, #98, #161, #309, #396, #401, #505). +- **Maintainability.** Static-analysis findings were fixed, long functions + split and docstrings corrected, with no change in behaviour + (#67, #117, #121, #125, #139, #140, #141, #142, #143, #144, #145, #398). + openRepoShape itself stays a single repository (#372). + +## Known limits + +Each line names the input that triggers the limit, then what you see. Each +limit stays open under its area's tracking issue. A number at the end of a +line, such as (#524), is the issue in the `v1.1.0` milestone that groups it +with others for one pull request. + +### [adopt-project.py](https://github.com/opensoft/openRepoShape/issues/457) + +- #388: A source symlink whose target begins with a NUL byte: `check` passes, and `execute` fails after both legs exist, with nothing on stderr. +- #389: A plan that keeps the source's own `project.yaml` in the root: `check` and `execute` now refuse it before any leg exists, but verification never checks the root manifest itself, so nothing behind that refusal would catch it. +- #417: A relative submodule URL under `--local-remote-dir`: `check` and `execute` resolve it from different remotes, so one can refuse what the other passes. +- #418: A kept submodule whose URL names no repository: `adoption verified`, then the recursive clone the `NEXT:` line prints fails. +- #420: A hand-edited `adopt_branch:` that git refuses as a branch name, such as `a..b`: `check` says `plan ok`, and `execute` fails after both legs exist. A bad `tracking_branch:` is caught by `check` as `plan-tracking-branch-missing`. +- #425: A source whose default branch name is not UTF-8: `plan`, `check` and `execute` end in a `UnicodeDecodeError` traceback. +- #427: An `origin` whose organisation is not ASCII or holds a space: `plan` refuses `unsafe-value: --org`, a flag nobody passed. +- #449: A hand edit that makes a neutral pin's `commit:` and `tree_sha256:` disagree: `adoption verified`, then the root's `validate-pins.py` fails `neutral-pin-digest-mismatch`. +- #451: A `--pin-source` that nothing reads, even a missing path: `plan` (and the scaffold's `--dry-run`) exit 0 without a word. +- A moved or dropped non-ASCII path under `LC_ALL=C PYTHONUTF8=0`: `execute` ends in a `UnicodeEncodeError` traceback after both legs are pushed (#527). +- A kept or dropped name holding a line break, a control character or a byte that is not UTF-8: it reaches the split commit message and the pull request body unspelled. +- A source with its own `single-repository.yaml`: `plan` asks the general top-level YAML question. A dedicated rule (drop or root, defaulting to drop) is planned (#528). +- A source branch that differs from the split branch only by letter case, on macOS or Windows: `check` passes, and `execute` fails after both legs exist (#520). +- A name holding a backtick or a double quote, or wrapped in backticks: its spelled form can name another entry, or make the plan unreadable. +- A path policy whose `question:` or rule id holds control characters: `plan`'s terminal report prints them raw. +- A hand-edited `adopt_branch:` such as `a=b`: `check` says `plan ok`, and `execute` refuses `unsafe-value` before any leg exists (#520). +- A hand-edited plan with a bad `id:` or `org:` beside a bad leg path: `check` and `execute` name different problems first. No leg is made either way. +- A project adopted with a `--spec-path` other than `spec`: the root Makefile still exports `CONTRACTS_DIR ?= $(CURDIR)/spec/contracts`, so the code leg's tooling points at a directory that does not exist (#522). +- A nested `--code-path`, such as `legs/code` beside `legs/spec`: the cross-leg follow-up names the spec leg `../legs/spec/…` relative to the code leg, where `../spec/…` is right (#522). +- `plan`, `check` or `execute` run without `--work-dir`: each leaves an `openreposhape-adopt-*` directory in the system temp folder, empty or holding the working trees, and nothing removes it (#526). +- `plan --source ` without `--work-dir`, and the temporary clone cleaned before `check` or `execute`: they refuse `source-unclonable`, with text about the repository's name. Passing `--source` again recovers (#526). +- Any adoption: the follow-up `plan` prints, and the split commit lists, says to open the split as a pull request, which `execute` does itself against GitHub, and calls the extracted legs seeded (#523). +- `execute --local-remote-dir`: it ends with `NEXT: review the pull request`, though none was opened (#523). +- An answer in the plan with no `resolution:` beside it: `check` says `plan ok`, though AGENTS.md says such an answer will be read as a finding (#523). +- A verification that fails against GitHub (exit 1): the pull request and the topic are already in place, and neither the report nor AGENTS.md says so. Deleting the split branch, as the way out says, closes the pull request (#523). +- `plan` with no `--elected-by` and no `git config user.name`: it is refused `adopt-manifest-invalid`, naming `--elected-by`, a flag nobody passed (#529). +- A path the plan answered by hand, such as `spec/examples/`: the doctor's `placement` row keeps listing it as `review_required`, because `execute` records only the submodule answers (#525). +- A local source or assembly that hides a branch with `uploadpack.hideRefs` or `transfer.hideRefs`: `check` does not see it, and `execute --local-remote-dir` pushes both legs before the split push fails (#520). +- A submodule moved to the other leg on purpose after adoption: the doctor reads `MISPLACED` from then on, and nothing says how its `submodules:` row is changed. A row whose submodule is gone is never reported (#525). +- A pinned product that only a standard YAML reader misreads, such as `yes`, `on` or `.5`: `execute` lists it bare, because only the project's own reader is asked. The project's validators read it as text. +- A source cloned from a local clone or mirror whose path does not end in `/`: `plan` refuses `adopt-not-the-source` even with the right `--org`, and says the name was read from the path (#521). +- An `origin` that is a Windows path with backslashes, or a URL with a trailing slash: it names no repository, so `--org` and `--project` are not compared with it, and a mistyped `--project` plans with exit 0 (#521). +- `plan --source /` with another `--project`: `adopt-not-the-source` comes only after the clone, though the name is known before it. +- A hand-edited plan in the source's own refused name, such as `billing-service`: `check` reports `naming-unclassified` without the rename-first remedy that `execute` gives. +- An `origin` removed after `plan`, or `execute --source` a copy with no `origin`: the plan's recorded `source:` repository is not consulted, so a plan hand-edited to another name passes, and `execute` acts on that name. +- A path `origin` such as `…/straße/Atlas.git` with `--org strasse`: the source check takes them for one repository, though GitHub folds ASCII case only. +- A hand-edited `org:` that is null, deleted, `false` or `0`, for a source whose repository is literally `None/` (or `False/`, `0/`): `check` says `plan ok` where a text `org:` finds `plan-split-branch-taken`. `execute` still refuses. +- A file the root keeps at `contracts/shape-pin.yaml`, or a link there to a declared pin's file: `check` passes, `execute` fails `adopt-shape-pin-unreadable` after both legs are pushed, and the first bootstrap fails (#524). +- A declared pin's file reached through a root-kept link, such as `contracts -> governance` with a `governance/openglass-pin.yaml`: `check` and `execute` pass, and the first bootstrap fails `yaml-unparsable` or `neutral-pin-unreadable`. +- A link at a path the shape writes to a name the clone's `.git` does not hold under `objects/`, `refs/`, `logs/` or `modules/`, such as `CLAUDE.md -> .git/refs/remotes/origin/main`: `check` passes, and `execute` writes the shape's file into the clone's `.git` and says `adoption verified`, or, under `objects/`, fails after both legs exist. +- A link at a path the shape writes whose target climbs back out of `.git` or a directory in it, such as `CLAUDE.md -> .git/refs/../HEAD`: it is refused `plan-root-symlink` before any leg, saying `.git/refs` is not a directory in the assembly root, though the shape's copy could go beside it. +- A kept link above a path the shape writes, where git checks a symlink out as a file: `check`'s `plan-root-symlink` says where the link leads, though the clone holds it as a file, and its "change where it points" leads on to `execute`'s `adopt-symlinks-off` (#531). +- A link at a path the shape writes to `.git/reftable`, on git 2.45 or later with the reftable backend: it is refused `plan-root-symlink`, though the shape's copy could go beside it. Untested: read from the code. +- A hand-edited plan with a bare number or boolean in a text field (`id: 007`, `tracking_branch: true`): it is turned into text (`"7"`, `"True"`) without a word, and `check` and `execute` pass. +- A hand-edited pin whose product holds a tab: `check` reports two findings about the one value, `plan-pin-malformed` and `plan-unwritable-value`. No leg is made. +- A root-kept link at a shape copy's path, such as `shape/README.md -> ../README.md`: `check`'s `plan-root-blocks-shape-copy` says `execute` would write through it after both legs are pushed, but `execute` refuses it before any leg exists. +- A `--work-dir` the user cannot write: `check` passes, and `execute` refuses `extracting the spec leg` after both leg repositories are pushed (#524). +- On Windows, a source path holding a character NTFS refuses (a TAB, a control character): it cannot be checked out there, so adopting it is untested on Windows. +- On Windows, a source path that is not UTF-8: it cannot be created there, so adopting it is untested on Windows. +- On Windows, a source tree name holding a newline, a TAB or a backslash: its submodule plan is untested on Windows. +- On Windows, a source that tracks symlinks, without `core.symlinks=true`: the links check out as plain files, so adopting one with a root-kept link the shape writes through, or a mount through a kept link, is untested on Windows. +- A source whose root `.gitattributes` is a symlink: the split appends no line-ending block, and git reads no `.gitattributes` through a link, so on Windows the pinned copies at the root can still check out with CRLF. The plan's follow-up lists the lines a regular file needs. +- A nested `.gitattributes`, such as `scripts/.gitattributes`, that sets `eol` or `-text` for a pinned copy under it: it outranks the root block for that copy, so a Windows checkout can still write the copy with CRLF (#530). +- A root adopted before #511 landed, from a source that kept its own root `.gitattributes`: it has no block, so on Windows its pinned copies check out with CRLF and `make bootstrap` reports `shape-copy-drift`. `update-shape.py apply --add` only extends a block that is there. +- A pinned copy written through a root-kept symlinked directory, such as `.github -> ci`: the block names it by the link's spelling, `/.github/workflows/validate.yml`, which governs nothing, so under a source rule such as `* text=auto eol=crlf`, `validate-pins.py` reports `shape-copy-drift` for that copy. +- A source `.gitattributes` committed with CRLF line ends under a rule other than `text=auto`: the split renormalises it to LF, and `execute` fails `adopt-lost: .gitattributes` after both legs exist. +- A source `filter=` or `working-tree-encoding=` rule that matches a pinned copy: the block settles line endings only, so the rule still applies. With `*.py working-tree-encoding=UTF-16LE-BOM`, `execute` fails at `git add -A` after both legs exist. + +### [update-shape.py and bump-leg.py](https://github.com/opensoft/openRepoShape/issues/458) + +- #422: A pinned file kept as a hard link: `apply` rewrites it through its other name, which may lie outside the root. +- #423: A file name that is not UTF-8 under `core.quotePath=false`: `bump-leg.py`, the scaffold and `family.py init` end in a `UnicodeDecodeError` traceback, not a refusal or warning. +- #428: An `apply` whose rollback cannot put one file back (an unwritable copy, a full disk): an `OSError` traceback, with the remaining files left unrestored. +- #431: `apply --branch` started from a directory git cannot open: a good branch name is refused as `update-branch-invalid`. +- #437: A trailing comment on the pin's `files:` or the manifest's `shape:` line: `apply` refuses `update-pin-unrecognised` or `update-manifest-no-shape`, though `check` sent you there. +- #438: A leg pin written as `digests: {tree_sha256: …}`: `bump-leg.py --dry-run` exits 0, and the real run refuses `bump-leg-pin-unrecognised`. +- `bump-leg.py --to` an annotated tag's own id: the dry run shows the tag id and exits 0, while the real run is refused and rolled back with exit 2 (#534). +- A locally edited `scripts/repo_shape.py` kept with `apply --accept-local` while taking the new `bootstrap.py`: `make bootstrap` ends in an `ImportError`, though `apply`'s validators pass and `check` then reads everything unchanged (#536). +- `apply` without `--branch`, moving the pin back to what HEAD already commits: it still prints a commit line, and pasted, git answers `nothing to commit` (#535). +- A root under a path holding a space, such as `My Projects/`: the two `switch -c` remedies and the closing push line that `bump-leg.py` prints are unquoted, and paste as another command (#532). +- A root or path holding a space or a shell character: `update-shape.py`'s `update-branch-exists` remedy, its `ADDED UPSTREAM` `--add` list, its `pass --add` note and its `update-local-drift` remedy print unquoted (#532). +- A Windows root whose path holds an apostrophe (`C:\Users\Jane O'Neill\Atlas`): the commands every tool but the doctor prints, the shipped validators' included, are quoted for a POSIX shell, which PowerShell does not read as one argument (#532). +- Every bump: `bump-leg.py` says `make bootstrap` re-places the leg on its tracking branch, which it does only when that branch is at or behind the pin, or ahead of it with no commit of its own (#533). +- A commit made on a detached leg whose branch is already at the pin: `make bootstrap` checks the branch out, leaves the commit on no branch, and says nothing (#533). +- A leg on a branch with commits of its own past the pin: bootstrap's `not moving an existing branch` line says the checkout stays at the pin, though it stays on the branch (#533). +- A checkout git refuses while the leg is already on its branch at the pin, such as behind a stale `index.lock`: `make bootstrap` prints `checked out, and then git said:` though nothing was checked out (#533). +- `bump-leg.py` and `apply` list the paths they staged, not the ones the commit changed: `apply` names `project.yaml` beside the pin even when only the pin moved, as when it drops a merged-away row. Repairing a gitlink moved by hand prints `X -> X` while the pin moves. +- An `update-conflict` refusal: its remedy says to read the upstream bytes with `git show :`, but nothing names the source path (#535). +- A copy re-pinned with `--accept-local`: the commit `apply` writes still says the copies are not hand-edited (#535). +- `apply --branch` without `--pr`: the `gh pr create` it prints carries no `--title`, `--body` or `--fill`, so `gh` asks for them at a prompt (#535). +- Merged-away rows beside a pinned copy kept as a symlink: the doctor's `pins` row names `update-shape.py check` and says its `apply` drops their rows, though that `apply` is refused `update-rewrite-symlink`. +- A root with `copy-is-link`, `copy-missing`, `upstream-removed`, `unmapped` or `already-at-target` rows: the doctor's `shape currency` counts fall short of its rows. +- The shape pin's `files:` key written quoted (`"files":`) or with a space before the colon, and likewise the manifest's `shape:` key: the validators and `check` accept it, and `apply` refuses `update-pin-unrecognised`, or `update-manifest-no-shape` saying the manifest has no `shape:` block. +- `bump-leg.py --dry-run` over a pin that its own validators already refuse, such as one with a stray tab line: it exits 0 and prints the `old -> new` line, and the real run exits 2 `bump-leg-validators-red` and rolls back. + +### [setup, scaffold, family and the shared helpers](https://github.com/opensoft/openRepoShape/issues/459) + +- #362: A holder index holding `Members/…`, a case variant of `members/`, on macOS or Windows: `family.py add` cannot add any member. +- #363: An uncommitted `git rm -r Members` on macOS or Windows: `add` commits the gitlink as `Members/

`, and fresh clones leave the member empty. +- #364: An untracked `Members/

` directory: refused `member-path-taken` on macOS and Windows, but mounted beside it on Linux. +- #392: A flow collection with an empty entry or a leading `:`, `#`, `- ` or `? ` (`[,a]`, `[:]`): the YAML reader accepts it, and reads `[a: b]` as a string. +- #393: A plain value that opens with `]`, `}`, `,`, `@`, a backtick or `%`: the YAML reader takes it as text, where YAML refuses it. +- #407: An uncommitted deletion of the holder's `.gitmodules`: `family.py remove` commits it with the member, and fresh clones leave every member empty. +- #421: `--tracking-branch +x`: `--dry-run` passes, and the push fails after every remote exists. +- #426: A `git config user.name` that is not UTF-8, with no `--created-by`: `family.py init` ends in a `UnicodeDecodeError` traceback. +- #433: A `--local-remote-dir` path that is not UTF-8: the scaffold and `family.py init` end in a traceback after the bare remotes exist. +- #434: A `.github/workflows/` directory the user cannot list: `validate-pins.py` ends in a `PermissionError` traceback. +- #441: A bare `--link-source ` in a chain of three or more links: one tree answers for two links, and a correct chain is reported broken. +- #442: An empty path, as in `--link-source openXdox=` or `--pin-source openXdox=`: the current directory is read, and a correct chain is reported broken. +- #443: A scalar (`5`, `openXdox`) where `neutral_product_pins:` or `referent_chain:` should be a list: the validators crash, or read it one letter at a time. +- #444: Product names that fold differently (`openß`, `openss`): the chain readers treat them as one product, and the pin validator as two. +- #450: A `--pin-source` path holding a line break or a byte that is not UTF-8: it is written into the pin file as it stands, or the scaffold crashes after all three repositories exist. +- #454: A family `name` holding a lone surrogate, or any run under `PYTHONIOENCODING=ascii`: `make siblings`, `park` and `resume` end in a `UnicodeEncodeError` traceback. +- #455: A lone surrogate or NUL in `tracking_branch` or a leg's `branch:`: `make bootstrap` ends in a traceback, while `validate-manifest.py` says `manifest ok`. +- `family.py bump --to` a commit whose `project.yaml` `id` another row already pins: `bump` commits, and the holder's validator then refuses `member-duplicate-id` (#541). +- An annotated tag's own id: `family.py add --at` pins the commit it tags, while `bump --to` refuses it (`member-to-not-a-commit`). As ruled, `bump --to` is to peel it too (#541). +- No git on `PATH`: the shipped `bootstrap.py` scripts end in a `FileNotFoundError` traceback. +- A directory name that is not UTF-8: `validate-repository-naming.py` ends in a `UnicodeEncodeError` or `UnicodeDecodeError` traceback. +- A holder member directory the user cannot enter: `validate-family.py` refuses `git-not-runnable` and says to put git on `PATH` (#539). +- Re-running before the commit, after the `git rm` that `member-gitlink-unnamed` prints: the finding repeats, and the same command then fails. +- A shape-pin `files:` row with no `path:`: the shipped validators report the path `None` as missing, instead of refusing the row. +- A YAML key written twice: the reader keeps the last one without a word. +- `--org OpenSoft`, or any casing of `opensoft` but the lower-case one: the upstream-organisation guard compares exactly and lets it through, though GitHub reads an organisation name in any case (#538). +- The plan a person approves: it names neither the tracking branch nor, in local mode, the visibility. +- A `--pin` passed to setup after `--`: step (4) never asks the naming check about it, so the report says no referent pin is declared, and the plan then says domain-descendant (#538). +- Inside setup, the scaffold's own `NEXT STEPS` block prints just before setup runs those steps itself (#538). +- `--local-remote-dir`: the clone in the scaffold's `NEXT STEPS` fails with `transport 'file' not allowed`, because it lacks the `-c protocol.file.allow=always` that setup passes (#538). +- Step (7)'s by-hand recovery under `--local-remote-dir`: its recursive clone lacks `-c protocol.file.allow=always` too (#538). +- A clone path or `--into` holding a space: setup's closing `cd` and step (7)'s by-hand recovery print the paths unquoted, and the shell splits them (#538). +- `-- --dry-run` passed through setup: step (7)'s refusal says the three repositories WERE created when none were (#538). +- A file standing where `--family` puts the clone's parent folder: step (3) passes, all three repositories are created, and the run ends in a `FileExistsError` traceback (#538). +- `scaffold-project.py --dry-run` against remotes that already exist: it does not ask, so `scaffold-remote-exists` comes only after the yes, before anything is created (#537). +- A printed value holding a C1 control or a direction-override character: setup escapes only C0 controls and DEL, so it reaches the terminal. +- A re-run where `gh` can see a leg that git cannot read: the scaffold refuses with the generic `a git or gh command failed`, not `scaffold-remote-exists`, and on a terminal git can stop to ask for a username (#542). +- A re-run that reuses legs: its visibility notes say the two legs were just created, and can contradict its own per-leg warning (#542). +- A seed found only on `seed/scaffold`, the branch the old ruleset hint made: the re-run accepts it and creates `main` at it, and says nothing about `seed/scaffold`, which on GitHub may stay the leg's default branch. +- A ruleset that refuses `adopt-project.py`'s push: its hint still offers the seed-branch-and-pull-request exit that the scaffold no longer offers (#523). +- A `--pin-source` given as an absolute path: the committed pin file records the scaffolding machine's own directory (#540). +- A lone `shape-copy-drift`: `validate-pins.py` follows it with THE LOCKSTEP RULE, about moving a gitlink, a pin and the workflows together (#539). +- `family.py init --dry-run` against a holder repository that already exists: it exits 0, and the real run refuses `family-remote-exists` after leaving an empty `/` folder (#537). +- `family.py bump --to` a commit not yet pushed to the member's remote: it fails in git's words (`reference is not a tree`), exit 2, and does not say to push first (#541). +- A member whose `tracking_branch` is not its remote's default branch: `make siblings` clones the default branch, not the tracking branch the docs promise (#543). +- `family.py siblings`: it runs the standard checkout's `siblings.py`, not the holder's pinned copy, so a holder pinned at an older shape runs other code than `make siblings` does, and the `siblings-script-missing` remedy says otherwise (#543). +- `make siblings` in a holder whose folder is not named after the family: it clones the members, then prints `mv` lines for the holder alone, so a re-run after them clones the members again (#543). +- The holder's `validate-family.py` remedies that say to run `family.py add` (`member-gitlink-absent`, `member-gitlink-unnamed`): they name no branch step, so the `add` is first refused `family-on-tracking-branch`, whose remedy names the `git switch -c` (#539). +- The README's and the handbook's `--into` sentence: it gives the default, but not that `init` refuses that default inside a checkout of the standard (`family-landing-in-standard`). +- A `--pin` product that begins with a digit or `-`, such as `3dGlass` or `0x1F`: the scaffold may write it bare where `adopt-project.py` quotes it, so the two tools spell the same list differently. The project's own reader reads both back as typed (#540). + +### [shape-doctor.py](https://github.com/opensoft/openRepoShape/issues/460) + +- #342: A `.pth` file in the operator's Python setup that reaches a `repo_shape.py`: the `manifest` row passes for a root that has lost its own copy, and CI then fails. +- #410: A crash of the standard's own naming script, or a directory name that is not UTF-8: the doctor reports `INVALID (naming)` about the project. +- #413: A leg or member whose detached HEAD holds the only copy of a commit: pasting the `legs` or `members` NEXT moves it, so only the reflog keeps the commit. +- #414: A mount with no `.git`, an unborn HEAD, no `.gitmodules` section, or a pin that is not 40 hex: the `legs` NEXT is the wrong command, or leaves the row red. +- #432: A family member URL in `.gitmodules` that is not UTF-8: the doctor ends in a traceback with no verdict. +- #439: A registered-kind file at a path no check reads (such as `contracts/project.yaml` in an assembly root): the doctor counts it validated and answers `COMPLIANT`. +- #447: A validator killed by a signal, or one that exits with nothing printed: the doctor reports a finding about the project, not a crash. +- #448: A `WARNING` from the project's own `validate-manifest.py`, such as an unreachable referent chain: the `manifest` row says `ok`, and the warning is not shown. +- An ordinary git repository: `NOT A SHAPE ROOT` names only the `adopt-project.py plan` command, though AGENTS.md says to report both ways in (#545). +- A machine that is not ready: the `machine` row quotes the preflight's `(see above)` with nothing above it, because the preflight's lines are kept only in `--json` (#549). +- The README's `validate-pins.py` paragraph: it no longer says that a digest that recomputes differently is the finding `neutral-pin-digest-mismatch`, exit 1. +- A merge left unfinished inside a leg: the `merge` row reads only the root's own index, so it is not reported. +- A hand-edited `@` in a workflow, or a leg pin whose digest no longer matches: `validate-pins.py` ends the finding at THE LOCKSTEP RULE and names no command (#539). +- A passing `validate-pins.py` that prints a note, such as `neutral-pin-source-collides`: the `pins` row says `ok` and drops the note. +- A NEXT argument that begins with `=`, such as a branch named `=x`: the doctor leaves it bare, and zsh expands a bare `=word` (#532). +- A conflicted shape pin whose conflicted copies neither side edited: each copy goes to a person to merge by hand, and keeping both sides of a hunk, then `--accept-local`, pins a stale line as the project's own edit (#547). +- A leg pin broken in the bump's own commit: the `manifest kinds` NEXT restores the pin from before the bump, and the doctor then reads `DRIFTED`. Nothing says to commit it and run the `pins` row's `bump-leg.py` line (#547). +- An unregistered kind no gate here runs, or a `family.yaml` beside an assembly root: the `manifest kinds` NEXT is prose, with `family.py` commands in backticks that an interactive zsh would run if the line were pasted (#548). +- The test that holds every NEXT to a bare command checks each string constant alone, so it would miss a ` # ` tail split across a `+`, or one with no blank after the `#`. +- A git repository with no `origin`: the doctor's adoption NEXT carries no `--org`, so pasted, it is refused `adopt-no-org` (#545). +- `openRepoShape --doctor --placement-plan a.yaml --placement-plan b.yaml`: only the last plan is written, without a word (#546). +- A relative `--placement-plan ` given from inside the repository: it writes that file there, though `openRepoShape`'s usage and refusal lines, the README and the handbook say the doctor writes nothing into the repository (#546). +- The kept clone's last line: it says any NEXT above runs from the clone, but the `pins` row's NEXT names no file in it, and the `placement` row's relative `--placement-plan`, run from the clone, writes the plan into the directory the line calls safe to delete (#546). +- A TERM sent to the installed doctor's shell alone, mid-run: it deletes the kept clone under the still-running doctor. +- An interrupted `openRepoShape --doctor` (INT, TERM, HUP): it deletes its clone, but no test holds that. +- The installed doctor's NEXT lines: only a drifted project's are tested. A plain repository's were run by hand. +- A checkout of the standard whose own `contracts/repository-naming.yaml` does not parse: a compliant project reads `INVALID (naming)`, exit 1, and the `naming` row says `the naming policy is not satisfied (exit 2)` over a `REFUSED yaml-unparsable` that is the standard's file, not the project's. + +### [The test suite, CI and the docs](https://github.com/opensoft/openRepoShape/issues/461) + +- #415: A Windows root whose path holds a space: no test runs the PowerShell form of the leg NEXT line, and its quoting test passes only because temporary paths need no quoting. +- #429: Looking up `apply --add`'s refusals in the README or the handbook: `update-add-symlink` and `update-add-not-a-directory` are not listed. +- #430: A directory the user cannot search, on Python 3.14: the shipped tools treat it as absent, and skip it or misreport it instead of refusing. +- A test fixture that clones a just-pushed local remote by plain path (about a dozen of them, `siblings.py`, four `git submodule update --init` runs in two test files, and the recursive clone in the adopt quoting test): it can race git's background maintenance. None has failed in CI (#544). +- An unreadable root pin in the doctor's tests: one test still accepts exit 3, where only exit 1 (`INVALID`) is correct (#544). +- Three test modules run the family template's scripts, two by loading them and `tests/test_cli_reference.py` through `--help` children that do not set `PYTHONDONTWRITEBYTECODE`: they leave `.pyc` files in the template tree, so a test that reads every template file could pass or fail by order (#544). +- README's by-hand version of what setup runs: its dry run omits the real run's `--visibility` and `--elected-by`, so the plan it shows can name a different elector than the real run writes (#550). +- The adopt suites run without `--work-dir` and set no `TMPDIR` of their own, so every test run leaves `openreposhape-adopt-*` directories in the system temp folder (#526). +- No test holds that a bare or absent `source:` or `legs:` in a plan is no type finding: two mutants survive that file's tests (#544). + +## Before tagging (remove at tag time) + + + +Open `v1.0.0` milestone issues at drafting time (1 open, 46 closed): + +- #472: Nothing in the repository yet says what the release covers or lists its known limits. + +## Full list + +- Merged pull requests: +- The `v1.0.0` milestone: