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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ install command). Seven steps, in order, and step 4 is the one you must not skip
```

2. **You resolve the questions, in writing, in the plan.** Each unresolved
entry carries a `question:`. Set `leg:` to `spec`, `code`, `root` or `drop`
entry carries a `question:`; so does one whose `leg:` is its rule's default,
which `execute` takes as written (`drop` for a `single-repository.yaml`
record). Set `leg:` to `spec`, `code`, `root` or `drop`
AND add a `resolution:` line saying why — read the repository to answer it
(does the specification cite `examples/golden-run/`, or do the tests?). An
answer with no reason beside it is a guess that will be read as a finding.
Expand Down
119 changes: 113 additions & 6 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
honestly call, every path holding one of the source's own
submodules, and every path `execute` will not extract into a leg
as it is named, carry `leg: null`, `review_required: true` and
the QUESTION to ask.
the QUESTION to ask. A question whose rule names a default
carries that default as its `leg:` instead, still
`review_required: true`: the source's own `single-repository.yaml`
record, `drop` (#528).
A leg path over a path it keeps in the root itself is a NOTE,
not a refusal: the answer is the person's to change. A
`--pin <product>@<40 hex>` is read as `scaffold-project.py
Expand Down Expand Up @@ -1495,7 +1498,15 @@


class Entry:
"""One row of the plan: a path, the leg proposed for it, and why."""
"""One row of the plan: a path, the leg proposed for it, and why.

`default` is the answer the entry's rule names for its question, or
None, and `review_required` is the verdict's: whether a question was
asked. The two part only in `with_its_default`, the one place a
question's `leg:` is filled in, which `cmd_plan` alone calls; `walk`
leaves every question's `leg` None, so the doctor, which walks a leg
with it, still reads one as a question (#528).
"""

def __init__(self, path: str, verdict: Verdict, files: int, size: int):
self.path = path
Expand All @@ -1504,9 +1515,23 @@
self.reason = verdict.reason
self.confidence = verdict.confidence
self.question = verdict.question
self.default = verdict.default
self.review_required = verdict.review_required
self.files = files
self.bytes = size

def with_its_default(self) -> "Entry":
"""This question with its rule's default written in as its `leg:`,
still `review_required` and still carrying the question, the rule
and the reason, so a reader sees what was proposed and why."""
out = Entry(self.path, Verdict(None, self.rule, self.reason,
self.confidence), self.files,
self.bytes)
out.question = self.question
out.default = self.default
out.leg = self.default
return out

def write(self, lines: list[str]) -> None:
lines.append(f" - path: {y(self.path)}")
emit(lines, "leg", self.leg, 4)
Expand All @@ -1515,7 +1540,7 @@
emit(lines, "reason", self.reason, 4)
emit(lines, "files", self.files, 4)
emit(lines, "bytes", self.bytes, 4)
emit(lines, "review_required", self.leg is None, 4)
emit(lines, "review_required", self.review_required, 4)
if self.question:
emit(lines, "question", self.question, 4)

Expand Down Expand Up @@ -1553,6 +1578,61 @@
return sorted(entries, key=lambda e: e.path)


#: The path policy's rule for the source's own staying-single record (#528).
#: Its question and its default are the record's, so `plan` asks them only
#: of a file that IS the record (`ask_about_a_record_only`).
RECORD_RULE = "ambiguous-single-repository-record"


def ask_about_a_record_only(policy: PathPolicy, entries: list[Entry],
source: "Source") -> list[Entry]:
"""`entries`, with a top-level `single-repository.yaml` that is no
record asked the question the rules below the record's ask (#528).

`kind:` DECIDES, NOT THE NAME, for the record's one reader, the Triad
advisory (`shape_advisory.read_record`). A file of that name declaring
`kind: release-config` silences nothing, and the same `plan` run said
so in a NOTE while the record's question told the person the file
records that the repository stays single, and its default would have
dropped it. So RECORD_RULE answers only a file that declares
`single-repository-record` in the commit being planned, read as the
advisory reads it there; any other is classified as if the policy held
no such rule, which for a top-level YAML file is the general question,
asked as before. A source with no entry RECORD_RULE answered is not
read at all.
"""
if not any(entry.path == shape_advisory.RECORD_FILE
and entry.rule == RECORD_RULE for entry in entries):
return entries
state, _, _ = shape_advisory.read_record(
shape_advisory.Tree(source.path, source.commit))
if state == shape_advisory.RECORD_SILENCES:
return entries
return [Entry(entry.path,
policy.classify_file(entry.path, skip=(RECORD_RULE,)),
entry.files, entry.bytes)
if entry.path == shape_advisory.RECORD_FILE
and entry.rule == RECORD_RULE else entry
for entry in entries]


