Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
233dc71
templates: `project.yaml`, both leg pins and `family.yaml` write thei…
brettheap Oct 12, 2026
3894aa8
scripts/family.py: `_member_id`'s docstring says a legacy row whose b…
brettheap Oct 12, 2026
4ce3f1f
Merge origin/main (3ef059c, #314) into fix/template-quoted-placeholders
Oct 12, 2026
77cd0b2
tests/test_scaffold_manifest_values.py, tests/test_adopt_manifest_val…
Oct 12, 2026
ce22052
Merge branch 'main' into fix/template-quoted-placeholders
brettheap Oct 12, 2026
1ef3310
Merge branch 'main' into fix/template-quoted-placeholders
brettheap Oct 12, 2026
4d64794
tests/test_family_add_member_id.py: #395's quoted `id: "{{PROJECT_ID}…
Oct 12, 2026
12715cf
Merge branch 'main' into fix/template-quoted-placeholders
brettheap Oct 12, 2026
ec3dfc3
Merge branch 'main' into fix/template-quoted-placeholders
brettheap Oct 12, 2026
d526f76
Merge branch 'main' into fix/template-quoted-placeholders
brettheap Oct 12, 2026
d651b59
Merge origin/main (aa4e1f59) into fix/template-quoted-placeholders
Oct 13, 2026
6cbbac6
tests: test_template_text_placeholders.py's member test switches the …
Oct 13, 2026
6d108d1
Merge origin/main (56713e21) into fix/template-quoted-placeholders
Oct 13, 2026
7968f90
tests: test_adopt_github_exits.py matches the quoted tracking_branch …
Oct 13, 2026
e5e768b
tests: test_template_text_placeholders.py's adoption read-back gives …
Oct 13, 2026
3220c9e
Merge branch 'main' into fix/template-quoted-placeholders
brettheap Oct 13, 2026
4984651
Merge branch 'main' into fix/template-quoted-placeholders
brettheap Oct 13, 2026
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
8 changes: 2 additions & 6 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,7 @@ options:
-h, --help show this help message and exit
--org ORG
--project PROJECT the assembly-root name, e.g. Atlas
--id ID lowercase project id (default: --project lowercased).
One the project's YAML reader would read as another
type -- `null`, `true`, `007` -- is refused.
--id ID lowercase project id (default: --project lowercased)
--name NAME display name
--visibility {private,public,internal}
default: private for a fresh scaffold; with --reuse-
Expand Down Expand Up @@ -282,9 +280,7 @@ options:
exist yet.
--tracking-branch TRACKING_BRANCH
the branch every repository is created on (default:
main). One the project's YAML reader would read as
another type -- `null`, `true`, `007`, `2026` -- is
refused.
main)
--spec-path SPEC_PATH
where the spec leg is mounted, as a path in the
assembly root (default: spec). A path that is, holds
Expand Down
37 changes: 19 additions & 18 deletions scaffold-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@
WHAT THE NEW PROJECT MUST BE ABLE TO HOLD (#258, #261). The manifest is
rendered from the values the run is about to use, read with the project's own
YAML reader and put through the validator the template ships, BEFORE anything
is created and under `--dry-run` alike. A value the reader turns into something
else (`--id null`, `--tracking-branch true` or `007`: the template writes them
bare) is refused as `scaffold-manifest-misread`, and one the validator refuses
(a blank `--elected-by`, `--reference` or `--name`, an `--org` it cannot hold)
as `scaffold-manifest-invalid`, in the validator's own words; an
`--elected-on` is read in the validator's form only, on every Python. A name
that cannot be a directory's is refused as `scaffold-name-too-long`.
is created and under `--dry-run` alike. A value the reader would turn into
something else where a template line writes it bare is refused as
`scaffold-manifest-misread`. The template writes `id` and `tracking_branch`
between double quotes (#259), so `--id null` or `--tracking-branch 007` is
held as typed, and that refusal guards the template rather than the flags. A
value the validator refuses (a blank `--elected-by`, `--reference` or
`--name`, an `--org` it cannot hold) is refused as
`scaffold-manifest-invalid`, in the validator's own words; an `--elected-on`
is read in the validator's form only, on every Python. A name that cannot be a
directory's is refused as `scaffold-name-too-long`.

IDEMPOTENCY. It refuses to write into a remote or a working directory that
already exists. There is no --force: re-running over a live project is not a
Expand Down Expand Up @@ -202,9 +205,7 @@ def main(argv: list[str] | None = None) -> int:
help="the assembly-root name, e.g. Atlas")
parser.add_argument("--id", default=None,
help="lowercase project id (default: --project "
"lowercased). One the project's YAML reader "
"would read as another type -- `null`, `true`, "
"`007` -- is refused.")
"lowercased)")
parser.add_argument("--name", default=None, help="display name")
parser.add_argument("--visibility", choices=VISIBILITY_CHOICES,
default=None,
Expand Down Expand Up @@ -237,9 +238,7 @@ def main(argv: list[str] | None = None) -> int:
"that did not exist yet.")
parser.add_argument("--tracking-branch", default="main",
help="the branch every repository is created on "
"(default: main). One the project's YAML reader "
"would read as another type -- `null`, `true`, "
"`007`, `2026` -- is refused.")
"(default: main)")
parser.add_argument("--spec-path", default="spec",
help="where the spec leg is mounted, as a path in "
"the assembly root (default: spec). A path that "
Expand Down Expand Up @@ -425,11 +424,13 @@ def _refuse_unholdable_manifest(args, values: dict, policy) -> None:

`values` is the whole substitution table, so the project's own YAML
reader and `validate-manifest.py` are asked about the `project.yaml` the
run is about to write: a value the reader turns into something else
(`--id null`, `--tracking-branch 007`) is `scaffold-manifest-misread`, and
one the validator refuses (a blank `--elected-by`, `--org test+org`) is
`scaffold-manifest-invalid`. `shape_materialize.refuse_manifest_values`
asks both, and says why each is refused.
run is about to write: a value the reader would turn into something else
where a template line writes it bare is `scaffold-manifest-misread` (the
template quotes `id` and `tracking_branch`, #259, so `--id null` and
`--tracking-branch 007` are held as typed), and one the validator refuses
(a blank `--elected-by`, `--org test+org`) is `scaffold-manifest-invalid`.
`shape_materialize.refuse_manifest_values` asks both, and says why each is
refused.

A value a flag did not supply is named by where it came from, so the
sentence does not point at a flag nobody typed.
Expand Down
41 changes: 38 additions & 3 deletions scripts/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,9 @@
from repo_shape import ( # noqa: E402
free_plan_secret_hint,
COMMIT_RE, PROJECT_ID_RE, PYTHON, TREE_DIGEST_DEFINITION,
VISIBILITY_CHOICES, NamingPolicy, Refusal, checked_value, git_out,
load_yaml, recorded_gitlink, repo_basename, same_repository, tree_digest,
VISIBILITY_CHOICES, NamingPolicy, Refusal, YamlError, checked_value,
double_quoted, git_out, load_yaml, parse_yaml, recorded_gitlink,
repo_basename, same_repository, tree_digest,
)
from shape_materialize import ( # noqa: E402
RULESET_HINT, SHAPE_REPOSITORY, CommandFailed, check_program,
Expand Down Expand Up @@ -320,12 +321,46 @@ def rewritten(self, rows: list[dict]) -> str:
return "\n".join(out) + "\n"


def _member_id(value) -> str:
"""A row's `id:` as `_member_lines` spells it: bare where the holder's
reader takes the bare text back as that same text, and between double
quotes where it does not (#259).

THE MEMBER'S OWN `project.yaml` QUOTES ITS ID NOW. A project scaffolded
with `--id 007` used to write `id: 007`, which every reader took as the
number 7; this row then recorded `str(7)`, and the two agreed on a
misread. The template writes `id: "007"` since #259, so the id read here
is `007`, and written bare it would come back as 7: `validate-family.py`
would report `member-id-mismatch` for a member it had just pinned, and so
would `2026`, `20261011`, `true` and `null`. The reader is asked, not a
list of the words it treats as something else, so a word it learns later
is quoted with no edit here. An id that reads back bare, which is every
ordinary one, is written as it always was, so a row whose id reads back
as text does not move.

A ROW WHOSE BARE ID THE READER MISREADS MOVES, ONCE. A holder written
before #259 holds `id: 2026` for a member scaffolded with `--id 2026`,
and the next `bump`, `add` or `remove` of that holder rewrites the row as
`id: "2026"`. That is harmless: `validate-family.py` compares the id with
`str()`, and passes before and after.
"""
text = str(value)
try:
if parse_yaml(f"id: {text}\n") == {"id": text}:
return text
except YamlError:
# Bare, `[`, `{`, `&a`, `*a`, `!t` and an unclosed quote are not read
# as another type: the reader raises, and the holder would not load.
pass
return double_quoted(text)


def _member_lines(row: dict) -> list[str]:
"""One member as the YAML subset `repo_shape.parse_yaml` reads back."""
pin = row.get("pin") or {}
return [
f" - project: {row['project']}",
f" id: {row['id']}",
f" id: {_member_id(row['id'])}",
f" repository: {row['repository']}",
f" path: {row['path']}",
" pin:",
Expand Down
2 changes: 1 addition & 1 deletion templates/assembly-root/contracts/code-pin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ kind: pinned_contract_manifest

leg_role: code
source_repository: {{CODE_REPOSITORY}}
submodule_path: {{CODE_PATH}}
submodule_path: "{{CODE_PATH}}"

commit: "{{CODE_COMMIT}}"
revision_kind: commit
Expand Down
2 changes: 1 addition & 1 deletion templates/assembly-root/contracts/spec-pin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ kind: pinned_contract_manifest

leg_role: spec
source_repository: {{SPEC_REPOSITORY}}
submodule_path: {{SPEC_PATH}}
submodule_path: "{{SPEC_PATH}}"

commit: "{{SPEC_COMMIT}}"
revision_kind: commit
Expand Down
12 changes: 6 additions & 6 deletions templates/assembly-root/project.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ kind: project-manifest
# identically and owes no declaration.
# ===========================================================================

id: {{PROJECT_ID}}
id: "{{PROJECT_ID}}"
name: "{{PROJECT_NAME}}"

# The elected schema, and the document the election followed. For a project
Expand All @@ -44,7 +44,7 @@ reference: "{{REFERENCE}}"
# Electing the shape is a human's act (a `PA` decision, `CA`-constrained and
# `PM`-sequenced), so the manifest records whose act it was and when.
elected_by: "{{ELECTED_BY}}"
elected_on: {{ELECTED_ON}}
elected_on: "{{ELECTED_ON}}"

# The GitHub topic every one of this project's repositories carries. Names
# carry the human eye, topics carry the organisation's search, and the
Expand All @@ -59,12 +59,12 @@ topic: {{TOPIC}}
# in-place adoption it is the value the two NEW legs were created with — the
# assembly root IS the adopted source and keeps whatever visibility it
# already had on GitHub, unchanged by this tool.
visibility: {{VISIBILITY}}
visibility: "{{VISIBILITY}}"

# The branch `scripts/bootstrap.py` places each leg on, AT its pinned commit.
# The pin stays exact: `.gitmodules` carries no `branch=`, so there is exactly
# one authoritative answer to "what commit is this project".
tracking_branch: {{TRACKING_BRANCH}}
tracking_branch: "{{TRACKING_BRANCH}}"

# The openRepoShape revision this project was scaffolded from, pinned by
# commit and digest under the same rule the family's neutral products use: a
Expand Down Expand Up @@ -156,9 +156,9 @@ legs:
{{ASSEMBLY_NAMING}}
- role: spec
repository: {{SPEC_REPOSITORY}}
path: {{SPEC_PATH}}
path: "{{SPEC_PATH}}"
{{SPEC_NAMING}}
- role: code
repository: {{CODE_REPOSITORY}}
path: {{CODE_PATH}}
path: "{{CODE_PATH}}"
{{CODE_NAMING}}
10 changes: 5 additions & 5 deletions templates/family-root/family.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,25 @@ kind: family-manifest
# this file assert anything about a tree it does not own.
# ===========================================================================

id: {{FAMILY_ID}}
id: "{{FAMILY_ID}}"
name: "{{FAMILY_NAME}}"
org: {{ORG}}
org: "{{ORG}}"
repository: {{FAMILY_REPOSITORY}}

# Creating a family is a human's act, like electing the shape, so the manifest
# records whose act it was and when.
created_by: "{{CREATED_BY}}"
created_on: {{CREATED_ON}}
created_on: "{{CREATED_ON}}"

# `private`, `public` or `internal`. NOT ENFORCED HERE — GitHub is the source
# of truth for what a repository's visibility actually is — but recording it
# is what lets a reader tell why the members need a credential in CI.
visibility: {{VISIBILITY}}
visibility: "{{VISIBILITY}}"

# The branch `scripts/bootstrap.py` places each member on, AT its pinned
# commit. `.gitmodules` carries no `branch=`: there is exactly one
# authoritative answer to "what commit of this member is this family".
tracking_branch: {{TRACKING_BRANCH}}
tracking_branch: "{{TRACKING_BRANCH}}"

# The directory every member is mounted under. One name, so a reader and a
# validator cannot disagree about where to look.
Expand Down
2 changes: 1 addition & 1 deletion tests/test_adopt_github_exits.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ def test_a_source_on_another_branch_opens_its_pull_request_against_it(
assert_pushed(github, branch)
root = git("show", f"{SPLIT_BRANCH}:project.yaml",
cwd=github / ORG / f"{NORTHWIND}.git").stdout
assert f"\ntracking_branch: {branch}\n" in root, root
assert f'\ntracking_branch: "{branch}"\n' in root, root
assert "Base ref must be a branch" not in result.stderr


Expand Down
Loading
Loading