Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 139 additions & 40 deletions adopt-project.py

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,12 @@ options:
--pin-source PIN_SOURCE
a local clone to compute --pin digests from, instead
of reading the forge with `gh api` (the TEST path; no
network)
network). The pin file says only that the digest was
read from `local-clone`, never the path. Given with no
--pin and no --referent-chain to read it, it is
refused as `pin-source-unused`; an empty value is
refused as `pin-source-no-path`, and `.` means this
directory.
--work-dir WORK_DIR where the working trees are built (default: a
temporary directory)
--no-next-steps leave out the closing NEXT STEPS (the clone, the cd
Expand Down Expand Up @@ -490,7 +495,13 @@ options:
--pin-source PIN_SOURCE
a local clone to compute --pin digests from, instead
of reading the forge with `gh api` (the TEST path; no
network)
network). The plan records the path, so that `check`
recomputes each digest there; a path that is not one
printable line is refused as `adopt-unwritable-value`.
The pin file says only `local-clone`, never the path.
Given with no --pin to read it, the flag is refused as
`pin-source-unused`; an empty value is refused as
`pin-source-no-path`, and `.` means this directory.
--allow-empty-leg {spec,code}
record in the plan that this leg having NO path is
intended, so it may be SEEDED from the shape's
Expand Down
30 changes: 27 additions & 3 deletions scaffold-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@
#: the same file; imported under the names this file has always called them
#: by, so what it refuses, and in which words, is unchanged.
from shape_materialize import ( # noqa: E402
declared_pin_products, declared_pin_values,
declared_pin_products, declared_pin_values, pin_source_path,
refuse_an_unread_pin_source,
refuse_colliding_pins as _refuse_colliding_pins,
)

Expand Down Expand Up @@ -343,10 +344,18 @@ def main(argv: list[str] | None = None) -> int:
"openXdox); the manifest RECORDS it, which is "
"what makes a descendant that pins a LAYER a "
"descendant (2026-09-05).")
parser.add_argument("--pin-source", type=Path, default=None,
# TEXT, not `type=Path`: `Path("")` is `.`, so an empty value could not
# be told from a typed `.` once parsed (`pin_source_path`).
parser.add_argument("--pin-source", default=None,
help="a local clone to compute --pin digests from, "
"instead of reading the forge with `gh api` (the "
"TEST path; no network)")
"TEST path; no network). The pin file says "
"only that the digest was read from "
"`local-clone`, never the path. Given with no "
"--pin and no --referent-chain to read it, it "
"is refused as `pin-source-unused`; an empty "
"value is refused as `pin-source-no-path`, and "
"`.` means this directory.")
parser.add_argument("--work-dir", type=Path, default=None,
help="where the working trees are built "
"(default: a temporary directory)")
Expand Down Expand Up @@ -752,6 +761,11 @@ def _identity(args, project: str) -> tuple[str, str, str, str, str]:
"the assembly root's `project.yaml` and `README.md`, each repository's "
"`AGENTS.md` and each first commit's message")

#: How a `pin-source-unused` refusal's remediation begins (#451), and a
#: `pin-source-no-path` one's.
UNREAD_PIN_SOURCE_MADE = ("nothing was created, and `--dry-run` refuses the "
"same.")

