Skip to content

adopt-project.py _verify never asks whether the split's root project.yaml is the shape's manifest, so the one refusal #305 added before any leg exists is all that keeps the source's own file out of the root, and a plan that gets past it ends adoption verified over an assembly shape-doctor.py calls NOT A SHAPE ROOT, where #165's gitlink checks in the same function fail closed #389

Closed

Description

@brettheap

Lane: openreposhape-5 (openRepoShape-5)

(lane openRepoShape-5, 2026-10-12T15:18:02Z)

Found by lane 5's review of fix/adopt-check-link-and-manifest at 69c54c9 (the fix for #303 and #305), its N8. That PR leaves out the second bullet of #305's "What would fix it", on purpose, and closes #305; this issue is where the bullet is tracked. Pre-existing on origin/main 161c6c9 (adopt-project.py is the same file at 0f67f7b). Not claimed.

What is wrong. _verify (adopt-project.py:4358-4430) is the check that makes the rest trustworthy: every source blob in exactly one place. It fails closed on a root gitlink that is not a mount (#165) and on a mount at another commit (#243), and says so: it "does not lean on check's refusals to keep it out" (:4376-4382). It has no word about the root's project.yaml. Its only look at the shape's own files is _shape_file_findings (:4781, called at :4408), which reads the files: list of contracts/shape-pin.yaml, and project.yaml is rendered, not pinned, so it is not on that list.

When the source's own project.yaml is kept in the root, materialize_assembly_root(..., collision_dir=COLLISION_DIR) (:4023-4025; COLLISION_DIR = "shape", :182) writes the shape's manifest beside it as shape/project.yaml and prints "beside ... nothing was overwritten" (:4026-4028). _verify counts the source's file as a root path like any other and ends adoption verified (:4426). That is #305's outcome.

The fix for #305 (69c54c9, not merged when this was filed) makes check find that plan, as plan-root-blocks-manifest, and execute refuse it before any leg exists (_refuse_what_check_finds, called at :4066). Through execute as shipped nothing reaches _verify with the file any more. What keeps it out is that one refusal: _verify itself, untouched by that PR, is as blind as on main.

SHAPE=<openRepoShape checkout>   # with git-filter-repo on PATH
# Every repository is local (--local-remote-dir), so no GitHub is reached. Keep the real `gh` out of reach anyway:
STUB=$(mktemp -d); printf '#!/bin/sh\nexit 1\n' > "$STUB/gh"; chmod +x "$STUB/gh"
export PATH="$STUB:$PATH" GH_TOKEN=bogus GH_CONFIG_DIR=$(mktemp -d)
export GIT_AUTHOR_NAME=T GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=T GIT_COMMITTER_EMAIL=t@example.invalid
for mode in as-shipped refusal-removed; do (
  TMP=$(mktemp -d)
  git init -q -b main "$TMP/Thing" && cd "$TMP/Thing" || exit 1
  mkdir src docs && printf 'print(1)\n' > src/app.py && printf '# Spec\n' > docs/spec.md
  printf '# Thing\n' > README.md && printf 'name: theirs\n' > project.yaml   # the source's own project.yaml
  git add -A && git commit -qm init
  python3 "$SHAPE/adopt-project.py" plan --source "$TMP/Thing" --project Northwind --org testorg \
      --elected-by 'T H' --elected-on 2026-09-02 --out "$TMP/plan.yaml" >/dev/null
  python3 - "$TMP/plan.yaml" <<'EOF'   # answer root, by hand: the one answer #305 refuses
import re, sys
plan = sys.argv[1]
text = open(plan).read()
text, n = re.subn(r"(?m)^(  - path: project\.yaml\n    leg: )\S+$",
                  r'\g<1>root\n    resolution: "the person\'s answer"', text)
assert n == 1, n
open(plan, "w").write(text)
EOF
  echo "== $mode"
  python3 "$SHAPE/adopt-project.py" check --plan "$TMP/plan.yaml" 2>&1 | grep -E '^plan ok|^FINDING' | cut -c1-60
  if [ "$mode" = as-shipped ]; then
    python3 "$SHAPE/adopt-project.py" execute --plan "$TMP/plan.yaml" --yes \
        --local-remote-dir "$TMP/remotes" --work-dir "$TMP/work" > "$TMP/out" 2> "$TMP/err"
  else   # as tests/test_adopt_verify.py:419 does for #165: `check`'s refusal taken away
    python3 - "$SHAPE" "$TMP" > "$TMP/out" 2> "$TMP/err" <<'EOF'
import importlib.util, sys
from argparse import Namespace
from pathlib import Path
shape, tmp = Path(sys.argv[1]), Path(sys.argv[2])
spec = importlib.util.spec_from_file_location("adopt", shape / "adopt-project.py")
adopter = importlib.util.module_from_spec(spec)
spec.loader.exec_module(adopter)
adopter._refuse_what_check_finds = lambda *_: None
sys.exit(adopter.cmd_execute(Namespace(
    plan=str(tmp / "plan.yaml"), source=None, local_remote_dir=tmp / "remotes",
    allow_empty_leg=[], yes=True, work_dir=tmp / "work")))
EOF
  fi
  echo "execute exit $?:"; grep -E '^  beside  shape/project|^adoption verified' "$TMP/out" | cut -c1-100
  echo "leg repositories: $(ls "$TMP/remotes" 2>/dev/null | tr '\n' ' ')"
  [ -d "$TMP/remotes" ] || exit 0
  git -c protocol.file.allow=always clone -q --recurse-submodules -b adopt/three-repo-shape "$TMP/Thing" "$TMP/clone"
  echo "the root's project.yaml begins: $(head -1 "$TMP/clone/project.yaml")"
  python3 "$SHAPE/shape-doctor.py" --root "$TMP/clone" < /dev/null 2>&1 | grep -E '\(exit [0-9]\)$'
) done

Observed on origin/main 161c6c9 (Linux, git 2.43.0, Python 3.12.3). The two modes print the same there, because check finds nothing on main:

== as-shipped
plan ok
execute exit 0:
  beside  shape/project.yaml (the source already has project.yaml; nothing was overwritten)
adoption verified: every source path is in exactly one place
leg repositories: Northwind-code.git Northwind-spec.git
the root's project.yaml begins: name: theirs
NOT A SHAPE ROOT   (exit 2)
== refusal-removed
(the same lines)

Observed on 69c54c9, the fix for #305:

== as-shipped
FINDING plan-root-blocks-manifest: the plan keeps the file `
execute exit 2:
leg repositories:
== refusal-removed
FINDING plan-root-blocks-manifest: the plan keeps the file `
execute exit 0:
  beside  shape/project.yaml (the source already has project.yaml; nothing was overwritten)
adoption verified: every source path is in exactly one place
leg repositories: Northwind-code.git Northwind-spec.git
the root's project.yaml begins: name: theirs
NOT A SHAPE ROOT   (exit 2)

Why it matters.

Where (origin/main 161c6c9):

What would fix it. _verify refuses a split whose root project.yaml is not the shape's manifest: a finding of its own (for example adopt-manifest-not-shape), exit 1 like its others, so adoption verified is never printed over one. How to tell is the author's:

  • the split tree holds shape/project.yaml: the manifest could not be written at the root and went beside the source's file. Cheap, and no judgement of content. Only project.yaml is fatal: shape/README.md and the other copies are the designed follow-up merge, and stay as they are;
  • or the source's project.yaml is counted to the root in _account_for;
  • or the root's bytes are compared with the manifest execute rendered, or the project's own validate-manifest.py is run on the split. That costs more, and the stand-in in the tests' ROOT_FILES ("project.yaml": "kind: project\n", tests/test_adopt_verify.py:61) would have to become a real manifest.

How to test. In tests/test_adopt_verify.py, the way the #165 tests do:

  • (a) with the verify fixture, which calls _verify on hand-made commits and needs no git filter-repo, so Windows runs it: a split whose tree holds shape/project.yaml beside the root's project.yaml (root_files=) returns 1 with the new finding, and prints no adoption verified;
  • (b) shaped like test_execute_fails_a_source_submodule_the_plan_checks_let_into_the_root (:386-427): the source's own project.yaml answered root, _refuse_what_check_finds monkeypatched to a no-op, execute returns 1 with the finding and no adoption verified. It needs git filter-repo, and is skipped without it as every execute test is;
  • (c) a control: shape/README.md beside a kept README.md still gives adoption verified.
    Reverting the check fails (a) and (b).

Not in this issue.

Related. #305 and the PR that closes it. #165 and #243: the gitlink and mount-commit checks this would sit beside. #172 and PR #198: the same outcome by a leg mount path. #168: execute refuses what check finds.

Activity

  1. added this to the v1.1.0 milestone on Oct 16, 2026
  2. brettheap commented on Oct 16, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-1, session 300f8f46-004a-4e72-8587-4f192be2e108@Eagle, 2026-10-16T11:30:05Z, for #389

    Logged in lanes/log/openRepoShape-1.md at bc511caea4f4d2c78f9b862a54c28e18d8604462.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#389:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#389"`, kept where the row or its body names `#389`:
    473	docs/release-notes/v1.0.0.md: the v1.0.0 release notes, a draft for Brett to amend before he tags, saying what 1.0 contains and promises, how to pin it by commit, what changed on the way grouped by the five tracking areas, and every known limit as the input that triggers it and what the person sees (#472)	docs/v1.0.0-release-notes	MERGED
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `389` as a whole token:
       none
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions