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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Running backlog (kept here, in a committed file, so the guidance survives across
- Sweep `ManagePackageVersionsCentrally` placement to `Directory.Packages.props` fleet-wide (PlexCleaner sets it in `Directory.Build.props`, off the CODESTYLE canonical).
- Finish onboarding hardening (from #310): make the `AUDIT.md` audit a required onboarding step and run the per-type cold-start self-tests tracked in `reports/conformance-matrix.md` (`STANDUP.md` is already in place).
- Refresh the README (it has gone stale) and evaluate a lower-maintenance structure - for example a per-section index that points into each doc with a one-line description, keeping the README as the adoption and audit-instruction entry point with pointers to the other docs. A per-section index trades brevity for a sync obligation: it must track what the docs contain.
- Add a linter-only Python project type for codegen/boilerplate Python - code that runs during another tool's build to emit generated source (e.g. ESPHome codegen that produces enriched C++ at compile time), so it ships no unit tests and no coverage and needs only the linter. Keep it distinct from the existing `python` type, which is utility code that can and should carry unit tests and coverage (as in PlexCleaner). Until it exists, ESPHome-Config stays `source-only` and its `+python` reclassification is deferred - accept its one outstanding validation finding meanwhile.

## License

Expand Down
38 changes: 33 additions & 5 deletions spec/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,19 @@ def check_verbatim(label, down_text, canonical_rel, extract=None):
return [("DRIFT", f"verbatim: {label} differs from the canonical and matches no past hub revision - the repo modified fixed content, review it")]


def classify_branch_drift(base, main, develop):
"""Split the paths main changed since the merge-base by direction, given {path: object-sha} maps
for the merge-base, main-head, and develop-head trees. `behind`: main moved the path and develop
still holds the merge-base object - a genuine forward-sync gap. `diverged`: both branches moved
the path, so develop may already supersede main and a blind sync from main would revert it. A
path only develop moved is not main-side drift and is excluded. Returns (behind, diverged)."""
changed_on_main = {p for p in set(base) | set(main) if base.get(p) != main.get(p)}
differ = [p for p in changed_on_main if main.get(p) != develop.get(p)]
behind = sorted(p for p in differ if develop.get(p) == base.get(p))
diverged = sorted(p for p in differ if develop.get(p) != base.get(p))
return behind, diverged


def audit_repo(entry, spec):
findings = [] # (kind, text)
slug = repo_slug(entry)
Expand Down Expand Up @@ -510,11 +523,16 @@ def audit_repo(entry, spec):
findings.append(("DRIFT", f"branch: {len(cmp['files'])}+ main-side path change(s) develop lacks (forward-sync needed; tree unavailable or too large to filter cherry-pick noise)"))
else:
objs = {name: {e["path"]: e["sha"] for e in t["tree"] if e["type"] in ("blob", "commit")} for name, t in trees.items()}
changed_on_main = {p for p in set(objs["base"]) | set(objs["main"]) if objs["base"].get(p) != objs["main"].get(p)}
lacking = sorted(p for p in changed_on_main if objs["main"].get(p) != objs["develop"].get(p))
if lacking:
shown = ", ".join(lacking[:8]) + (" ..." if len(lacking) > 8 else "")
findings.append(("DRIFT", f"branch: {len(lacking)} main-side path change(s) develop lacks (forward-sync needed): {shown}"))
# Split main-side drift by direction, never flagging every difference as a develop deficit.
# A path develop still holds at the merge-base is a genuine forward-sync gap.
# A path both branches moved is diverged, and develop may already supersede main.
behind, diverged = classify_branch_drift(objs["base"], objs["main"], objs["develop"])
if behind:
shown = ", ".join(behind[:8]) + (" ..." if len(behind) > 8 else "")
findings.append(("DRIFT", f"branch: {len(behind)} main-side path change(s) develop lacks (forward-sync needed): {shown}"))
if diverged:
shown = ", ".join(diverged[:8]) + (" ..." if len(diverged) > 8 else "")
findings.append(("DRIFT", f"branch: {len(diverged)} path(s) changed on both main and develop since the merge-base - reconcile before promoting (develop may already supersede main): {shown}"))

# --- General settings ---
expected = dict(spec["settings"])
Expand Down Expand Up @@ -954,6 +972,16 @@ def _selftest():
else:
print(" ok cspell: workspace cSpell word list detected, a plain cspell.json mention is not")

# branch-drift direction split, covering modify/add/delete on main and a develop-only change
bd_base = {"keep": "a", "moda": "1", "modb": "2", "deld": "e", "devonly": "x"}
bd_main = {"keep": "a", "moda": "9", "modb": "9", "add": "n", "devonly": "x"} # moved moda/modb, added 'add', deleted 'deld'
bd_dev = {"keep": "a", "moda": "1", "modb": "7", "add": "m", "deld": "e", "devonly": "y"} # still at base on moda/deld, moved modb/add/devonly
bd_behind, bd_diverged = classify_branch_drift(bd_base, bd_main, bd_dev)
if bd_behind != ["deld", "moda"] or bd_diverged != ["add", "modb"]:
ok = False
print(f" FAIL branch-drift classify -> behind={bd_behind} diverged={bd_diverged}")
else:
print(" ok branch-drift: behind (modify/delete develop still at base) vs diverged (both moved), develop-only excluded")
print("SELFTEST PASS" if ok else "SELFTEST FAIL")
return 0 if ok else 1

Expand Down