#: What every `scaffold-unwritable-value` is told to do (#282).
UNWRITABLE_REMEDIATION = (
"Remediation: nothing was created, and `--dry-run` refuses the same. Pass "
Expand Down Expand Up @@ -1524,6 +1538,16 @@ def _scaffold(args) -> int:
args.code_path = checked_value(*code_leg)
args.org = checked_value("--org", args.org)
args.pin_owner = checked_value("--pin-owner", args.pin_owner)
# A `--pin-source` THAT NAMES NO PATH, `''`, which as a `Path` is the
# directory this runs in, and then one NOTHING WILL READ (#451): no
# `--pin` reads a digest there and no `--referent-chain` link a
# declaration. Asked of the flags as typed, with no process started, so
# a dry run refuses them too.
args.pin_source = pin_source_path(args.pin_source,
nothing_made=UNREAD_PIN_SOURCE_MADE)
refuse_an_unread_pin_source(args.pin_source, args.pin,
referent_chain_links(args.referent_chain),
nothing_made=UNREAD_PIN_SOURCE_MADE)
# EACH `--pin`'S FILE AGAINST THE SHAPE'S OWN AND AGAINST EACH OTHER, from
# the arguments as typed (#241), and first: the leg-path question below
# asks about every pin's file, and would miscount one written over the
Expand Down
100 changes: 98 additions & 2 deletions scripts/shape_materialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2633,6 +2633,12 @@ def pin_digest_from_gh(repository: str, commit: str) -> str:
return digest.hexdigest()


#: How a pin file says its digest was read (`digest_source:`): from a clone
#: on this machine, or from the forge's own recursive tree listing.
DIGEST_FROM_CLONE = "local-clone"
DIGEST_FROM_GH = "gh-api-tree-recursive"


def declared_pin_values(args) -> dict[str, dict[str, str]]:
"""The template values for every `--pin`, with its digest resolved.

Expand All @@ -2644,17 +2650,32 @@ def declared_pin_values(args) -> dict[str, dict[str, str]]:
are the same three flags in `scaffold-project.py` and in
`adopt-project.py plan`, which records these values in the plan so that
`check` and `execute` read no network for them (#273).

A CLONE IS RECORDED AS `local-clone`, AND ITS PATH IS NOT (#540). The
path went into the committed pin file's `digest_source:` as it was
typed, `local-clone (<path>)`, so an absolute `--pin-source` committed
the directory a clone sat in on the machine that ran the tool, which is
of no use to any other reader of the file. The path was also written
bare (#450): one holding a line break wrote a line of its own choosing
into the pin file, and the scaffold died in `write_lf` on one holding a
byte that is not UTF-8, after all three repositories existed. What the
field is for is which of the two ways the digest was read, so that is
what it says; the refusals above still name the path the run was given.
`adopt-project.py plan` records the path in its plan beside these
values, `local-clone (<path>)`, so that `check` recomputes the digest in
that clone (#449): the plan is a file on that machine, which nothing
commits, and `execute` writes the pin file's field as this says it.
"""
pins: dict[str, dict[str, str]] = {}
for raw in args.pin:
product, repository, commit = parse_pin(raw, args.pin_owner)
if args.pin_source is not None:
digest = pin_digest_from_source(
_resolved_pin_source(args.pin_source), commit, repository)
digest_source = f"local-clone ({args.pin_source})"
digest_source = DIGEST_FROM_CLONE
else:
digest = pin_digest_from_gh(repository, commit)
digest_source = "gh-api-tree-recursive"
digest_source = DIGEST_FROM_GH
pins[product] = {
"PIN_PRODUCT": product,
"PIN_REPOSITORY": repository,
Expand All @@ -2681,6 +2702,81 @@ def declared_pin_products(args) -> dict[str, None]:
return dict.fromkeys(parse_pin(raw, args.pin_owner)[0] for raw in args.pin)


def pin_source_path(typed: str | None, *, nothing_made: str) -> Path | None:
"""`--pin-source` as the `Path` both tools read, or None when it was not
given; a value that names no path is `pin-source-no-path`.

AN EMPTY VALUE WAS THE DIRECTORY THE TOOL RAN IN. `Path("")` is
`Path(".")`, so `--pin-source ''` read the current directory as the
clone: the digest came from wherever the command was run, or
`pin-source-unreadable` blamed `.`, a path nobody typed. #442 refuses
an empty value in the naming gate (`link-source-no-path`) and in the
root's `validate-pins.py` (`pin-source-no-path`); the two tools that
make a project refuse it in the same words. A typed `.` still means
the current directory.

So both tools take the flag as TEXT and ask here, before anything
exists and before any process starts, the scaffold's `--dry-run` alike:
once `argparse` has made a `Path` of it, an empty value and a typed `.`
are one. `nothing_made` begins the remediation in the tool's own words,
as it does for `refuse_an_unread_pin_source`.
"""
if typed is None:
return None
if not str(typed):
raise Refusal(
"pin-source-no-path",
"`--pin-source ''` names no path. An empty path would be read as "
"the directory this runs in.",
f"Remediation: {nothing_made} Give the clone's path "
"(`--pin-source <path>`), or `.` to mean this directory.")
return Path(typed)


def refuse_an_unread_pin_source(pin_source, pins, chain=None, *,
nothing_made: str) -> None:
"""`--pin-source` is read by this run, or this refuses it as
`pin-source-unused` (#451): before anything exists and before any digest
is read, so the scaffold's `--dry-run` refuses it too and `plan` writes
no plan.

ITS READERS ARE `declared_pin_values`, which reads each `--pin`'s digest
there, and, in the scaffold, `link_pins_from_trees`, which reads the
first `--referent-chain` link's declaration there when nothing keyed
names that link's tree: a bare path is the first link's tree alone
(#441), never a later link's. With neither, nothing read it, so nothing
asked about it either: a path that does not exist was accepted,
`--dry-run` said `nothing was created.` and `plan` wrote its plan, each
exit 0 with no word that the flag had changed nothing. Typed meaning to
pin, with the `--pin` forgotten, it made a project that declares no
descent. The root's own `validate-pins.py` NOTES an unused
`--pin-source` of its own (#295), a check that is run again and again;
the two tools that make a project refuse one, before it is made, as
they refuse each other value they cannot use.

`pins` is the `--pin` list and `chain` the scaffold's `--referent-chain`
links, or `None` for a tool that takes no chain (`adopt-project.py
plan`), so the sentence names only flags the tool has. `nothing_made`
begins the remediation in the tool's own words. The path is never read
here, and is named as `shown_source` names it.
"""
if pin_source is None or pins or chain:
return
if chain is None:
given, reads = "no --pin", "each --pin's digest is read from"
else:
given = "no --pin and no --referent-chain"
reads = ("each --pin's digest is read from, and the first "
"--referent-chain link's declaration")
raise Refusal(
"pin-source-unused",
f"--pin-source {shown_source(pin_source)} was given with {given}, so "
f"nothing reads it: it names the clone {reads}.",
f"Remediation: {nothing_made} Add the --pin "
"<openProduct>@<40 hex commit> that clone was given for, or drop "
"--pin-source.")


#: What every refused `--pin` is told to do. The file a pin is written at is
#: `neutral_pin_path`'s, spelled for a placeholder product, never by hand. It
#: names `spec`, `code` and `shape` outright (#288): for a repository truly
Expand Down
7 changes: 5 additions & 2 deletions templates/assembly-root/contracts/neutral-product-pin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ kind: pinned_contract_manifest
# This file is not copied into a project under this name. It is RENDERED once
# per declared pin as `contracts/<product lowercased>-pin.yaml` — so a project
# declaring `openGlass` carries `contracts/openglass-pin.yaml` — by
# `scaffold-project.py --pin <openProduct>@<commit>`.
# `scaffold-project.py --pin <openProduct>@<commit>`, and by
# `adopt-project.py execute` for each pin its `plan --pin` recorded.
#
# WHAT IT IS FOR. `<Domainx><Product>` is a CLAIM of descent and a claim needs
# a REFERENT (Brett Heap, 2026-09-02). The referent is a pin the project
Expand Down Expand Up @@ -40,7 +41,9 @@ digests:
tree_sha256: "{{PIN_TREE_SHA256}}"

# How the digest above was read, so a later reader knows whether it came from
# a clone on this machine or from the forge's own tree listing.
# a clone on the machine that wrote this file (`local-clone`) or from the
# forge's own tree listing (`gh-api-tree-recursive`). The clone's path is that
# machine's own, so it is not recorded.
digest_source: {{PIN_DIGEST_SOURCE}}

declared_in: project.yaml
Expand Down
Loading
Loading