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: 2 additions & 2 deletions AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Which carried files and sections a repo is expected to have is decided by its sc

For each applicable type in [`spec/project-types.json`][project-types] and every cross-cutting dimension, evaluate each check at its stated verdict tier:

**Every check under a project type is judged by hand. The cross-cutting dimensions are only partly mechanized, and the line between the two halves is not where a reader assumes.** [`spec/audit.py`][audit-runner] evaluates **no** check belonging to a type in `spec/project-types.json`, and it reads that file for one purpose only, to resolve the id a registry `driftNote` names (section 8) against the catalog and against the repo's declared types. An id is resolved against the runner's own checks in `spec/checks.json` too. Resolving a catalog id is not running the check it names. What the runner does mechanize is the deterministic subset in section 0, and that subset lands on several `crossCutting` checks without being organized by them: branch protection and the ruleset diffs, secret names, Dependabot ecosystems, the cspell single source, section presence, script file names and secret-file stems, and `driftNotes` freshness. So read a clean run precisely. It is evidence for that subset, it is **no** evidence for any of a type's checks, and it is partial evidence across the cross-cutting dimensions. The three are easy to conflate, because adding a check under a type changes what an auditor must judge and changes no tool's output, so the check reports nothing until someone evaluates it, and silence from a tool that was never looking reads exactly like a pass. Cite the `file:line` each check was judged against, since that citation is the only durable record that the judgment happened.
**Every check under a project type is judged by hand. The cross-cutting dimensions are only partly mechanized, and the line between the two halves is not where a reader assumes.** [`spec/audit.py`][audit-runner] evaluates **no** check belonging to a type in `spec/project-types.json`, and it reads that file for one purpose only, to resolve the id a registry `driftNote` names (section 8) against the catalog and against the repo's declared types. An id is resolved against the runner's own checks in `spec/checks.json` too. Resolving a catalog id is not running the check it names. What the runner does mechanize is the deterministic subset in section 0. That subset lands on several `crossCutting` checks without being organized by them. It covers branch protection and the ruleset diffs, secret names, and Dependabot ecosystems. It also covers the cspell single source, section presence, file names, and `driftNotes` freshness. So read a clean run precisely. It is evidence for that subset, it is **no** evidence for any of a type's checks, and it is partial evidence across the cross-cutting dimensions. The three are easy to conflate, because adding a check under a type changes what an auditor must judge and changes no tool's output, so the check reports nothing until someone evaluates it, and silence from a tool that was never looking reads exactly like a pass. Cite the `file:line` each check was judged against, since that citation is the only durable record that the judgment happened.

