Skip to content
2 changes: 1 addition & 1 deletion AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ A check with `intentRef`/`workflowRef` points at the prose section that owns the
- **docker** - registry layer cache (`buildcache-<branch>`, never `type=gha`), the size-limited Docker Hub README is published via the docker-readme task, and the image always re-pushes on publish.
- **hugo** - the build fails on a generator warning, the URL-parity gate asserts a length floor before comparing, the rendered output is untracked, the generator is pinned by version and checksum and declared once, a vendored tree records its upstream ref, and the deploy asserts what the host serves (the release id and the environment). Retention is bounded by a declared count with one side recorded as owning the prune, which is the deploy where its credential can observe the destination and the host where that credential is confined write-only, so grade which shape the repo uses rather than looking for a prune step. Deploy credentials are per-environment, which `spec/secrets.json` cannot express, so a clean **repo-setup** verdict says nothing about whether the environments are configured.
- **branch-model** - `main` and `develop` both exist and are protected, and the live rulesets match [`repo-config/*.json`][repo-config] by normalized diff (below).
- **carried-scope** - the repo carries no file the hub hosts rather than carries. The set is derived, not listed: the hub's git-tracked paths minus the [`spec/files.json`][files] baseline, so a file dropped from the manifest starts being reported on the next run with no retirement list to remember to edit. The remedy is the opposite of every other file finding, a **deletion**, since the repo reaches the hub's copy per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. The match is on path alone, so a hit is a candidate and not a verdict: a repo's own content at a path the hub also uses matches while carrying nothing of the hub's, which the first fleet run showed twice, a KiCad tooling doc at `scripts/README.md` and per-repo formatting hooks at `.husky/pre-commit`. A [`spec/divergences.json`][divergences] `gaps` disposition decides which case a hit is, so only `retire` asserts a deletion, `accepted` closes a collision or a repo-owned file, and an untriaged hit is read before it is acted on.
- **carried-scope** - the repo carries no file the hub hosts rather than carries. The set is derived, not listed: the hub's git-tracked paths minus the [`spec/files.json`][files] baseline, so a file dropped from the manifest starts being reported on the next run with no retirement list to remember to edit. The manifest's `repoOwnedTrees` entries name hub directories no repo carries, such as `tests`, and every hub path below one is left out of the set. The remedy is the opposite of every other file finding, a **deletion**, since the repo reaches the hub's copy per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. The match is on path alone, so a hit is a candidate and not a verdict: a repo's own content at a path the hub also uses matches while carrying nothing of the hub's, which the first fleet run showed twice, a KiCad tooling doc at `scripts/README.md` and per-repo formatting hooks at `.husky/pre-commit`. A [`spec/divergences.json`][divergences] `gaps` disposition decides which case a hit is, so only `retire` asserts a deletion, `accepted` closes a collision or a repo-owned file, and an untriaged hit is read before it is acted on.
Comment thread
ptr727 marked this conversation as resolved.
- **verbatim-tree** - every applicable `trees[]` declaration in [`spec/files.json`][files] owns its target tree. The audit reports missing files as letter findings, stale or modified bytes as drift, and extra files under a pruned target as drift. An unreadable or truncated repository tree is undecided and produces drift rather than a clean result.
- **repo-setup** - every required secret for the repo's publish mechanisms is configured, and no forbidden secret is present (per [`spec/secrets.json`][secrets]).
- **runtime-secrets** - a repo-scoped runtime-secrets directory, when present, is named `.secrets/` (dotted, not a bare `secrets/`), and a single opaque credential file carries no extension. Every real secret file has a tracked `<name>.example` beside it cataloged in `.secrets/README.md`. Those parts are N/A for a repo with no such directory. See [GOVERNANCE.md "Repo-Scoped Secrets"][governance-repo-scoped-secrets]. Every env and secret file the repo names, tracked or not, follows the env and secret file rule of [CODESTYLE.md "Script and Utility File Names"][codestyle-file-names]. `spec/audit.py` reports a tracked name that breaks the rule's form at **drift**. It reads each file directly in a `.secrets/` directory and each `.env` file elsewhere whose name does not open on a dot. It sets a trailing `.example` aside first, and the directory's own `README.md`, `.gitignore`, and `.gitkeep` are not secret files. The rest is judged by hand. A hyphen joins words only inside a host field, which takes the DNS label form. The check passes a hyphenated word in any field, since it cannot tell which field it is. A host field is a short name, never a fully qualified one. The check cannot tell an extension from a dot-joined last field of an extensionless name, a two-label host included. A consumer limit applies only where a name is used as a Docker secret or maps to a variable. A host-side name is judged where the repo's docs and scripts spell it out, never by reading a host. An `ENV_FILE` path and a compose `env_file` are such places. Renaming a file on a host is the maintainer's step, listed in the resync pull request.
Expand Down
4 changes: 2 additions & 2 deletions RESYNC.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ The order is load-bearing. Each step below either changes the rules the later st

