Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 53 additions & 2 deletions scaffold-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@
VISIBILITY_CHOICES, NamingPolicy, Refusal, accepts_role, checked_value,
git_out, link_pins_from_trees, repo_basename, shell_line, tree_digest,
)
# The variable a chain link's tree is read through, and which links share
# one, for the sources the scaffold offers a link it could not read (#610).
from repo_shape import ( # noqa: E402
pin_product_key, pin_source_env_name, pin_source_env_shared,
)
from shape_materialize import ( # noqa: E402
RULESET_HINT, SHAPE_REPOSITORY, CommandFailed,
copy_tree, default_reference, descendant_note, election_date, env_commit,
Expand Down Expand Up @@ -988,15 +993,61 @@ def _declaration(args) -> tuple[dict, set, tuple, dict]:
too, and the warning says that tree could not be READ, with why, rather
than that it "is not reachable here" (#338): the scaffold prints no other
line about a source, so `with_reasons` is asked for here.

A tree that is not reachable is offered the sources THIS TOOL reads
(`_link_remedies`, #610), never the naming validator's.
"""
pins = declared_pin_values(args)
declared_pins: set[str] = set(pins)
chain = referent_chain_links(args.referent_chain)
link_pins = link_pins_from_trees(chain, None, {}, args.pin_source,
with_reasons=True) if chain else {}
link_pins = link_pins_from_trees(
chain, None, {}, args.pin_source, with_reasons=True,
remedies=_link_remedies(chain)) if chain else {}
return pins, declared_pins, chain, link_pins


def _link_remedies(chain: tuple) -> dict[str, str]:
"""What would let each chain link's own tree be read, in the scaffold's
own sources: `{pin_product_key(link): the words}` for the
`declared-unverified` WARNING about a tree that is not reachable (#610).

That warning offered "a sibling checkout, SHAPE_PIN_SOURCE_<LINK>, or
--link-source <link>=<path>": the naming validator's three, and two of
them the scaffold does not have. It has no `--link-source`, and it reads
no checkout beside anything (`link_pins_from_trees(chain, None, ...)`).
It reads `SHAPE_PIN_SOURCE_<LINK>` for every link, unless another link
of the chain shares the variable (#275), and its `--pin-source` for the
FIRST link alone (#441), the pinned product's clone. A later link that
shares its variable has neither, which only a hand-written chain can
hold, and the warning says so.

Keyed by `pin_product_key`, the key `_chain_link_findings` finds a
holder's entry by (#444). Keyed by `casefold()`, `openß` and `openss`,
two links of one chain, wrote one entry, and the first link was offered
the later one's words.
"""
links = [repo_basename(link) for link in chain]
shared = pin_source_env_shared(links)
remedies: dict[str, str] = {}
for index, link in enumerate(links[:-1]):
key = pin_product_key(link)
sources = [] if key in shared else [pin_source_env_name(link)]
if not index:
sources.append(f"a --pin-source clone of {link} that holds its "
"project.yaml")
if not sources:
remedies[key] = (
"the scaffold reads no source for it: "
f"{pin_source_env_name(link)} is another link's too, and "
"--pin-source is the first link's tree alone")
elif len(sources) == 1:
remedies[key] = f"{sources[0]} would let it be read"
else:
remedies[key] = (f"{sources[0]}, or {sources[1]}, "
"would let it be read")
return remedies


def _check_referent_chain(policy, assembly_name: str, declared_pins: set,
chain: tuple, link_pins: dict) -> None:
"""(d) A declared `--referent-chain` reaches the referent the name claims,
Expand Down
104 changes: 86 additions & 18 deletions scripts/repo_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,23 @@
own. A git that cannot be STARTED (none on PATH) raises `OSError` from
here, never a refusal: whether that is a question the caller cannot
answer is the caller's to say (#368).

THE REFUSAL'S TWO UNTRUSTED PARTS ARE SPELLED (#610), because it is
printed as a line, and a root's `validate-pins.py` prints it inside the
`neutral-pin-source-unreadable` FINDING. `cwd` is a person's path, named
with `shown_path`: as it is, unless a character in it is not printable.
What git printed is `spelled`, as ONE line. git echoes the paths it
reads (`invalid gitfile format: <cwd>/.git`, `not a git repository:
<gitdir>`, `detected dubious ownership in repository at '<cwd>'`) and
passes a line break through, so a folder named with a line break, then
`WARNING forged-line: ...`, began a line the tool never printed; git
2.43 replaces an ESC, a CR or a BEL that it echoes with `?` but passes
a line break and a C1 control (U+0080 to U+009F) through, and another
git may replace nothing, so everything git printed is spelled. A line
break git itself printed between two lines of its message is `\\n`
here, because it cannot be told from a path's. Printable text, UTF-8
included, is unchanged. `args`, which the caller wrote, are named as
they were.
"""
proc = subprocess.run(
["git", *args], cwd=str(cwd), capture_output=True, check=False,
Expand All @@ -1041,8 +1058,8 @@
raise Refusal(
"git-failed",
"`git {}` in {} exited {}: {}".format(
" ".join(args), cwd, proc.returncode,
proc.stderr.decode("utf-8", "replace").strip(),
" ".join(args), shown_path(cwd), proc.returncode,
spelled(proc.stderr.decode("utf-8", "replace").strip()),
),
)
return proc.stdout if binary else proc.stdout.decode("utf-8").strip()
Expand Down Expand Up @@ -1556,7 +1573,7 @@
if pins_of is not None}


def _chain_link_findings(chain, link_pins) -> tuple:

Check failure on line 1576 in scripts/repo_shape.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 20 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=opensoft_openRepoShape&issues=AaFB71sMUu7qVCf7yTvg&open=AaFB71sMUu7qVCf7yTvg&pullRequest=634
"""What each link of a recorded chain says about the next one along it.

Split out of `NamingPolicy.resolve_referent` for #135. Returns the detail
Expand All @@ -1570,12 +1587,19 @@
reached could not: a warning for such a link says so, where a plain
mapping's says the tree is not reachable (#338).

A tree that is not reachable is offered the sources that would let it be
read: the caller's own, where its `LinkPins` has `remedies` for the link
(the scaffold's, #610), and otherwise the naming validator's, which
reads a checkout beside the project and takes `--link-source`.

A holder's tree and the next link are matched by `pin_product_key`
(#444): a tree declaring `openss` does not declare `openß`, whose pin
file is another.
file is another. A holder's `remedies` entry is found by it too, so in a
chain naming both, each is offered its own sources and not the other's.
"""
available = _link_declarations(link_pins)
unreadable = getattr(link_pins, "unreadable", None) or {}
remedies = getattr(link_pins, "remedies", None) or {}
warnings: list[str] = []
unverified: list[str] = []
# A link whose variable another link of the chain shares is never offered
Expand All @@ -1596,15 +1620,17 @@
continue
if declared is None:
unverified.append(holder)
alone = pin_product_key(holder) not in shared
offered = (f"a sibling checkout, {pin_source_env_name(holder)}, or"
if alone else "a sibling checkout or")
remedy = remedies.get(pin_product_key(holder))
if remedy is None:
variable = ("" if pin_product_key(holder) in shared
else f", {pin_source_env_name(holder)},")
remedy = (f"a sibling checkout{variable} or --link-source "
f"{holder}=<path> would let it be read")
warnings.append(
f"{CHAIN_UNVERIFIED}: {holder} declaring a pin on {held} "
f"is a fact in {holder}'s own tree, which is not "
f"reachable here ({offered} --link-source "
f"{holder}=<path> would let it be read). The recorded "
"chain still classifies.")
f"reachable here ({remedy}). The recorded chain still "
"classifies.")
continue
if pin_product_key(held) not in {pin_product_key(pin)
for pin in declared}:
Expand Down Expand Up @@ -2846,6 +2872,22 @@
return getattr(exc, "strerror", None) or str(exc)


def shown_path(path) -> str:
"""A path as a line about it names it: as it is, unless a character in
it is not printable (`str.isprintable`), and then as Python's `repr`
spells it -- `\\x1b`, `\\n`, `\\r`, and `\\udcff` for a byte that is not
UTF-8, as `sys.argv` keeps one (#610).

THE SPELLING `pin-source-unreadable` GIVES A `--pin-source` (#339):
`shape_materialize.shown_source` is this function. Printed raw, an ESC
in a path reached the terminal, and a line break or a carriage return
began a line of the path's choosing. A printable path is named byte for
byte as it always was.
"""
text = str(path)
return text if text.isprintable() else repr(text)


def link_manifest_lookup(path) -> tuple[Path | None, str | None]:
"""`(manifest, None)` where `path` can answer as a link's tree, else
`(None, why)`. `<dir>` -> `<dir>/project.yaml`; a file is taken as the
Expand Down Expand Up @@ -2898,32 +2940,39 @@
iterating it as one read `openDox` as the characters `D`, `e`, `n`, `o`,
`p`, `x` and ended the run in a `TypeError` for `5` (#338). An absent or
empty `key`, and a blank entry in the list, declare nothing, as ever.

THE MANIFEST'S PATH IN `why` IS `shown_path`'S (#610). It is the
scaffold's `--pin-source`, or a `--link-source`, with `/project.yaml`
after it, and the scaffold prints `why` in its `declared-unverified`
WARNING: a `--pin-source` holding an ESC put a raw `\\x1b[31m` on
stderr. A printable path is the bytes it always was.
"""
path, why = link_manifest_lookup(manifest_path)
if path is None:
return None, why
named = shown_path(path)
try:
data = load_yaml(path)
except Refusal as exc:
cause = exc.__cause__
if isinstance(cause, OSError):
return None, f"{path}: {_os_reason(cause)}"
return None, exc.detail
return None, f"{named}: {_os_reason(cause)}"
return None, exc.detail.replace(str(path), named)
except (YamlError, OSError, UnicodeDecodeError) as exc:
return None, f"{path}: {exc}"
return None, f"{named}: {exc}"
if not isinstance(data, dict):
return None, f"{path}: not a mapping"
return None, f"{named}: not a mapping"
declared = data.get(key)
if declared is None:
return set(), None
if not isinstance(declared, list):
return None, f"{path}: {key} is not a list"
return None, f"{named}: {key} is not a list"
pins: set[str] = set()
for pin in declared:
if pin is None or pin == "":
continue
if not isinstance(pin, str):
return None, f"{path}: {key} holds an entry that is not text"
return None, f"{named}: {key} holds an entry that is not text"
pins.add(repo_basename(pin))
return pins, None

Expand Down Expand Up @@ -2996,19 +3045,31 @@
it, which names the reason instead of saying the tree is "not reachable
here". Only a caller that asks for one gets one, so every other
reader's warning is exactly the text it always was.

`remedies`, `{pin_product_key(link): what would let its tree be read}`,
is the sources a caller that reads OTHER sources than the naming
validator's has to offer for a tree that is not reachable (#610, #444).
That warning offers "a sibling checkout, SHAPE_PIN_SOURCE_<LINK>, or
--link-source <link>=<path>", and the scaffold reads no sibling checkout
and has no `--link-source`, so it names its own here. A link with no
entry, and every link for a caller that gives none, gets the naming
validator's words, as ever.
"""

def __init__(self, found=(), unreadable: dict | None = None) -> None:
def __init__(self, found=(), unreadable: dict | None = None,
remedies: dict | None = None) -> None:
super().__init__(found)
self.unreadable: dict[str, str] = dict(unreadable or {})
self.remedies: dict[str, str] = dict(remedies or {})


def link_pins_from_trees(links, root: Path | None = None,
keyed_sources: dict | None = None,
default_source=None,
key: str = CHAIN_LINK_DECLARED_BY, *,
tried: dict | None = None,
with_reasons: bool = False) -> dict[str, set]:
with_reasons: bool = False,
remedies: dict | None = None) -> dict[str, set]:
"""`{link: the pins ITS manifest declares}` for the links that answered.

A link whose tree is not on the disk is simply ABSENT from the mapping,
Expand All @@ -3026,6 +3087,11 @@
line of its own for it (the scaffold) can have the warning say so. It is
off for the callers that print the reason themselves, and for the rest.

`remedies` returns a `LinkPins` too, carrying them: what would let each
link's tree be read, in the caller's own sources, for the warning about
a tree that is not reachable (#610). Given without `with_reasons`, the
`LinkPins` remembers no reason, as the plain mapping does not.

A link whose `SHAPE_PIN_SOURCE_<PRODUCT>` another of `links` names too is
not read through it (#275): one tree would answer for two links, and a
chain verified against the wrong tree is worse than one left
Expand Down Expand Up @@ -3071,7 +3137,9 @@
unreadable[pin_product_key(name)] = why
continue
found[pin_product_key(name)] = pins
return LinkPins(found, unreadable) if with_reasons else found
if not (with_reasons or remedies):
return found
return LinkPins(found, unreadable if with_reasons else None, remedies)


def find_repo_root(start: Path) -> Path:
Expand Down
12 changes: 9 additions & 3 deletions scripts/shape_materialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
GH_TREE_API, NEUTRAL_PRODUCT_OWNER, PIN_SOURCE_ENV_PREFIX,
pin_product_key, pin_source_env_collisions, tree_digest,
)
# How a `--pin-source` that is not printable is named, the one spelling the
# scaffold's chain WARNING gives it too (#610).
from repo_shape import shown_path # noqa: E402

SHAPE_REPOSITORY = "opensoft/openRepoShape"

Expand Down Expand Up @@ -2749,9 +2752,12 @@ def shown_source(source) -> str:
Python's `repr` spells it -- `\\x1b`, `\\n`, `\\r`, and `\\udcff` for a
byte that is not UTF-8, as `sys.argv` keeps one -- which is how every
other value a refusal here names is spelled (`--pin {raw!r}`). The
question `adopt-project.py`'s `_shown` asks of a path it prints."""
text = str(source)
return text if text.isprintable() else repr(text)
question `adopt-project.py`'s `_shown` asks of a path it prints.

It IS `repo_shape.shown_path` (#610), which the scaffold's
`declared-unverified` WARNING spells the same `--pin-source` with, so
the two lines about one path cannot spell it two ways."""
return shown_path(source)


def _digest_from_pin_source(source: Path, commit: str) -> str:
Expand Down
24 changes: 19 additions & 5 deletions scripts/validate-repository-naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,19 @@
DEFAULT_POLICY = Path(__file__).resolve().parents[1] / "contracts" / "repository-naming.yaml"


def _shown(path) -> str:
"""A path as a line about it names it: as it is, unless a character in
it is not printable (`str.isprintable`), and then as Python's `repr`
spells it (#610). It is `repo_shape.shown_path`, which spells the
manifest in a `link-source-unreadable` reason, so one line holds one
spelling. Written here rather than imported: this file is copied into
every project and may meet a `scripts/repo_shape.py` older than itself,
which has no such name, and the gate would end in an `ImportError` before
it classified a name."""
text = str(path)
return text if text.isprintable() else repr(text)


class Target:
"""One name to classify, with everything DECLARED about it.

Expand Down Expand Up @@ -147,9 +160,10 @@ class LinkSources:
That is `validate-pins.py`'s reading of `--pin-source` (#295), with a
chain link where that file has a declared pin, so the two copied tools
read `[PRODUCT=]PATH` one way OVER ASCII ONLY (#444). Outside it they part
company: `PinSources` (`templates/assembly-root/scripts/validate-pins.py`
`:831`, `:845`, `:848-849` and `:912`) still keys `--pin-source PRODUCT=`
by `casefold()`, so `--pin-source openss=` answers for a declared `openß`
company: `templates/assembly-root/scripts/validate-pins.py` still keys
`--pin-source PRODUCT=` by `casefold()`, in `PinSources.__init__`, which
builds the table, and in `resolve_neutral_pin_source`, which looks a pin
up in it, so `--pin-source openss=` answers for a declared `openß`
where `--link-source openss=` no longer answers for the link `openß`. It
is left, and the issue leaves it as its own change, because a pin's digest
stops a wrong source from passing: `validate-pins.py` checks it, a wrong
Expand Down Expand Up @@ -346,8 +360,8 @@ def unread_lines(self) -> list[str]:
continue
path, how, why, links = self.unread[typed]
lines.append(
f"link-source-unreadable: {path} ({how}) could not answer "
f"for {', '.join(links)}: {why}"
f"link-source-unreadable: {_shown(path)} ({how}) could not "
f"answer for {', '.join(links)}: {why}"
+ "".join(f"\n {line}" for line in (
self.read_as_path(typed), self._also_a_directory(typed))
if line))
Expand Down
9 changes: 7 additions & 2 deletions templates/assembly-root/AGENTS-shape.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,13 @@ row: they are this project's own content, and yours to edit.
A contract the code READS but does not OWN lives in the SPEC leg. That was
ruled and measured on the MedxEHR conversion: `contracts/` moved to the spec
leg, and the code leg's tooling now takes `CONTRACTS_DIR` from the environment
— the root Makefile exports `$(CURDIR)/spec/contracts` once — rather than each
script guessing at `../`.
rather than each script guessing at `../`. Where this root was ADOPTED, the
shape's Makefile exports `CONTRACTS_DIR` once, as the `contracts/` of the spec
leg at the path `project.yaml` mounts it, and `make contracts-dir` prints it.
Where the source kept a Makefile of its own, that block waits in
`shape/Makefile` until the two are merged. A SCAFFOLDED root's Makefile exports
none: its legs start from the scaffold's seed, with no `contracts/`, so
nothing reads across them yet.

## The five commands

Expand Down
Loading
Loading