Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 4 additions & 2 deletions reports/divergences.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur

## Untriaged - add a disposition to `spec/divergences.json`

_None - every live divergence has a recorded disposition._
- **AGENTS.md > Repository Boundaries and Write Safety** - hand-modified in AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair (verbatim canonical)
- **AGENTS.md > Git and Commit Rules** - hand-modified in AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair (verbatim canonical)
- **AGENTS.md > Verification Discipline** - hand-modified in AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair (verbatim canonical)

## Mechanical re-vendor (verbatim stale copies)

A past hub revision, not the current canonical - the audit already flags these as DRIFT. Copy the current file down. No judgment needed.

- **.markdownlint-cli2.jsonc** (15): DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, homeassistant-purpleair
- **repo-config/configure.sh** (8): ESPHome-Config, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, PlexCleaner, ProjectTemplate, Utilities, Vantage-Config
- **repo-config/configure.sh** (7): ESPHome-Config, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, PlexCleaner, Utilities, Vantage-Config

## Promote candidates (intent uniform -> verbatim)

Expand Down
2 changes: 1 addition & 1 deletion spec/divergences.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "./divergences.schema.json",
"note": "Curated dispositions for known fleet divergences from the manifest canonicals - the burn-down ledger. spec/fidelity_honesty.py --report joins this against live fleet reality to write reports/divergences.md. A recorded divergence still present renders as a burn-down task with its disposition. A live divergence absent here renders as UNTRIAGED. A recorded divergence no longer live renders as resolved. Edit this file (not the generated report) and regenerate. dispositions cover per-repo file divergences from a verbatim or intent canonical. gaps cover files carried by the fleet but absent from spec/files.json. disposition vocabulary: re-vendor (drift-to-fix, copy the current canonical down), track (a gap to add to the manifest), accepted (a legitimate permanent divergence, no action), upstream-candidate (the downstream carries an improvement the hub should adopt, then re-vendor), investigate (recorded, decision pending).",
"note": "Curated dispositions for known fleet divergences from the manifest canonicals - the burn-down ledger. spec/fidelity_honesty.py --report joins this against live fleet reality to write reports/divergences.md. A recorded divergence still present renders as a burn-down task with its disposition. A live divergence absent here renders as UNTRIAGED. A recorded divergence no longer live renders as resolved. Edit this file (not the generated report) and regenerate. dispositions cover per-repo divergences from a verbatim or intent canonical - a whole file, or a verbatim section whose unit key is the file path, then a space-greater-space delimiter, then the section name (for example AGENTS.md > Git and Commit Rules). gaps cover files carried by the fleet but absent from spec/files.json. disposition vocabulary: re-vendor (drift-to-fix, copy the current canonical down), track (a gap to add to the manifest), accepted (a legitimate permanent divergence, no action), upstream-candidate (the downstream carries an improvement the hub should adopt, then re-vendor), investigate (recorded, decision pending).",
"dispositions": [
{ "path": ".editorconfig-checker.json", "repos": ["HomeAutomation-Config", "HolidayLights"], "disposition": "accepted", "reason": "Both carry a legitimate repo-specific Exclude list (HomeAutomation-Config excludes a Vantage/ subtree, HolidayLights excludes .fseq sequence files). The uniform Disable block is carried intent-equivalent. Exclude is inherently repo-local, which is why the unit is intent, not verbatim.", "tracking": null },
{ "path": ".markdownlint-cli2.jsonc", "repos": ["aiopurpleair", "PhotoCleaner", "AudioCleaner"], "disposition": "re-vendor", "reason": "Verbatim config held as a hand-modified copy in these three (not a past hub revision). Restore the current canonical.", "tracking": null },
Expand Down
70 changes: 50 additions & 20 deletions spec/fidelity_honesty.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,7 @@
REPORT_PATH = "reports/divergences.md" # the generated, checked-in burn-down report (--report)


def canonical_text(entry):
"""The hub's canonical for a unit: its reference snippet, else its own root copy."""
ref = entry.get("reference") or entry["path"]
try:
return (audit.ROOT / ref).read_text(encoding="utf-8", errors="replace")
except OSError:
return None
SECTION_SEP = " > " # joins a file path and a verbatim section name into one unit label (path > section), ASCII


def fetch(slug, path, ref):
Expand All @@ -46,46 +40,82 @@ def fetch(slug, path, ref):
return base64.b64decode(content["content"]).decode("utf-8", "replace")


def check_units(spec):
"""Yield the fleet's check units - whole intent/verbatim files, plus each verbatim section of any file.

Each is (label, fidelity, entry_appliesTo, section_appliesTo, downstream_path, canonical_path, extract).
downstream_path is where the repo's copy lives (always the entry's `path`); canonical_path is the hub's
canonical (the `reference` snippet for a reference-backed unit, else the same path). extract is None for a
whole file, or a section extractor for a verbatim section (the region is compared, not the whole file).
"""
for e in spec["files"]["baseline"]:
fid = e.get("fidelity")
if fid in ("intent", "verbatim"):
yield (e["path"], fid, e.get("appliesTo", "*"), "*", e["path"], e.get("reference") or e["path"], None)
sections = e.get("sections")
if not isinstance(sections, list):
continue
for elt in sections:
if isinstance(elt, dict) and elt.get("fidelity") == "verbatim" and isinstance(elt.get("name"), str):
name = elt["name"]
Comment thread
ptr727 marked this conversation as resolved.
yield (f"{e['path']}{SECTION_SEP}{name}", "verbatim", e.get("appliesTo", "*"),
elt.get("appliesTo", "*"), e["path"], e.get("reference") or e["path"],
(lambda t, n=name: audit.extract_section(t, n)))


def fidelity_pass(spec):
defaults = spec["registry"].get("defaults", {})
repos = [r for r in spec["registry"]["repos"] if r.get("status") == "cataloged"]
units = [e for e in spec["files"]["baseline"] if e.get("fidelity") in ("intent", "verbatim")]

spreads = [] # (entry, spread dict) for the full table
spreads = [] # (unit dict, spread dict) for the full table
promote = [] # intent units that are uniform fleet-wide
mislabel = [] # verbatim units that diverge non-stale

for e in units:
path, fid = e["path"], e["fidelity"]
canon = canonical_text(e)
for label, fid, ent_ap, sec_ap, down_path, canon_path, extract in check_units(spec):
unit = {"path": label, "fidelity": fid}
try:
canon_raw = (audit.ROOT / canon_path).read_text(encoding="utf-8", errors="replace")
except OSError:
canon_raw = None
canon = None if canon_raw is None else (canon_raw if extract is None else extract(canon_raw))
if canon is None:
spreads.append((e, None))
spreads.append((unit, None))
continue
canon_hash = audit.content_hash(canon)
history = {audit.content_hash(t) for t in audit.git_file_history(e.get("reference") or path)}
# Hash each past revision's same region (a section may be absent in an old revision -> skip it).
history = set()
for t in audit.git_file_history(canon_path):
region = t if extract is None else extract(t)
if region is not None:
history.add(audit.content_hash(region))
spread = {"match": [], "stale": [], "differs": [], "unavailable": []}
for r in repos:
if not audit.applies(e.get("appliesTo", "*"), audit.repo_selectors(r, defaults)):
sel = audit.repo_selectors(r, defaults)
if not (audit.applies(ent_ap, sel) and audit.applies(sec_ap, sel)):
continue
text = fetch(audit.repo_slug(r), path, r.get("groundTruthBranch", "main"))
text = fetch(audit.repo_slug(r), down_path, r.get("groundTruthBranch", "main"))
if text is None: # missing (404) or present-but-non-inline (too large / encoding "none")
spread["unavailable"].append(r["name"])
continue
dh = audit.content_hash(text)
region = text if extract is None else extract(text)
if region is None: # a verbatim section whose heading is absent downstream
spread["differs"].append(r["name"])
continue
dh = audit.content_hash(region)
if dh == canon_hash:
spread["match"].append(r["name"])
elif dh in history:
spread["stale"].append(r["name"])
else:
spread["differs"].append(r["name"])
spreads.append((e, spread))
spreads.append((unit, spread))
# A verbatim candidate has NO hand-modified copy ("differs") and at least one confirmed match with
# the current canonical. Stale copies do not disqualify it - verbatim would flag them "stale ->
# re-vendor", which is the point. A unit that is entirely stale/unavailable is not confirmed uniform.
if fid == "intent" and spread["match"] and not spread["differs"]:
promote.append((e, spread))
promote.append((unit, spread))
if fid == "verbatim" and spread["differs"]:
mislabel.append((e, spread))
mislabel.append((unit, spread))
return spreads, promote, mislabel


Expand Down
11 changes: 11 additions & 0 deletions spec/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ def check_selector(where, applies_to):
continue
for elt in sections:
if isinstance(elt, dict):
if not isinstance(elt.get("name"), str) or not elt.get("name"):
errors.append(f"files.json: {path} section object missing a non-empty string 'name': {elt!r}")
check_selector(f"{path} section '{elt.get('name', '?')}'", elt.get("appliesTo", "*"))
# A section may carry its own fidelity (intent default, or verbatim for a universal rule block
# checked byte-for-byte). verbatim is meaningful only on a markdown file, where the heading
Expand All @@ -304,6 +306,15 @@ def check_selector(where, applies_to):
div = load("spec/divergences.json")
repo_names = {r.get("name") for r in repos["repos"] if isinstance(r, dict)}
manifest_paths = {i.get("path") for i in baseline if isinstance(i, dict)}
# A verbatim section is an addressable unit too, labelled "path > section" (matches fidelity_honesty's
# SECTION_SEP), so a section-scoped divergence can carry its own disposition. Only well-formed section
# entries produce a label - a malformed one is already reported by the files.json checks above.
for i in baseline:
if not isinstance(i, dict) or not isinstance(i.get("path"), str) or not isinstance(i.get("sections"), list):
continue
for elt in i["sections"]:
if isinstance(elt, dict) and elt.get("fidelity") == "verbatim" and isinstance(elt.get("name"), str):
manifest_paths.add(f"{i['path']} > {elt['name']}")
Comment thread
ptr727 marked this conversation as resolved.
Comment thread
ptr727 marked this conversation as resolved.
Comment thread
ptr727 marked this conversation as resolved.
# Guard the root type: a non-object root (a list from a bad edit) would crash the .get() calls below.
if not isinstance(div, dict):
errors.append("divergences.json: root must be an object")
Expand Down