Every other finding in this procedure is satisfied by adding or replacing content. This one is satisfied by removing it, which makes it the only class where acting on a wrong finding loses something.

The detector is derived rather than listed: the hub's git-tracked paths minus the [`spec/files.json`][files] baseline is what the hub hosts and no repository carries. That means a file dropped from the manifest starts being reported on the next run with no retirement list to maintain, and it also means **the match is on path alone**, so a hit is a candidate and not a verdict.
The detector is derived rather than listed: the hub's git-tracked paths minus the [`spec/files.json`][files] baseline is what the hub hosts and no repository carries. Every hub path below a `repoOwnedTrees` entry in that file, such as `tests`, is left out. That means a file dropped from the manifest starts being reported on the next run with no retirement list to maintain, and it also means **the match is on path alone**, so a hit is a candidate and not a verdict.

```mermaid
flowchart LR
candidate["hub git-tracked path, not in spec/files.json"] --> disp["check spec/divergences.json disposition"]
candidate["hub git-tracked path, not in spec/files.json or a repo-owned tree"] --> disp["check spec/divergences.json disposition"]
disp -->|"retire"| delete["delete the file"]
disp -->|"accepted"| keep["keep, closed permanently"]
disp -->|"untriaged"| read["read before touching"]
Expand Down
38 changes: 21 additions & 17 deletions spec/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,29 @@ def hub_tracked(rev=None):
return frozenset(paths)


def hub_paths_below(source, include=("**/*",), rev=None):
"""Hub-tracked paths below a declared source directory, mapped from the path relative to it, that match its include patterns."""
root = source.rstrip("/") + "/"
return {
path.removeprefix(root): path
for path in hub_tracked(rev)
if path.startswith(root) and tree_path_included(path.removeprefix(root), include)
}


def hub_only_paths(spec, rev=None):
"""Hub-tracked paths the manifest does not declare, so a downstream copy is hub-hosted content rather than a carry."""
declared = {e["path"] for e in spec["files"]["baseline"]}
tree_paths = set()
"""Hub-tracked paths the manifest does not declare, so a downstream copy is hub-hosted content rather than a carry.

A path below a declared repo-owned tree is left out, since a downstream file there is that repo's own.
"""
excluded = {e["path"] for e in spec["files"]["baseline"]}
for declaration in spec["files"].get("trees", []):
root = declaration["source"].rstrip("/") + "/"
tree_paths.update(
path
for path in hub_tracked(rev)
if path.startswith(root)
and tree_path_included(path.removeprefix(root), declaration["include"])
excluded.update(
hub_paths_below(declaration["source"], declaration["include"], rev).values()
)
return hub_tracked(rev) - declared - tree_paths
for declaration in spec["files"].get("repoOwnedTrees", []):
excluded.update(hub_paths_below(declaration["source"], rev=rev).values())
return hub_tracked(rev) - excluded


def gap_dispositions(spec):
Expand Down Expand Up @@ -2079,14 +2089,8 @@ def check_tree_verbatim(run):
for declaration in run.spec["files"].get("trees", []):
if not applies(declaration.get("appliesTo", "*"), run.sel):
continue
source_root = declaration["source"].rstrip("/")
target_root = declaration["target"].rstrip("/")
source_files = {
path.removeprefix(source_root + "/"): path
for path in hub_tracked()
if path.startswith(source_root + "/")
and tree_path_included(path.removeprefix(source_root + "/"), declaration["include"])
}
source_files = hub_paths_below(declaration["source"], declaration["include"])
target_files = {
path.removeprefix(target_root + "/"): path
for path in run.entries
Expand Down
3 changes: 3 additions & 0 deletions spec/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
{ "source": ".github/skills", "target": ".github/skills", "fidelity": "verbatim-tree", "appliesTo": "*", "include": ["**/*"], "prune": true },
{ "source": "lib/sh", "target": "lib/fleet/sh", "fidelity": "verbatim-tree", "appliesTo": ["host-secrets"], "include": ["**/*"], "prune": true }
],
"repoOwnedTrees": [
{ "source": "tests", "reason": "The hub's own test suite is never carried. Every repo with code keeps its own tests here, so a shared file name is a path collision rather than a carry." }
],
"baseline": [
{ "path": "CLAUDE.md", "fidelity": "verbatim", "whole": true, "appliesTo": "*" },
{ "path": "AGENTS.md", "fidelity": "intent", "sections": [{ "name": "Fleet Bootstrap", "fidelity": "verbatim" },
Expand Down
13 changes: 13 additions & 0 deletions spec/files.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@
}
}
},
"repoOwnedTrees": {
"description": "Hub directories no repo carries, where a downstream file is that repo's own content. The hub-only check in spec/audit.py skips every hub path below one.",
"type": "array",
"items": {
"type": "object",
Comment thread
ptr727 marked this conversation as resolved.
"required": ["source", "reason"],
"additionalProperties": false,
"properties": {
"source": { "type": "string", "minLength": 1 },
"reason": { "type": "string", "minLength": 1 }
}
}
},
"baseline": {
"type": "array",
"items": {
Expand Down
2 changes: 1 addition & 1 deletion spec/project-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
"carried-scope": {
"appliesTo": "*",
"checks": [
{ "id": "carried.hubonly.absent", "verdict": "letter", "assert": "The repo carries no file the hub hosts rather than carries. spec/audit.py derives the set as the hub's git-tracked paths minus the spec/files.json baseline, and reports each one the repo's ground-truth branch also holds. The remedy is a deletion rather than a re-vendor, since the repo reaches the hub's copy per GOVERNANCE.md 'Hub-Hosted Tooling'. The match is on path alone, so a repo's own content at a path the hub also uses matches without carrying anything of the hub's, and a spec/divergences.json 'gaps' disposition decides which case a hit is: 'retire' asserts a deletion, 'accepted' closes a path collision or a repo-owned file, and an untriaged hit is read before it is acted on.", "intentRef": "GOVERNANCE.md#hub-hosted-tooling" }
{ "id": "carried.hubonly.absent", "verdict": "letter", "assert": "The repo carries no file the hub hosts rather than carries. spec/audit.py derives the set as the hub's git-tracked paths minus the spec/files.json baseline, and reports each one the repo's ground-truth branch also holds. Every hub path below a spec/files.json 'repoOwnedTrees' entry, such as tests, is left out of the set, since no repo carries it. The remedy is a deletion rather than a re-vendor, since the repo reaches the hub's copy per GOVERNANCE.md 'Hub-Hosted Tooling'. The match is on path alone, so a repo's own content at a path the hub also uses matches without carrying anything of the hub's, and a spec/divergences.json 'gaps' disposition decides which case a hit is: 'retire' asserts a deletion, 'accepted' closes a path collision or a repo-owned file, and an untriaged hit is read before it is acted on.", "intentRef": "GOVERNANCE.md#hub-hosted-tooling" }
]
},
"repo-setup": {
Expand Down
30 changes: 30 additions & 0 deletions spec/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,36 @@ def check_selector(where, applies_to):
f"files.json: tree targets {left_target} and {right_target} have overlapping ownership"
)

owned_trees = files.get("repoOwnedTrees", [])
if not isinstance(owned_trees, list):
errors.append("files.json: 'repoOwnedTrees' must be an array")
owned_trees = []
for owned in owned_trees:
if not isinstance(owned, dict):
errors.append(f"files.json: repo-owned tree {owned!r} is not an object")
continue
source = owned.get("source")
if not isinstance(source, str) or not source:
errors.append(f"files.json: repo-owned tree has an invalid source: {owned!r}")
continue
reason = owned.get("reason")
if not isinstance(reason, str) or not reason:
errors.append(f"files.json: repo-owned tree {source} reason must be a non-empty string")
if (
reduces_to_repo_root(source)
or escapes_repo_root(source)
or pathlib.PurePosixPath(source).as_posix() != source
):
errors.append(
f"files.json: repo-owned tree {source} source must be a normalized path below the repository root"
)
elif (link := symlinked_component(ROOT, source)) is not None:
errors.append(
f"files.json: repo-owned tree {source} has a symlinked component {link}, below which git tracks no file"
)
elif not (ROOT / source).is_dir():
errors.append(f"files.json: repo-owned tree {source} is missing or not a directory")

copilot_path = ROOT / ".github/copilot-instructions.md"
try:
copilot_instructions = copilot_path.read_text(encoding="utf-8")
Expand Down
44 changes: 39 additions & 5 deletions tests/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,30 @@ def __call__(self, path: str, ok404: bool = False) -> Any:


def run_audit(
tree: set[str], modes: dict[str, str] | None = None, unread: frozenset[str] = frozenset()
tree: set[str],
modes: dict[str, str] | None = None,
unread: frozenset[str] = frozenset(),
hub: frozenset[str] = frozenset(),
) -> tuple[list[tuple[str, str]], FakeGh]:
"""Audit a Python repository carrying `tree`, where a test module makes codecov.yml owed.

The hub's tracked paths come from a fetch of its origin. Answering them as empty keeps the
run offline and leaves the checkout the suite runs in untouched.
The hub's tracked paths come from a fetch of its origin. Answering them with `hub` keeps the
run offline and leaves the checkout the suite runs in untouched. The repo-owned trees are the
shipped declaration, so a test reads the exclusion the real audit applies.
"""
spec = {
"registry": {"repos": [ENTRY], "defaults": {}},
"settings": audit.load("repo-config/settings.json"),
"secrets": audit.load("spec/secrets.json"),
"files": {"baseline": [CODECOV]},
"files": {
"baseline": [CODECOV],
"repoOwnedTrees": audit.load("spec/files.json")["repoOwnedTrees"],
},
}
fake = FakeGh(tree, modes, unread)
with (
mock.patch.object(audit, "gh", fake),
mock.patch.object(audit, "hub_tracked", lambda rev=None: frozenset()),
mock.patch.object(audit, "hub_tracked", lambda rev=None: hub),
):
findings, _ = audit.audit_repo(ENTRY, spec)
return findings, fake
Expand Down Expand Up @@ -195,5 +202,32 @@ def test_a_failed_shebang_read_raises_rather_than_reading_as_undecided(self) ->
run_audit(tree, {"bin/Run_Backup": "100755"}, frozenset({"bin/Run_Backup"}))


class AuditRepoHubOnlyCase(unittest.TestCase):
"""audit_repo leaves a carrier's own test module out of the hub-only check.

The hub's tests are never carried, so a carrier's file at a hub test path is a collision.
Reporting it would ask for the deletion of that repo's own suite.
"""

HUB = frozenset({"tests/test_a.py", "tools/hub_tool.py"})

def hub_only_findings(self, findings: list[tuple[str, str]]) -> list[str]:
return [text for _, text in findings if text.startswith("hub-only:")]

def test_a_test_module_sharing_a_hub_test_name_is_not_reported(self) -> None:
findings, _ = run_audit(CARRIED | {".codecov.yml"}, hub=self.HUB)

self.assertEqual(self.hub_only_findings(findings), [])

def test_a_hub_only_path_outside_tests_is_still_reported(self) -> None:
"""The control, showing the check under test ran and reports a hub-only path it sees."""
findings, _ = run_audit(CARRIED | {".codecov.yml", "tools/hub_tool.py"}, hub=self.HUB)

hub_only = self.hub_only_findings(findings)
self.assertTrue(
any(text.startswith("hub-only: tools/hub_tool.py ") for text in hub_only), hub_only
)


if __name__ == "__main__":
unittest.main()
Loading
Loading