From ead792a8f4caa50d3f3bcd467f462e2cc3ff280c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 26 Oct 2026 09:14:59 -0700 Subject: [PATCH 1/7] Apply One Naming Convention to Every Env and Secret File CODESTYLE.md now states the env and secret file rule the maintainer chose in #3503. It reaches every file a repository names, tracked or host-side. Fields are joined by `_` and each keeps its own valid form, so a host name keeps its hyphens. The name carries no dot but its extension. The rule also sets the consumer limits and how a field converts to a variable name. The `secrets.stems` audit check now reads that grammar instead of a snake_case stem. It also reads tracked `.env` files outside `.secrets/` whose name no tool fixes. AUDIT.md and the spec catalog state what the check reads and what stays judged by hand: a hyphen joining words, the consumer limits, and host-side names. Refs #3503 Co-Authored-By: Claude Opus 5.5 --- AUDIT.md | 2 +- CODESTYLE.md | 8 ++++++-- GOVERNANCE.md | 2 +- spec/audit.py | 19 +++++++++++++------ spec/checks.json | 2 +- spec/project-types.json | 2 +- tests/test_audit.py | 6 +++--- tests/test_audit_helpers.py | 37 ++++++++++++++++++++++++++++--------- 8 files changed, 54 insertions(+), 24 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 5afb6a580..cb8b05db1 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -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 `.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/`), a single opaque credential file carries no extension, and every real secret file has a tracked `.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. Three parts are judged by hand. A hyphen is right only inside a field value that carries its own, such as a host name, and the check passes a hyphenated word because it cannot tell the two apart. 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, such as an `ENV_FILE` path or a compose `env_file`, never by reading a host. 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). diff --git a/CODESTYLE.md b/CODESTYLE.md index 69342a783..0827c23ed 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -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, a host-side file the repository's docs or scripts name (an `ENV_FILE` path, a compose `env_file`), and a secret file mounted into a container. +- **`_` 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. A hyphen never joins words, since that is the job of `_`. A DNS host name 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.** Where a field becomes an env variable or CI secret name, which allow only `A-Z`, `0-9`, and `_`, 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`. It also covers any name a platform defines. - **Directories are out of scope.** Departures, stated on purpose so a reader does not "fix" them back: diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 4245f3a3a..923ef5f04 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -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. diff --git a/spec/audit.py b/spec/audit.py index 9a857412f..edc32aa31 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -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 @@ -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 no tool fixes. `tree` is the blob path set, or None when unread, which reports nothing. """ if tree is None: @@ -492,9 +495,13 @@ 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) + if not in_secrets and (named.startswith(".") or not named.lower().endswith(".env")): + continue + if not ENV_SECRET_NAME.fullmatch(named): broken.append(path) if not broken: return [] @@ -502,8 +509,8 @@ def secret_stem_findings(tree): ( "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": {capped_paths(broken)}' ), ) ] diff --git a/spec/checks.json b/spec/checks.json index b2f57329b..06336d445 100644 --- a/spec/checks.json +++ b/spec/checks.json @@ -9,7 +9,7 @@ { "id": "python.directories.coverage", "family": "python", "description": "Every tracked .py file sits inside a directory the registry declares.", "spec": "registry/repos.json", "hub": true, "scope": "repo" }, { "id": "python.directories.root", "family": "python", "description": "A root tests/ suite has the manifest the undeclared root default needs to run it.", "spec": "registry/repos.json", "hub": true, "scope": "repo" }, { "id": "filenaming.scripts", "family": "filenaming", "description": "Each script file name takes the stem its extension sets.", "spec": "CODESTYLE.md", "hub": true, "scope": "repo", "suppression": { "endsWithType": null } }, - { "id": "secrets.stems", "family": "secrets", "description": "Each runtime secret file's stem is lowercase snake_case.", "spec": "GOVERNANCE.md", "hub": true, "scope": "repo" }, + { "id": "secrets.stems", "family": "secrets", "description": "Each tracked env and secret file name takes the form the env and secret file rule sets.", "spec": "GOVERNANCE.md", "hub": true, "scope": "repo" }, { "id": "secrets.names", "family": "secrets", "description": "The secret names each store holds against those the applicable mechanisms require and forbid.", "spec": "spec/secrets.json", "hub": true, "scope": "repo" }, { "id": "dependabot.ecosystems", "family": "dependabot", "description": "dependabot.yml declares each ecosystem the tree implies.", "spec": "AUDIT.md", "hub": true, "scope": "repo" }, { "id": "files.presence", "family": "files", "description": "Each baseline file, or a declared alternative, is present, with its intent staleness advisory.", "spec": "spec/files.json", "hub": true, "scope": "baseline" }, diff --git a/spec/project-types.json b/spec/project-types.json index f155cfe41..5ff7779ec 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -167,7 +167,7 @@ "runtime-secrets": { "appliesTo": "*", "checks": [ - { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, and each secret file's stem, the name before its first dot, is lowercase snake_case, the directory's own README.md, .gitignore, and .gitkeep not being secret files. A bare secrets/ directory, a .txt-suffixed single-value secret file, or a hyphenated or capitalized stem is drift. N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, + { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, the directory's own README.md, .gitignore, and .gitkeep not being secret files. Every env and secret file the repo names, tracked or host-side, follows CODESTYLE.md's env and secret file rule: lowercase fields joined by _, each field keeping its own valid form so a host name keeps its hyphens, no dot but the extension once a tracked template's .example suffix is set aside, and a letter or digit at each end. A bare secrets/ directory, a .txt-suffixed single-value secret file, a capitalized or dotted name, or a hyphen joining words is drift. The directory parts are N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, { "id": "runtimesecrets.catalog", "verdict": "letter", "assert": "Every real secret file in a repo-scoped .secrets/ directory has a tracked .example beside it, .gitignore un-ignores only the .example files plus README.md, and that README.md catalogs each file: what it holds and what consumes it. N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" } ] }, diff --git a/tests/test_audit.py b/tests/test_audit.py index b9b8ed939..a5524906c 100755 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -160,7 +160,7 @@ def test_each_non_conforming_shape_is_reported(self) -> None: ".codecov.yml", "scripts/Fetch-Report.py", "bin/Run_Backup", - ".secrets/db-root-password.example", + ".secrets/Db_Root_Password.example", } findings, fake = run_audit(tree, {"bin/Run_Backup": "100755"}) @@ -179,8 +179,8 @@ def test_each_non_conforming_shape_is_reported(self) -> None: "(an executable with no shebang)" ), ( - "runtime-secrets: 1 secret file(s) carry a stem that is not lowercase " - "snake_case: .secrets/db-root-password.example" + "runtime-secrets: 1 env or secret file(s) break the env and secret file rule " + 'of CODESTYLE.md "Script and Utility File Names": .secrets/Db_Root_Password.example' ), ], ) diff --git a/tests/test_audit_helpers.py b/tests/test_audit_helpers.py index a825bd4bb..9f6f444a6 100755 --- a/tests/test_audit_helpers.py +++ b/tests/test_audit_helpers.py @@ -3318,43 +3318,62 @@ def test_unreadable_tree_reports_nothing(self) -> None: class SecretStemCase(unittest.TestCase): - """Holds the runtime secret file stem findings.""" + """Holds the env and secret file name findings.""" def test_secret_stem_cases(self) -> None: secret_stem_cases = [ ( - "conforming stems, examples, and the directory's own files pass", + "conforming names, examples, and the directory's own files pass", { ".secrets/README.md", ".secrets/.gitignore", ".secrets/db_root_password.example", ".secrets/mail_relay.env.example", + ".secrets/app_web-01_prod.env.example", "deploy/.secrets/service_account.json.example", + "deploy/app_web-01_prod.env.example", "docs/Mixed-Case.md", }, set(), ), ( - "a hyphenated, capitalized, or bare stem breaks", + "a capitalized, dotted, or bare name in a .secrets directory breaks", { - ".secrets/db-root-password.example", ".secrets/Mail_Relay.env.example", ".secrets/.env.example", - "deploy/.secrets/api-token", + ".secrets/app.web-01.prod.env", }, { - ".secrets/db-root-password.example", ".secrets/Mail_Relay.env.example", ".secrets/.env.example", - "deploy/.secrets/api-token", + ".secrets/app.web-01.prod.env", }, ), ( - "a file outside a .secrets directory, or nested below one, is out of scope", + "a badly separated name, or a capitalized .env file elsewhere, breaks", + { + ".secrets/app_-web.env", + "deploy/.secrets/api_token_", + "deploy/App_Prod.env", + "deploy/app_prod.ENV.example", + }, + { + ".secrets/app_-web.env", + "deploy/.secrets/api_token_", + "deploy/App_Prod.env", + "deploy/app_prod.ENV.example", + }, + ), + ( + "a file nested below a .secrets directory, a tool-fixed env name, or another file is out of scope", { "secrets/db-root-password.example", "config/.secrets-notes/Odd-Name", ".secrets/docs/Mixed-Case.md", + ".env", + "web/.env.example", + "web/.env.local", + "deploy/Notes.environment", }, set(), ), @@ -3365,7 +3384,7 @@ def test_secret_stem_cases(self) -> None: good = (not want and got == []) or ( len(got) == 1 and got[0][0] == "DRIFT" - and got[0][1].startswith(f"runtime-secrets: {len(want)} secret file(s)") + and got[0][1].startswith(f"runtime-secrets: {len(want)} env or secret file(s)") and all(p in got[0][1] for p in want) ) self.assertTrue(good, f"{got}") From 9dd721e1d4625d5b9a186c707b2f0d67b9aa7b43 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 26 Oct 2026 09:24:34 -0700 Subject: [PATCH 2/7] Answer the Local Review of the Env and Secret Name Check A field may now carry a run of hyphens, as an IDN host label does. The scope test folds case before setting a `.example` suffix aside, so an uppercase suffix is judged rather than skipped. The finding names the GOVERNANCE.md rule a bare `.env` breaks, and the catalog entry names CODESTYLE.md as the rule's home. Long sentences are split, and the judged-by-hand list drops its count. Refs #3503 Co-Authored-By: Claude Opus 5.5 --- AUDIT.md | 2 +- CODESTYLE.md | 4 ++-- spec/audit.py | 11 +++++++---- spec/checks.json | 2 +- tests/test_audit.py | 4 +++- tests/test_audit_helpers.py | 3 +++ 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index cb8b05db1..160b9a20c 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -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, and every real secret file has a tracked `.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. Three parts are judged by hand. A hyphen is right only inside a field value that carries its own, such as a host name, and the check passes a hyphenated word because it cannot tell the two apart. 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, such as an `ENV_FILE` path or a compose `env_file`, never by reading a host. Renaming a file on a host is the maintainer's step, listed in the resync pull request. +- **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 `.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 is right only inside a field value that carries its own, such as a host name. The check passes a hyphenated word, since it cannot tell the two apart. 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). diff --git a/CODESTYLE.md b/CODESTYLE.md index 0827c23ed..b2884a9c8 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -59,9 +59,9 @@ A command-line script is one a person or an agent runs by name. A hook its host - **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`. -- **Env and secret file names reach every file a repository names.** That covers a tracked file or template, a host-side file the repository's docs or scripts name (an `ENV_FILE` path, a compose `env_file`), and a secret file mounted into a container. +- **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. A hyphen never joins words, since that is the job of `_`. A DNS host name 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.** Where a field becomes an env variable or CI secret name, which allow only `A-Z`, `0-9`, and `_`, the hyphen becomes `_` there, as in `APP_WEB_01_PROD_TOKEN`, and never in the file name. +- **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`. It also covers any name a platform defines. - **Directories are out of scope.** diff --git a/spec/audit.py b/spec/audit.py index edc32aa31..d92ef070e 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -352,7 +352,7 @@ 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" -ENV_SECRET_FIELD = 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"} @@ -486,7 +486,7 @@ def file_naming_findings(entry, blobs, read_text): def secret_stem_findings(tree): """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 no tool fixes. + 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: @@ -499,7 +499,8 @@ def secret_stem_findings(tree): if in_secrets and name in SECRETS_DIRECTORY_EXEMPT: continue named = name.removesuffix(ENV_SECRET_TEMPLATE) - if not in_secrets and (named.startswith(".") or not named.lower().endswith(".env")): + 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) @@ -510,7 +511,9 @@ def secret_stem_findings(tree): "DRIFT", ( 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": {capped_paths(broken)}' + 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)}" ), ) ] diff --git a/spec/checks.json b/spec/checks.json index 06336d445..b9444a64b 100644 --- a/spec/checks.json +++ b/spec/checks.json @@ -9,7 +9,7 @@ { "id": "python.directories.coverage", "family": "python", "description": "Every tracked .py file sits inside a directory the registry declares.", "spec": "registry/repos.json", "hub": true, "scope": "repo" }, { "id": "python.directories.root", "family": "python", "description": "A root tests/ suite has the manifest the undeclared root default needs to run it.", "spec": "registry/repos.json", "hub": true, "scope": "repo" }, { "id": "filenaming.scripts", "family": "filenaming", "description": "Each script file name takes the stem its extension sets.", "spec": "CODESTYLE.md", "hub": true, "scope": "repo", "suppression": { "endsWithType": null } }, - { "id": "secrets.stems", "family": "secrets", "description": "Each tracked env and secret file name takes the form the env and secret file rule sets.", "spec": "GOVERNANCE.md", "hub": true, "scope": "repo" }, + { "id": "secrets.stems", "family": "secrets", "description": "Each tracked env and secret file name takes the form the env and secret file rule sets.", "spec": "CODESTYLE.md", "hub": true, "scope": "repo" }, { "id": "secrets.names", "family": "secrets", "description": "The secret names each store holds against those the applicable mechanisms require and forbid.", "spec": "spec/secrets.json", "hub": true, "scope": "repo" }, { "id": "dependabot.ecosystems", "family": "dependabot", "description": "dependabot.yml declares each ecosystem the tree implies.", "spec": "AUDIT.md", "hub": true, "scope": "repo" }, { "id": "files.presence", "family": "files", "description": "Each baseline file, or a declared alternative, is present, with its intent staleness advisory.", "spec": "spec/files.json", "hub": true, "scope": "baseline" }, diff --git a/tests/test_audit.py b/tests/test_audit.py index a5524906c..5207b47c3 100755 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -180,7 +180,9 @@ def test_each_non_conforming_shape_is_reported(self) -> None: ), ( "runtime-secrets: 1 env or secret file(s) break the env and secret file rule " - 'of CODESTYLE.md "Script and Utility File Names": .secrets/Db_Root_Password.example' + 'of CODESTYLE.md "Script and Utility File Names", or are a bare .env in a ' + '.secrets directory, which GOVERNANCE.md "Repo-Scoped Secrets" rules out: ' + ".secrets/Db_Root_Password.example" ), ], ) diff --git a/tests/test_audit_helpers.py b/tests/test_audit_helpers.py index 9f6f444a6..07a391bb0 100755 --- a/tests/test_audit_helpers.py +++ b/tests/test_audit_helpers.py @@ -3330,6 +3330,7 @@ def test_secret_stem_cases(self) -> None: ".secrets/db_root_password.example", ".secrets/mail_relay.env.example", ".secrets/app_web-01_prod.env.example", + ".secrets/app_xn--bcher-kva_prod.env", "deploy/.secrets/service_account.json.example", "deploy/app_web-01_prod.env.example", "docs/Mixed-Case.md", @@ -3356,12 +3357,14 @@ def test_secret_stem_cases(self) -> None: "deploy/.secrets/api_token_", "deploy/App_Prod.env", "deploy/app_prod.ENV.example", + "deploy/app_prod.env.EXAMPLE", }, { ".secrets/app_-web.env", "deploy/.secrets/api_token_", "deploy/App_Prod.env", "deploy/app_prod.ENV.example", + "deploy/app_prod.env.EXAMPLE", }, ), ( From 22182451ca656e5866782b49205ba7c4d9c58ac9 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 26 Oct 2026 09:31:58 -0700 Subject: [PATCH 3/7] Name the Env and Secret Check by What It Now Reads AUDIT.md and the unreadable-tree message described the check as reading secret-file stems, which it no longer does. The catalog assert's list gains its missing conjunction. Refs #3503 Co-Authored-By: Claude Opus 5.5 --- AUDIT.md | 2 +- spec/audit.py | 4 ++-- spec/project-types.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 160b9a20c..5995e2275 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -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, 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 env and secret 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. diff --git a/spec/audit.py b/spec/audit.py index d92ef070e..5e3d85d0f 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -2067,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", ) ] @@ -2288,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 diff --git a/spec/project-types.json b/spec/project-types.json index 5ff7779ec..838e0afd7 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -167,7 +167,7 @@ "runtime-secrets": { "appliesTo": "*", "checks": [ - { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, the directory's own README.md, .gitignore, and .gitkeep not being secret files. Every env and secret file the repo names, tracked or host-side, follows CODESTYLE.md's env and secret file rule: lowercase fields joined by _, each field keeping its own valid form so a host name keeps its hyphens, no dot but the extension once a tracked template's .example suffix is set aside, and a letter or digit at each end. A bare secrets/ directory, a .txt-suffixed single-value secret file, a capitalized or dotted name, or a hyphen joining words is drift. The directory parts are N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, + { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, and the directory's own README.md, .gitignore, and .gitkeep are not secret files. Every env and secret file the repo names, tracked or host-side, follows CODESTYLE.md's env and secret file rule: lowercase fields joined by _, each field keeping its own valid form so a host name keeps its hyphens, no dot but the extension once a tracked template's .example suffix is set aside, and a letter or digit at each end. A bare secrets/ directory, a .txt-suffixed single-value secret file, a capitalized or dotted name, or a hyphen joining words is drift. The directory parts are N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, { "id": "runtimesecrets.catalog", "verdict": "letter", "assert": "Every real secret file in a repo-scoped .secrets/ directory has a tracked .example beside it, .gitignore un-ignores only the .example files plus README.md, and that README.md catalogs each file: what it holds and what consumes it. N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" } ] }, From 43c6d8ae85e923fb1b3f1338550a00c083f19eda Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 26 Oct 2026 09:50:13 -0700 Subject: [PATCH 4/7] Name a Host Field by Its Short Name, Never Its FQDN The maintainer answered the open local-review finding on a fully qualified host name field. A host field is the host's first DNS label or a logical role name, never the fully qualified name, which the check already rejects for its dots. The AUDIT.md list sentence this change lengthened is split. Refs #3503 Co-Authored-By: Claude Opus 5.5 --- AUDIT.md | 4 ++-- CODESTYLE.md | 2 +- spec/project-types.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 5995e2275..008b864db 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -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 env and secret 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. +**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: branch protection and the ruleset diffs, secret names, Dependabot ecosystems, the cspell single source, section presence, script file names and env and secret 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. @@ -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/`), and a single opaque credential file carries no extension. Every real secret file has a tracked `.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 is right only inside a field value that carries its own, such as a host name. The check passes a hyphenated word, since it cannot tell the two apart. 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. +- **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 `.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 is right only inside a field value that carries its own, such as a host name. A host field is a short name, never a fully qualified one, and the check rejects its dots. The check passes a hyphenated word, since it cannot tell the two apart. 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). diff --git a/CODESTYLE.md b/CODESTYLE.md index b2884a9c8..39feb0fdd 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -60,7 +60,7 @@ A command-line script is one a person or an agent runs by name. A hook its host - **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`. - **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. A hyphen never joins words, since that is the job of `_`. A DNS host name 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. +- **`_` 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. A hyphen never joins words, since that is the job of `_`. A DNS host name 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`. It also covers any name a platform defines. diff --git a/spec/project-types.json b/spec/project-types.json index 838e0afd7..294b5f203 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -167,7 +167,7 @@ "runtime-secrets": { "appliesTo": "*", "checks": [ - { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, and the directory's own README.md, .gitignore, and .gitkeep are not secret files. Every env and secret file the repo names, tracked or host-side, follows CODESTYLE.md's env and secret file rule: lowercase fields joined by _, each field keeping its own valid form so a host name keeps its hyphens, no dot but the extension once a tracked template's .example suffix is set aside, and a letter or digit at each end. A bare secrets/ directory, a .txt-suffixed single-value secret file, a capitalized or dotted name, or a hyphen joining words is drift. The directory parts are N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, + { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, and the directory's own README.md, .gitignore, and .gitkeep are not secret files. Every env and secret file the repo names, tracked or host-side, follows CODESTYLE.md's env and secret file rule: lowercase fields joined by _, each field keeping its own valid form so a host name keeps its hyphens and is a short name rather than a fully qualified one, no dot but the extension once a tracked template's .example suffix is set aside, and a letter or digit at each end. A bare secrets/ directory, a .txt-suffixed single-value secret file, a capitalized or dotted name, or a hyphen joining words is drift. The directory parts are N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, { "id": "runtimesecrets.catalog", "verdict": "letter", "assert": "Every real secret file in a repo-scoped .secrets/ directory has a tracked .example beside it, .gitignore un-ignores only the .example files plus README.md, and that README.md catalogs each file: what it holds and what consumes it. N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" } ] }, From d30f20a7c53c11590ba6ace59228e2a7e45e10b8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 26 Oct 2026 09:53:09 -0700 Subject: [PATCH 5/7] Give a Host Field the DNS Label Form, Role Names Included A role name such as `vps-staging` joins its words with `-`, which the rule otherwise reserves to `_`. The host field now takes the DNS label form whichever kind of name it holds, and the hyphen rule scopes to the other fields. AUDIT.md says where the check misses a host's dots, and the list sentence it lengthened is split under the limit. Refs #3503 Co-Authored-By: Claude Opus 5.5 --- AUDIT.md | 4 ++-- CODESTYLE.md | 2 +- spec/project-types.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 008b864db..21cc6bff0 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -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. 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 env and secret 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. +**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. @@ -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/`), and a single opaque credential file carries no extension. Every real secret file has a tracked `.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 is right only inside a field value that carries its own, such as a host name. A host field is a short name, never a fully qualified one, and the check rejects its dots. The check passes a hyphenated word, since it cannot tell the two apart. 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. +- **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 `.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, or inside a value that carries its own. 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 misses the dots of a two-label host that ends an extensionless name, since they read as an extension. 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). diff --git a/CODESTYLE.md b/CODESTYLE.md index 39feb0fdd..3d720acd7 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -60,7 +60,7 @@ A command-line script is one a person or an agent runs by name. A hook its host - **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`. - **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. A hyphen never joins words, since that is the job of `_`. A DNS host name 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. +- **`_` 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 DNS 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`. It also covers any name a platform defines. diff --git a/spec/project-types.json b/spec/project-types.json index 294b5f203..0b764a4bb 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -167,7 +167,7 @@ "runtime-secrets": { "appliesTo": "*", "checks": [ - { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, and the directory's own README.md, .gitignore, and .gitkeep are not secret files. Every env and secret file the repo names, tracked or host-side, follows CODESTYLE.md's env and secret file rule: lowercase fields joined by _, each field keeping its own valid form so a host name keeps its hyphens and is a short name rather than a fully qualified one, no dot but the extension once a tracked template's .example suffix is set aside, and a letter or digit at each end. A bare secrets/ directory, a .txt-suffixed single-value secret file, a capitalized or dotted name, or a hyphen joining words is drift. The directory parts are N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, + { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, and the directory's own README.md, .gitignore, and .gitkeep are not secret files. Every env and secret file the repo names, tracked or host-side, follows CODESTYLE.md's env and secret file rule: lowercase fields joined by _, each field keeping its own valid form, so a host field is a short name in DNS label form whose words join with -, never a fully qualified name, no dot but the extension once a tracked template's .example suffix is set aside, and a letter or digit at each end. A bare secrets/ directory, a .txt-suffixed single-value secret file, a capitalized or dotted name, or a hyphen joining words outside a host field is drift. The directory parts are N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, { "id": "runtimesecrets.catalog", "verdict": "letter", "assert": "Every real secret file in a repo-scoped .secrets/ directory has a tracked .example beside it, .gitignore un-ignores only the .example files plus README.md, and that README.md catalogs each file: what it holds and what consumes it. N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" } ] }, From c2e13b1fe8f3326cd07d42a323f41d078eae5131 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 26 Oct 2026 09:56:10 -0700 Subject: [PATCH 6/7] Scope the Hyphen to a Host Field and Name the Check's Dot Gap AUDIT.md's hyphen sentence now matches CODESTYLE.md, which allows a hyphen to join words only inside a host field. The check's gap is stated by its cause: it cannot tell an extension from a dot-joined last field of an extensionless name. The separator reasoning names a host name label, since a DNS label in general can carry `_`. Refs #3503 Co-Authored-By: Claude Opus 5.5 --- AUDIT.md | 2 +- CODESTYLE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 21cc6bff0..953a6290e 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -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/`), and a single opaque credential file carries no extension. Every real secret file has a tracked `.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, or inside a value that carries its own. 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 misses the dots of a two-label host that ends an extensionless name, since they read as an extension. 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. +- **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 `.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). diff --git a/CODESTYLE.md b/CODESTYLE.md index 3d720acd7..687ae5c7a 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -60,7 +60,7 @@ A command-line script is one a person or an agent runs by name. A hook its host - **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`. - **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 DNS 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. +- **`_` 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`. It also covers any name a platform defines. From 5753c6daeb7c886acf1407a85665af1ff2edd2b1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 26 Oct 2026 09:59:45 -0700 Subject: [PATCH 7/7] Name the Tool-Fixed Env Exemption in the Runtime Secrets Assert The assert's drift list read as covering a root `.env.local`, which CODESTYLE.md exempts and the check skips. Refs #3503 Co-Authored-By: Claude Opus 5.5 --- spec/project-types.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/project-types.json b/spec/project-types.json index 0b764a4bb..a593b3008 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -167,7 +167,7 @@ "runtime-secrets": { "appliesTo": "*", "checks": [ - { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, and the directory's own README.md, .gitignore, and .gitkeep are not secret files. Every env and secret file the repo names, tracked or host-side, follows CODESTYLE.md's env and secret file rule: lowercase fields joined by _, each field keeping its own valid form, so a host field is a short name in DNS label form whose words join with -, never a fully qualified name, no dot but the extension once a tracked template's .example suffix is set aside, and a letter or digit at each end. A bare secrets/ directory, a .txt-suffixed single-value secret file, a capitalized or dotted name, or a hyphen joining words outside a host field is drift. The directory parts are N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, + { "id": "runtimesecrets.naming", "verdict": "letter", "assert": "A repo-scoped runtime-secrets directory is named .secrets/ (dotted, not a bare secrets/), a single opaque credential file inside it carries no extension, a structured credential keeps its own format's extension, the shared env file is named for what it configures rather than a bare .env, and the directory's own README.md, .gitignore, and .gitkeep are not secret files. Every env and secret file the repo names, tracked or host-side, follows CODESTYLE.md's env and secret file rule: lowercase fields joined by _, each field keeping its own valid form, so a host field is a short name in DNS label form whose words join with -, never a fully qualified name, no dot but the extension once a tracked template's .example suffix is set aside, and a letter or digit at each end. A bare secrets/ directory, a .txt-suffixed single-value secret file, a capitalized or dotted name, or a hyphen joining words outside a host field is drift. A tool-fixed env name CODESTYLE.md exempts, such as .env or .env.local outside .secrets/, is not judged. The directory parts are N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" }, { "id": "runtimesecrets.catalog", "verdict": "letter", "assert": "Every real secret file in a repo-scoped .secrets/ directory has a tracked .example beside it, .gitignore un-ignores only the .example files plus README.md, and that README.md catalogs each file: what it holds and what consumes it. N/A for a repo carrying no repo-scoped secrets directory.", "intentRef": "GOVERNANCE.md#repo-scoped-secrets" } ] },