- **letter** - the exact file, section, config, or construct is present.
- **intent** - an equivalent outcome holds even if the form differs.
Expand All @@ -87,7 +87,7 @@ A check with `intentRef`/`workflowRef` points at the prose section that owns the
- **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.
- **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/`), a single opaque credential file carries no extension, each secret file's stem is lowercase snake_case, and every real secret file has a tracked `<name>.example` beside it cataloged in `.secrets/README.md`. N/A for a repo with no such directory. See [GOVERNANCE.md "Repo-Scoped Secrets"][governance-repo-scoped-secrets]. `spec/audit.py` reports a stem that is not lowercase snake_case at **drift**. The stem is the name before its first dot, and the directory's own `README.md`, `.gitignore`, and `.gitkeep` are not secret files.
- **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.
- **file-naming** - each script file takes the stem its extension sets, per [CODESTYLE.md "Script and Utility File Names"][codestyle-file-names]. A `.py` stem is lowercase snake_case. A `.sh`, `.ps1`, `.cmd`, or `.bat` stem is lowercase-hyphenated, and so is the name of an extensionless executable with a shebang. The extension is lowercase too. The text after a name's last dot is its extension, matched whatever its case. A name with a dot and none of those extensions is not judged here, whatever its mode. So an executable dotfile with no such extension, such as `.Hidden_Hook`, and a dotted name such as `Run_Me.v2` go unjudged. A launcher with an extension beside a `.py` of the same stem keeps that stem. `spec/audit.py` reports a name that breaks its pattern at **drift**. It reports a file the patterns cannot decide for judgment instead. One is a launcher whose `.py` sits in another directory. Another is an extensionless launcher beside its `.py`, which may be an installed command named `fetch-report` rather than `fetch_report`. A third is an extensionless executable with no shebang or no readable text. Pester's `.Tests.ps1` suffix is the one tool-fixed name the patterns know. Check a reported break against the externally fixed exemption before renaming it. Record any other tool-fixed name as a registry `driftNote` naming `(filenaming.scripts)` and each exempt path as a code span, such as `` `tools/Install.ps1` ``. The audit then skips each named path and no other. It reports a named path it no longer flags, or a note naming no path, as a stale exemption. The carried hook helper still under its former name `hub-fetch-run.py` is a break here. The names kept for Python-type discovery are no naming exemption.
- **linter-parity** - one config per linter (`.markdownlint-cli2.jsonc`, `cspell.json`, ruff/pyright, editorconfig/csharpier, actionlint) drives the CLI, CI, and the editor extension where the workspace recommends one, and CI runs each. That count is of the root config, so a nested `.markdownlint-cli2.jsonc` is not a second one. A local hook exists and runs at minimum the diff-scoped prose gate and the eol check via `hub_fetch_run.py`, or the hub's own local script copies for the hub repo itself (`parity.hooks`, intent). The helper's former name `hub-fetch-run.py` counts too. A repo with none wired is a defect, and one mid-convergence on the language-formatting half stays operational.
- **recurring-violations** - comments concise and non-narrative, ASCII only (no em-dash, no smart quotes) apart from the letters of a recorded name, US spelling, line endings per `.editorconfig`. These are frequent regressions, so this dimension is high priority and always runs, and each check is grep-able (see below).
Expand Down
8 changes: 6 additions & 2 deletions CODESTYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,16 @@ A command-line script is one a person or an agent runs by name. A hook its host
| Shell `.sh` and PowerShell `.ps1` | lowercase-hyphenated | The Linux command convention. A cross-platform pair keeps one name, `sync-tools.sh` beside `sync-tools.ps1` |
| Windows batch `.cmd` and `.bat` | lowercase-hyphenated | The same convention as shell and PowerShell. A shim that puts a command on `PATH` takes the name of the command it provides, `sync-tools.cmd` launching `sync-tools.ps1` |
| PowerShell functions inside a `.ps1` | Verb-Noun with an approved verb (`Get-Verb`) | PSScriptAnalyzer `PSUseApprovedVerbs`. This is a command rule, not a file rule |
| Env and secret files | lowercase snake_case stem | `mail_relay.env`, `db_root_password` |
| Env and secret files, tracked or not | lowercase fields joined by `_`, with no dot but the extension | `mail_relay.env`, `db_root_password`, `app_web-01_prod.env` |
| Env variable names and CI secret names | UPPER_SNAKE_CASE | The POSIX and GitHub convention |

- **Installed commands.** An executable or launcher on `PATH` or in a system slot (systemd, cron, run-parts, motd) is named as a command, whatever its language. That name is lowercase-hyphenated. It carries no extension unless the platform needs one to run it, such as `.cmd` or `.ps1` on Windows.
- **Wrapper exemption.** A thin launcher whose only job is to run a Python tool keeps that tool's stem, so `fetch_report.py`, `fetch_report.sh`, and `fetch_report.ps1` stay grouped together. It covers only a launcher run by its path. A launcher installed onto `PATH` or into a system slot is an installed command instead, such as `fetch-report` rather than `fetch_report`.
- **Externally fixed names are exempt.** That covers Git hook names, `__init__.py`, `conftest.py`, and Pester's `.Tests.ps1` suffix, whose stem mirrors the script under test. It covers a PowerShell data file (`.psd1`) whose name a tool fixes, such as a module manifest named for its module. `PSScriptAnalyzerSettings.psd1` is another, since PSScriptAnalyzer finds it by that name. It also covers any name a platform defines.
- **Env and secret file names reach every file a repository names.** That covers a tracked file or template and a secret file mounted into a container. It also covers a host-side file the repository's docs or scripts name, such as an `ENV_FILE` path or a compose `env_file`.
- **`_` joins the fields of an env or secret file name, and each field keeps its own valid form.** A host name field stays verbatim, hyphens included, so `app_web-01_prod.env` is right and `app_web_01_prod.env` and `app.web-01.prod.env` are not. That field is a short name, the host's first DNS label (`web-01`) or a logical role name (`vps-staging`), and never a fully qualified one. Either way it takes the DNS label form, so its own words join with `-`. In every other field a hyphen never joins words, since that is the job of `_`. A host name label can carry `-` and never `_`, so every `_` is a separator and the name splits back into its fields. The name is lowercase, carries no dot but its extension, and starts and ends with a letter or digit. A tracked template's `.example` suffix is not counted as a dot.
- **A variable name converts separately.** An env variable or CI secret name allows only `A-Z`, `0-9`, and `_`. Where a field becomes one, the hyphen becomes `_` there, as in `APP_WEB_01_PROD_TOKEN`, and never in the file name.
- **Some consumers set limits of their own.** A name used as a Docker secret stays within the 64 characters Docker allows. A name that maps to an env variable or CI secret does not start with a digit, which POSIX and GitHub both forbid.
- **Externally fixed names are exempt.** That covers Git hook names, `__init__.py`, `conftest.py`, and Pester's `.Tests.ps1` suffix, whose stem mirrors the script under test. It covers a PowerShell data file (`.psd1`) whose name a tool fixes, such as a module manifest named for its module. `PSScriptAnalyzerSettings.psd1` is another, since PSScriptAnalyzer finds it by that name. So is an env file a tool reads by a fixed name, such as `.env`, `.env.local`, or `.env.production`. An env or secret file in a `.secrets/` directory is never one, since it is read by a path the repository configures. The exemption also covers any name a platform defines.
- **Directories are out of scope.**

Departures, stated on purpose so a reader does not "fix" them back:
Expand Down
2 changes: 1 addition & 1 deletion GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Line-ending governance for an operational repo is in [Line Endings](#line-ending
A repo whose own stacks or scripts read local runtime credentials from disk, most commonly an operational repo's Docker Compose stack, keeps them in a dotted `.secrets/` directory at the repo root. This is the repo-scoped counterpart to the host-scoped `~/.secrets/` convention a repo's own `OPERATIONS.md` may document, and it is a different thing from `spec/secrets.json`, the CI/GitHub Actions secret-name registry `spec/audit.py` cross-checks. `spec/secrets.json` governs what a workflow reads from GitHub Actions. This convention governs what a repo's own process reads from its own checkout.

- **The directory is named `.secrets/`, dotted, never a bare `secrets/`.**
- **A secret file's stem is lowercase snake_case** (`homeassistant_db_password`, `mail_relay.env`), per [`CODESTYLE.md` "Script and Utility File Names"](./CODESTYLE.md#script-and-utility-file-names).
- **A secret file's name is lowercase fields joined by `_`** (`homeassistant_db_password`, `mail_relay.env`), per [`CODESTYLE.md` "Script and Utility File Names"](./CODESTYLE.md#script-and-utility-file-names).
- **A single opaque credential file carries no extension** (`homeassistant_db_password`, not `homeassistant_db_password.txt`), the same reason `README` and `LICENSE` carry none. It is a security property, read but never sourced, not a formatting preference.
- **A structured credential keeps its format's extension** (`.json` for structured config).
- **The shared env file is named for what it configures**, not a bare `.env` (`docker.env` for a repo whose stacks are Docker Compose), so a second env-shaped file added later stays unambiguous.
Expand Down
26 changes: 18 additions & 8 deletions spec/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,9 @@ def python_directory_coverage_findings(entry, tree):
HYPHENATED_FILE_STEM = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
HYPHENATED_SCRIPT_EXTENSIONS = (".sh", ".ps1", ".cmd", ".bat")
PESTER_SUFFIX = ".Tests.ps1"
SECRET_FILE_STEM = re.compile(r"[a-z0-9]+(?:_[a-z0-9]+)*")
ENV_SECRET_FIELD = r"[a-z0-9]+(?:-+[a-z0-9]+)*"
ENV_SECRET_NAME = re.compile(rf"{ENV_SECRET_FIELD}(?:_{ENV_SECRET_FIELD})*(?:\.[a-z0-9]+)?")
ENV_SECRET_TEMPLATE = ".example"
SECRETS_DIRECTORY_EXEMPT = {"README.md", ".gitignore", ".gitkeep"}
PATHS_SHOWN = 5

Expand Down Expand Up @@ -482,8 +484,9 @@ def file_naming_findings(entry, blobs, read_text):


def secret_stem_findings(tree):
"""DRIFT naming each secret file in a repo-scoped `.secrets/` directory whose stem is not lowercase snake_case.
"""DRIFT naming each tracked env or secret file whose name breaks CODESTYLE.md's env and secret file rule.

Every file directly in a `.secrets/` directory is one, and so is a `.env` file elsewhere whose name opens on no dot.
`tree` is the blob path set, or None when unread, which reports nothing.
"""
if tree is None:
Expand All @@ -492,18 +495,25 @@ def secret_stem_findings(tree):
for path in sorted(tree):
segments = path.split("/")
name = segments[-1]
if segments[-2:-1] != [".secrets"] or name in SECRETS_DIRECTORY_EXEMPT:
in_secrets = segments[-2:-1] == [".secrets"]
if in_secrets and name in SECRETS_DIRECTORY_EXEMPT:
continue
if not SECRET_FILE_STEM.fullmatch(name.split(".", 1)[0]):
named = name.removesuffix(ENV_SECRET_TEMPLATE)
folded = name.lower().removesuffix(ENV_SECRET_TEMPLATE)
if not in_secrets and (folded.startswith(".") or not folded.endswith(".env")):
continue
if not ENV_SECRET_NAME.fullmatch(named):
broken.append(path)
if not broken:
return []
return [
(
"DRIFT",
(
f"runtime-secrets: {len(broken)} secret file(s) carry a stem that is not lowercase "
f"snake_case: {capped_paths(broken)}"
f"runtime-secrets: {len(broken)} env or secret file(s) break the env and secret file "
f'rule of CODESTYLE.md "Script and Utility File Names", or are a bare .env in a '
f'.secrets directory, which GOVERNANCE.md "Repo-Scoped Secrets" rules out: '
f"{capped_paths(broken)}"
),
)
]
Expand Down Expand Up @@ -2057,7 +2067,7 @@ def check_tree_readable(run):
return [
(
"DRIFT",
f"tree: could not read the file tree for {run.slug}@{run.ground} in full, so verbatim trees, script file names, and secret-file stems are undecided",
f"tree: could not read the file tree for {run.slug}@{run.ground} in full, so verbatim trees, script file names, and env and secret file names are undecided",
)
]

Expand Down Expand Up @@ -2278,7 +2288,7 @@ def _selftest_cases():
("the title and intro", title_and_intro(doc)[0], "T"),
("an undeclared heading", undeclared_h2_headings(doc, ["a"]), ["c"]),
("a version literal", version_literals_outside_verbatim("uses 1.2.3\n", []), ["1.2.3"]),
("a secret-file stem", len(secret_stem_findings({".secrets/Bad.txt"})), 1),
("an env or secret file name", len(secret_stem_findings({".secrets/Bad.txt"})), 1),
("the selftest flag", parse_args(["--selftest"]).selftest, True),
]
ok = True
Expand Down
Loading
Loading