def with_defaults(entries: list[Entry]) -> list[Entry]:
"""`entries`, each question whose rule names a default answer written
with that default as its `leg:` (#528).

RULED BY BRETT HEAP ON 2026-10-12: a source's own `single-repository.yaml`
is asked drop or root, "the default answer is drop". So `plan` writes
`leg: drop`, and keeps `review_required: true`, the rule and the
question beside it: `check` passes it, and `execute` drops the path
unless a person writes `root`. A path with no default is untouched, as
is every question `ask_about_submodules` or `ask_about_refused_paths`
asked in place of a rule's, because those carry none: called after
them, this fills in only what a rule proposed and nothing replaced.
"""
return [entry.with_its_default() if entry.leg is None and entry.default
else entry for entry in entries]


def ask_about_submodules(policy: PathPolicy, entries: list[Entry],
tree: list) -> list[Entry]:
"""`entries`, with every entry that holds a gitlink of the source's
Expand Down Expand Up @@ -1646,6 +1726,9 @@
"own `leg:`. An entry added beside this one for a path "
"it covers is `plan-covered-twice`."
+ _refused_replacements(own, held))
# NO DEFAULT, from either rule (#528): the question is the submodules',
# which no rule's default answers, and one carried here would be
# written in as the entry's `leg:` by `with_its_default`.
out = Entry(entry.path, Verdict(None, asked.rule, asked.reason,
asked.confidence), entry.files,
entry.bytes)
Expand Down Expand Up @@ -1749,6 +1832,8 @@
"stands: `root` would keep a submodule in the assembly root, "
"which keeps none of the source's" if held else
"`root` or `drop` is the only answer `execute` takes for it")
# NO DEFAULT (#528): the rule's was an answer to its own question, and
# this one is why the name cannot go into a leg, which a person answers.
out = Entry(entry.path, Verdict(None, entry.rule, entry.reason,
entry.confidence), entry.files,
entry.bytes)
Expand Down Expand Up @@ -2012,6 +2097,8 @@
"# human or an AI answers it by setting `leg:` to spec, code, root or",
"# drop and adding one line of `resolution:` beside it. `check` then",
"# has to pass, and `execute` REFUSES while any leg is still null.",
"# An entry whose rule names a default arrives with it as its `leg:`",
"# and `execute` takes it as written: confirm it or change it.",
"",
f"generated_on: {_dt.date.today().isoformat()}",
f"tool: {SHAPE_REPOSITORY} adopt-project.py",
Expand Down Expand Up @@ -3495,7 +3582,7 @@
return


def _print_plan_report(args, source: Source, tree: list, entries: list,

Check failure on line 3585 in adopt-project.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=opensoft_openRepoShape&issues=AaE_fa8lQdQjWA2_Vf-a&open=AaE_fa8lQdQjWA2_Vf-a&pullRequest=566
follow_ups: list, seeded: list, out: Path,
pins: dict | None = None) -> None:
"""What `plan` says to the terminal once the file is on disk.
Expand Down Expand Up @@ -3533,7 +3620,16 @@
for line in _tracking_branch_notes(args, source):
print(line)
unresolved = [e for e in entries if e.leg is None]
# A default is still a question (#528): it is printed with the question,
# before the paths `execute` refuses over, which stay last.
proposed = [e for e in entries if e.leg is not None and e.review_required]
print(f"\nplan written to {out}")
if proposed:
print(f"{len(proposed)} path(s) carry their rule's default answer, "
"which `execute` takes as written unless it is changed:")
for entry in proposed:
print(f" {_shown(entry.path)} (leg: {entry.leg})\n"
f" {entry.question}")
if unresolved:
print(f"{len(unresolved)} path(s) need a human or an AI to answer a "
"question before `execute` will run:")
Expand Down Expand Up @@ -3568,16 +3664,27 @@
# or naming it as a spec directory, would be about nothing, and a leg it
# alone fed is seeded -- which the plan records, as `check` will say of
# the plan as written. A source with no such path plans as it did.
ruled = walk(policy, [(path, size) for path, _, _, size in tree])
#
# A `single-repository.yaml` that declares no record is asked as any
# other top-level data file is, before anything reads the entries
# (#528). The default a rule names is written into an entry last, after
# both re-asks: `ask_about_refused_paths` passes over a path answered
# `drop`, so a default written in first would be taken for a person's
# answer there. What the rules imply is read without it, and it changes
# none of what is read from them, because `drop` puts the path in no leg
# and keeps nothing in the root.
ruled = ask_about_a_record_only(
policy, walk(policy, [(path, size) for path, _, _, size in tree]),
source)
implied = ask_about_refused_paths(ruled, tree)
materialized = _predict_collisions(
implied, attributes_link=any(path == ATTRIBUTES and mode == SYMLINK_MODE
for path, mode, _, _ in tree))
follow_ups = follow_ups_for(source, implied, names, args.spec_path,
args.code_path, materialized)
seeded = seeded_legs(assigned_paths(implied))
entries = ask_about_refused_paths(
ask_about_submodules(policy, ruled, tree), tree)
entries = with_defaults(ask_about_refused_paths(
ask_about_submodules(policy, ruled, tree), tree))
out = Path(args.out)
write_lf(out, render_plan(args, source, entries, names, pins, follow_ups,
seeded))
Expand Down
52 changes: 52 additions & 0 deletions contracts/path-classification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ kind: path-classification-policy
# evidence or the code's test fixture?" is right every time and costs one
# sentence of human attention. The plan marks those `review_required: true`
# with `leg: null`, and `adopt-project.py execute` REFUSES while any remain.
# A rule that asks may name a `default:` answer, `drop` and nothing else:
# the plan writes it as `leg:`, still `review_required: true` with the
# question beside it, and `execute` takes it unless a person changes it.
#
# THE ROOT CLASS IS THE ONE PEOPLE GET WRONG. Ruled by Brett Heap on
# 2026-09-02 for the MedxEHR conversion: `.specify/` (vendored Spec-Kit
Expand Down Expand Up @@ -419,6 +422,55 @@ rules:
`adopt-project.py` refuses a plan that keeps the repository's own file
there.

# A REPOSITORY'S OWN `single-repository.yaml` is this standard's optional
# record that a project STAYS one repository
# (`contracts/single-repository-record.yaml`), so a repository that carries
# one and is adopted into the Triad holds a record of the very decision the
# adoption reverses (#528). The general question below offers `spec`,
# `code` and `root` for "a declaration, run-time configuration or wiring",
# and the file is none of those: its one reader is the Triad advisory
# (`scripts/shape_advisory.py`), which is silent in an assembly root
# whatever the file holds, so neither leg has any use for it. So it is asked
# a question of its own that names the file and offers drop or root, drop
# by default (Brett Heap's ruling of 2026-10-12, the A7 line of #457).
#
# THE DEFAULT IS WRITTEN INTO THE PLAN, AND IT IS STILL A QUESTION. `plan`
# writes `leg: drop` with `review_required: true`, this rule and its
# question, so `check` passes it and `execute` drops the file unless a
# person writes `root`, and the split commit names it among the paths it
# dropped. Nothing refuses `root` here, unlike `ambiguous-project-yaml`:
# the file blocks nothing the shape writes and no validator, gate or
# doctor row reads it. As for the rules above, what `execute` does is said
# of a repository WHEN IT IS ADOPTED: `shape-doctor.py --placement-plan`
# prints this question too, by name, for a leg already split, and reads
# the leg's copy as a question, because the verdict's leg stays null.
#
# `kind:` DECIDES, NOT THE NAME, as it does for the advisory. `plan` asks
# this question only of a file that declares `kind:
# single-repository-record` at the commit it plans. One of this name that
# declares anything else, or does not parse, is no record, and is asked
# the general question below as any other top-level data file is.
- id: ambiguous-single-repository-record
paths:
- "single-repository.yaml"
leg: ambiguous
default: drop
reason: >-
A top-level `single-repository.yaml` is this standard's record that a
project STAYS a single repository: who decided it, when and why.
Adopting the repository into the Triad is the decision it records being
reversed, so the file no longer describes the project it sits in. It
confers nothing, and its only reader is the Triad advisory, which is
silent in an assembly root whatever the file holds.
question: >-
This file records that the repository stays a single repository, and
adopting it makes the project a Triad. Drop it (drop, the default: the
decision it records is the one being reversed), or keep it in the
assembly root (root) as a record of who decided to stay single and why?
Neither leg has any use for it, and nothing but the Triad advisory
reads it, which is silent in an assembly root whatever it says, so
keeping it breaks nothing.

- id: ambiguous-top-level-yaml
paths:
- "*.yaml"
Expand Down
Loading
Loading