{ "$schema": "./project-types.schema.json", "note": "Type -> requirements and checks. Each check names a verdict tier (letter = exact form, intent = equivalent outcome) and points at the prose section that owns the rationale. The audit (AUDIT.md) runs the checks for a repo's declared types plus the cross-cutting dimensions. An absent construct is N/A, not a defect. How types and profiles are declared, validated against detection, and gated by profile is defined in spec/type-model.md.", "types": { "csharp": { "detect": ["*.csproj", "*.slnx", "*.sln"], "profiles": ["build"], "intentRefs": ["CODESTYLE.md", "GOVERNANCE.md#line-endings"], "requiredFiles": [".editorconfig", ".gitattributes"], "checks": [ { "id": "csharp.editorconfig.ruleblock", "verdict": "letter", "assert": ".editorconfig carries the shared [*.cs] plus ReSharper rule block.", "intentRef": "CODESTYLE.md" }, { "id": "csharp.analyzers.zerowarnings", "verdict": "intent", "assert": "Analyzer severities are enforced; warnings are not relaxed or suppressed wholesale.", "intentRef": "CODESTYLE.md" }, { "id": "csharp.centralconfig.props", "verdict": "letter", "assert": "Shared MSBuild configuration is centralized at the repo root: Directory.Build.props carries the common analyzer and warning properties (the Zero Warnings set), and Directory.Packages.props enables ManagePackageVersionsCentrally with every dependency version declared once - a csproj carries only project-specific properties and versionless PackageReference items.", "intentRef": "CODESTYLE.md" }, { "id": "csharp.coverage.codecov", "verdict": "letter", "assert": "The unit-test job collects coverage (dotnet test --collect:\"XPlat Code Coverage\" --results-directory ./coverage) and uploads it to Codecov via codecov/codecov-action, best-effort (fail_ci_if_error: false so a Codecov outage or an absent token never reds the gate). CODECOV_TOKEN is stored in the repo actions secrets and reaches the reusable validator via secrets: inherit. Required for every C# repo with tests.", "intentRef": "WORKFLOW.md", "minProfile": "build" } ] }, "nuget": { "detect": [".github/actions/build-nuget/action.yml", "dotnet nuget push", "nuget_project:"], "forbiddenSecrets": ["NUGET_API_KEY"], "checks": [ { "id": "nuget.publish.oidc", "verdict": "intent", "assert": "NuGet publish uses OIDC Trusted Publishing (NuGet/login), not a stored API key.", "workflowRef": "WORKFLOW.md#d4---release--publish" }, { "id": "nuget.publish.skipduplicate", "verdict": "letter", "assert": "The push uses --skip-duplicate and is gated on push, not on an existence check.", "workflowRef": "WORKFLOW.md#d4---release--publish" } ] }, "pypi": { "detect": [".github/actions/build-pypi/action.yml", "pypa/gh-action-pypi-publish", "pypi_project_dir:"], "forbiddenSecrets": ["PYPI_API_TOKEN", "TWINE_PASSWORD"], "checks": [ { "id": "pypi.publish.oidc", "verdict": "intent", "assert": "PyPI publish uses OIDC Trusted Publishing (no stored token).", "workflowRef": "WORKFLOW.md#d4---release--publish" }, { "id": "pypi.publish.environment", "verdict": "letter", "assert": "The publish job declares environment: pypi and id-token: write, with skip-existing: true.", "workflowRef": "WORKFLOW.md#d7---concurrency-permissions-safety" } ] }, "python": { "detect": ["pyproject.toml", "setup.py"], "profiles": ["build", "lint-only"], "canonicalPlacement": "pyproject.toml", "profileNote": "The declared profile is build or lint-only, each with a structural pyproject.toml shape (CODESTYLE.md Python 'Two profiles'). The build profile (structurally the PROJECT shape) is Python with third-party runtime dependencies or as the repo's deliverable - a PEP 621 uv project (pyproject [project]+deps+[build-system], committed uv.lock, uv sync --frozen + uv run in CI). The lint-only profile (structurally the SCRIPTS shape) is stdlib-only utility scripts embedded in a non-Python repo (e.g. a Python tooling subtree of a csharp app) - run with uvx, no uv.lock, no uv project, pyproject carries only [tool.ruff]/[tool.mypy] config. The two differ by whether the Python has third-party runtime dependencies, which the audit detects structurally from pyproject.toml (see python.profile.detect) rather than by inspecting imports. The shape [project]+deps/[build-system] + uv.lock is build, and tool-config-only with no [project]/[build-system] and no uv.lock is lint-only. The declared profile corresponds to this shape, and each check names the minimum profile it needs (see python.profile.detect and each check's minProfile).", "checks": [ { "id": "python.profile.detect", "verdict": "letter", "assert": "The declared profile corresponds to the pyproject.toml shape. A [project] table with runtime dependencies (or a [build-system]) is the build profile (the PROJECT shape). A pyproject carrying only [tool.*] config with no [project]/[build-system] and no uv.lock is the lint-only profile (the SCRIPTS shape). A lint-only subtree must not carry a uv.lock or project/build metadata, which would misrepresent it as a shippable package, and a build one must.", "intentRef": "CODESTYLE.md" }, { "id": "python.ruff.config", "verdict": "intent", "assert": "A ruff configuration is present (pyproject.toml [tool.ruff]). Both profiles.", "intentRef": "CODESTYLE.md" }, { "id": "python.pyright.config", "verdict": "intent", "assert": "Build profile: pyright is configured and runs strict on first-party code (src or the integration package) - the strong typing baseline. Third-party strictness is relaxed only where a dependency has no usable types. N/A for the lint-only profile, whose type checker is mypy over stdlib-only code (python.mypy.allowed).", "intentRef": "CODESTYLE.md", "minProfile": "build" }, { "id": "python.config.placement", "verdict": "letter", "assert": "ruff and the type-checker config live in pyproject.toml (canonical); standalone .ruff.toml / pyrightconfig.json is a drift finding. A Home Assistant integration is the exception - it follows home-assistant/core standalone-config conventions and is scored by ha.python.conventions instead.", "intentRef": "CODESTYLE.md" }, { "id": "python.mypy.allowed", "verdict": "intent", "assert": "mypy is permitted as an additional type checker, not banned. It is required for a Home Assistant integration (platinum strict-typing) and is the lint-only profile's type checker. When used it runs in CI and the editor.", "intentRef": "CODESTYLE.md" }, { "id": "python.coverage.codecov", "verdict": "letter", "assert": "The test job collects coverage (pytest --cov-report=xml) and uploads it to Codecov via codecov/codecov-action, best-effort (continue-on-error and fail_ci_if_error: false). CODECOV_TOKEN is stored in the repo actions secrets. Required for every Python repo with tests. N/A for the lint-only profile (its unittest suite runs under coverage in CI, reported without a threshold and never uploaded to Codecov). In a mixed repo the codecov.yml file-presence is still required by any co-present type that has tests, e.g. csharp.", "intentRef": "WORKFLOW.md", "minProfile": "build" }, { "id": "python.uvlock.pinned", "verdict": "letter", "assert": "Build profile: the committed uv.lock resolves to LF through the repository-wide .editorconfig and .gitattributes defaults. A CRLF-native operational repo adds a narrow uv.lock LF override only if it adopts the uv build profile. N/A for a non-uv Python repo (a Home Assistant integration on pip/requirements) and for the lint-only profile (no uv.lock by definition).", "intentRef": "GOVERNANCE.md#line-endings", "minProfile": "build" }, { "id": "python.scripts.uvx", "verdict": "letter", "assert": "Lint-only profile only: the tools run via uvx (no project install, no lockfile). A uvx @ pin in a run: step is not Dependabot-trackable, so CI runs uvx ruff@latest / uvx mypy@latest - the fleet rule pins only what Dependabot auto-updates and otherwise runs latest, never a manual pin that goes stale. VS Code tasks, README, and CI all run the unpinned latest. N/A for the build profile (which pins tool versions via uv.lock + uv sync --frozen instead).", "intentRef": "CODESTYLE.md" } ] }, "cpp": { "detect": ["*.cpp", "*.cxx", "*.cc", "*.hpp", "*.hxx", "*.h", "*.ino", "*.c"], "profiles": ["lint-only"], "intentRefs": ["CODESTYLE.md"], "checks": [ { "id": "cpp.clangformat.shared", "verdict": "intent", "assert": "A clang-format configuration (.clang-format) drives C/C++ formatting and is shared by the editor, the CLI, and CI, feeding the operational lint gate. The scope is style only. Semantic and static analysis are left to the downstream toolchain that compiles the code (an ESPHome build), which has the compile context clang-tidy would need. A repo's .h is treated as C++ by context.", "intentRef": "CODESTYLE.md" } ] }, "dotnet-publish": { "detect": [".github/actions/dotnet-publish/action.yml", "System.CommandLine", "dotnet_publish_project:"], "checks": [ { "id": "dotnet-publish.smoke.subset", "verdict": "letter", "assert": "The smoke runtime set is a strict non-empty subset of the full runtime set.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" }, { "id": "dotnet-publish.release.asset", "verdict": "letter", "assert": "A non-smoke run uploads one release-asset--dotnet-publish artifact, while a smoke run skips archive creation and upload.", "workflowRef": "WORKFLOW.md#d6---seam--architecture-conformance" } ] }, "docker": { "detect": ["Dockerfile", "build-docker-task.yml"], "requiredSecrets": ["DOCKER_HUB_USERNAME", "DOCKER_HUB_ACCESS_TOKEN"], "checks": [ { "id": "docker.cache.registry", "verdict": "intent", "assert": "Layer cache targets a registry tag (buildcache-), never type=gha.", "workflowRef": "WORKFLOW.md#d9---style--static-see-section-2" }, { "id": "docker.hub.readme", "verdict": "intent", "assert": "Where the Docker Hub overview differs from the project README, a Docker/README.md is published via the docker-readme task (Hub description is size-limited).", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" }, { "id": "docker.always.repush", "verdict": "letter", "assert": "The image always re-pushes on publish (base-image refresh), independent of the release-create skip.", "workflowRef": "WORKFLOW.md#d4---release--publish" } ] }, "homeassistant": { "detect": ["custom_components/*/manifest.json", "hacs.json"], "checks": [ { "id": "ha.release.only", "verdict": "intent", "assert": "Distribution is a GitHub release for HACS (push consumer model); no registry publish.", "workflowRef": "WORKFLOW.md#d4---release--publish" }, { "id": "ha.python.conventions", "verdict": "intent", "assert": "Follows home-assistant/core Python conventions (pip + requirements*.txt, custom_components/ layout, standalone .ruff.toml + pyrightconfig.json) - expected, not drift.", "intentRef": "CODESTYLE.md" }, { "id": "ha.typecheck.mypy", "verdict": "letter", "assert": "mypy --strict runs in CI (platinum strict-typing quality-scale tier).", "intentRef": "CODESTYLE.md" } ] }, "eda": { "detect": ["*.kicad_pcb", "*.kicad_sym", "*.kicad_mod", "*.kicad_sch", "*.pretty"], "checks": [ { "id": "eda.release.zip", "verdict": "intent", "assert": "Distribution is a GitHub release data zip pulled into a local EDA install (pull consumer); no package or image registry.", "workflowRef": "WORKFLOW.md#d4---release--publish" }, { "id": "eda.validation", "verdict": "intent", "assert": "CI runs EDA-specific validation (e.g. kicad-cli ERC/DRC or library linting) - the design-data analogue of the code linters.", "intentRef": "CODESTYLE.md" }, { "id": "eda.build.deterministic", "verdict": "intent", "assert": "Any build-time artifact generation (gerbers, drill, BOM) and version injection is deterministic from the design inputs; aspirational for data-only repos not yet building artifacts.", "workflowRef": "WORKFLOW.md#d3---versioning-and-classification" } ] }, "codegen": { "detect": ["run-codegen-pull-request-task.yml"], "requiredSecrets": [], "checks": [ { "id": "codegen.deterministic", "verdict": "intent", "assert": "Codegen output is deterministic from its inputs (no per-run timestamps/GUIDs); runs as a matrix over both branches.", "workflowRef": "WORKFLOW.md#d8---bots--automation" } ] }, "upstream-wrapper": { "detect": ["check-upstream-version-task.yml", "upstream-version.json"], "checks": [ { "id": "wrapper.tag.fromstate", "verdict": "letter", "assert": "The build reads the committed name->version state file for the tag instead of SemVer2.", "workflowRef": "WORKFLOW.md#d3---versioning-and-classification" }, { "id": "wrapper.tracker.branches", "verdict": "letter", "assert": "The tracker's bump-branch-prefix and branches match the merge-bot's head/base pairs.", "workflowRef": "WORKFLOW.md#d8---bots--automation" } ] }, "source-only": { "detect": ["This repo is source-only"], "checks": [ { "id": "sourceonly.release.tagonly", "verdict": "letter", "assert": "The publish job reaches build-release-task.yml with github: true, every enable_* input false, and expect_release_assets: false, producing a release of tag + source zip + README + LICENSE.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" }, { "id": "sourceonly.nbgv.retained", "verdict": "letter", "assert": "version.json is retained and the reusable release task runs NBGV to compute the tag.", "workflowRef": "WORKFLOW.md#d3---versioning-and-classification" } ] }, "hugo": { "detect": ["hugo.yaml", "hugo.toml", "config/_default/hugo.yaml"], "intentRefs": ["WORKFLOW.md"], "note": "Named for the generator rather than for the transport, because what a repo builds and where the result lands are separate axes. The destination is publish[] ({ target, mechanism }), so a repo changes transport without changing type. Every assert below is phrased without naming the generator except hugo.build.strict, where a generator-specific flag is the letter, so promoting the generic ones to a shared type when a second generator arrives is a registry edit. Deploy credentials are per-environment GitHub Environment secrets and variables, which spec/secrets.json cannot yet express, so a repo does not list them in its registry requiredSecrets: spec/audit.py resolves that list against the repository actions store and would report an environment-scoped name as missing.", "checks": [ { "id": "hugo.build.strict", "verdict": "letter", "assert": "The site build fails on a generator warning rather than rendering around it (hugo --gc --minify --panicOnWarning), and the pull request gate and the deploy run the same build command rather than two variants.", "workflowRef": "WORKFLOW.md#d1---pr-fast-feedback-smoke" }, { "id": "hugo.urls.parity", "verdict": "letter", "assert": "A URL contract gate compares the built tree against a committed list of the URLs that must render and the URLs that must redirect, and asserts a minimum length on each list before comparing it, since a truncated list makes every assertion below it pass vacuously. This is the type's check of record, standing in for the unit tests a site does not have.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" }, { "id": "hugo.output.uncommitted", "verdict": "letter", "assert": "The rendered output is produced in CI only: its roots are gitignored and untracked, and they are excluded from the prose, spelling, and Markdown gates along with any vendored third-party tree. A committed render is drift rather than a deliverable.", "intentRef": "GOVERNANCE.md#documentation-style-conventions" }, { "id": "hugo.generator.pinned", "verdict": "letter", "assert": "The generator is pinned by exact version and by a checksum of the downloaded artifact, verified before install, never installed from a floating action or a latest tag, since the site is reproducible only if the generator is. The pin is declared once, and where two workflows need it something asserts the two copies agree.", "workflowRef": "WORKFLOW.md#d9---style--static-see-section-2" }, { "id": "hugo.vendored.provenance", "verdict": "letter", "assert": "A vendored third-party tree records the upstream repository and the exact ref or commit it was taken from, or is carried by a mechanism that pins it, so a bot or a tracker can move it. An unpinned copy with no recorded origin cannot be updated, diffed against upstream, or audited for a security fix.", "intentRef": "GOVERNANCE.md#release-model" }, { "id": "hugo.deploy.environment", "verdict": "letter", "assert": "The deploy job binds a GitHub Environment and takes every host-specific value and its credential from that environment, so the workflow file names no host, path, or address. A reusable callee re-asserts the environment name in a job of its own, because the environment binding resolves before any step runs and a workflow_call caller is not bound by the dispatch choice list a human sees.", "workflowRef": "WORKFLOW.md#d7---concurrency-permissions-safety" }, { "id": "hugo.deploy.atomic", "verdict": "intent", "assert": "A release installs beside the retained ones under its own immutable id, and is published by moving a single pointer through a temporary and a rename, so no request observes a half-written site and the previous release stays on disk as a rollback target. The transport never deletes at the environment root.", "workflowRef": "WORKFLOW.md#d4---release--publish" }, { "id": "hugo.deploy.verified", "verdict": "letter", "assert": "The deploy's terminal step observes the running host rather than the transport's exit status. It asserts which release is answering, by comparing a version the artifact stamps into the configuration it ships against the id just installed, polling to a bounded timeout because the reload is asynchronous, and it asserts which environment answered. An unreachable host is reported distinctly from an HTTP status.", "workflowRef": "WORKFLOW.md#d4---release--publish" }, { "id": "hugo.deploy.retention", "verdict": "letter", "assert": "Retention at the destination is bounded by a declared count, and the side that owns the prune is written down rather than assumed. Where the deploy credential can observe the destination, the deploy asserts the count converged and fails when it does not. Where the credential is deliberately write-only and cannot observe it, the prune belongs to the host and that ownership is recorded, since widening the credential to reach it would trade a real boundary for a check. The release the live pointer resolves to is never a prune candidate. A prune that runs against a local scratch tree, or that is best-effort, or that no side is recorded as owning, satisfies none of this.", "workflowRef": "WORKFLOW.md#d5---resource-cleanup" } ] }, "docs": { "detect": ["governance-only repo"], "checks": [ { "id": "docs.ci.lintonly", "verdict": "intent", "assert": "CI runs linting only (markdownlint, cspell, JSON schema, actionlint); no build/test.", "intentRef": "AGENTS.md" } ] } }, "crossCutting": { "workflow": { "appliesTo": "*", "contract": "WORKFLOW.md", "method": "Run the WORKFLOW.md 5A static audit and 5B trace scenarios for each applicable D-guarantee against the repo's own Actions.", "note": "GitHub Actions may be repo-specific; assert they implement the contract by outcome, not that they match the catalog snippets byte for byte." }, "branch-model": { "appliesTo": "*", "checks": [ { "id": "branch.both-protected", "verdict": "letter", "assert": "main and develop both exist and are protected.", "intentRef": "GOVERNANCE.md#branching-model" }, { "id": "branch.ruleset.develop", "verdict": "letter", "assert": "The live develop ruleset matches the hub's main payload for the repo's workflowModel (normalized diff): release -> repo-config/develop.json (PR-gated), operational -> repo-config/operational/develop.json (direct signed pushes, deletion + non_fast_forward + required_signatures only).", "intentRef": "repo-config/README.md" }, { "id": "branch.ruleset.main", "verdict": "letter", "assert": "The live main ruleset matches the hub's main repo-config/main.json payload (normalized diff). This ruleset is shared by both workflow models.", "intentRef": "repo-config/README.md" }, { "id": "branch.operational.lintci", "verdict": "intent", "assert": "An operational (workflowModel) repo runs a lint/validation CI (editorconfig/EOL plus domain linters, e.g. Home Assistant or ESPHome config validation or a firmware build; no unit testing) feeding the required Check pull request workflow status job, so the develop -> main promotion PR is gated even though develop takes direct commits. N/A for release repos.", "intentRef": "GOVERNANCE.md#branching-model" }, { "id": "branch.operational.prtriggers", "verdict": "letter", "assert": "The operational repo's lint/validation workflow triggers on pull_request with branches: [ main, develop ], plus push to develop and workflow_dispatch. A pull_request set naming main alone leaves a PR into develop matching no trigger, so it merges with no validation at all (WORKFLOW.md D1.2). The develop result is reported, not required: the required status check stays on main only, since requiring it on develop would gate the direct push the model allows. N/A for release repos.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" } ] }, "carried-scope": { "appliesTo": "*", "checks": [ { "id": "carried.hubonly.absent", "verdict": "letter", "assert": "The repo carries no file the hub hosts rather than carries. spec/audit.py derives the set as the hub's git-tracked paths minus the spec/files.json baseline, and reports each one the repo's ground-truth branch also holds. The remedy is a deletion rather than a re-vendor, since the repo reaches the hub's copy per GOVERNANCE.md 'Hub-Hosted Tooling'. The match is on path alone, so a repo's own content at a path the hub also uses matches without carrying anything of the hub's, and a spec/divergences.json 'gaps' disposition decides which case a hit is: 'retire' asserts a deletion, 'accepted' closes a path collision or a repo-owned file, and an untriaged hit is read before it is acted on.", "intentRef": "GOVERNANCE.md#hub-hosted-tooling" } ] }, "repo-setup": { "appliesTo": "*", "checks": [ { "id": "setup.secrets.present", "verdict": "letter", "assert": "Every requiredSecret for the repo's publish mechanisms is configured (per spec/secrets.json).", "intentRef": "repo-config/README.md" }, { "id": "setup.secrets.noforbidden", "verdict": "letter", "assert": "No forbidden secret is present (e.g. a static NUGET_API_KEY on an OIDC repo).", "intentRef": "spec/secrets.json" }, { "id": "setup.driftnotes.current", "verdict": "intent", "assert": "A registry driftNote records a current deviation from the baseline; once resolved the note is deleted, not left describing finished work. spec/audit.py flags a note asserting outstanding work (pending / not yet / missing / behind / ...) on a repo that otherwise audits clean. Findings derived from an audit run carry its stamp and are re-verified at pickup, never trusted as current state.", "intentRef": "AUDIT.md" }, { "id": "setup.dependabot.ecosystems", "verdict": "intent", "assert": "For each ecosystem the repo's tree implies, .github/dependabot.yml declares it (dual-target main+develop per the fleet norm): github-actions when .github/workflows/ is present (its workflows reference actions, else their versions go stale and a stood-up merge-bot has no PRs to auto-merge), devcontainers when a .devcontainer is present. A missing implied ecosystem is a drift finding. Language ecosystems (nuget/uv/npm) are directory-scoped, audited by inspection.", "intentRef": "GOVERNANCE.md#branching-model" } ] }, "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, and the shared env file is named for what it configures rather than a bare .env. A bare secrets/ directory or a .txt-suffixed single-value secret file is drift. 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" } ] }, "linter-parity": { "appliesTo": "*", "checks": [ { "id": "parity.markdownlint", "verdict": "letter", "assert": "One .markdownlint-cli2.jsonc drives the VS Code extension, the CLI, and CI, and CI runs it.", "intentRef": "GOVERNANCE.md#running-the-linters-locally-known-working-invocations" }, { "id": "parity.cspell", "verdict": "letter", "assert": "A cspell.json (not only a workspace word list) drives the extension, the CLI, and CI.", "intentRef": "CODESTYLE.md" }, { "id": "parity.lang", "verdict": "intent", "assert": "ruff/pyright (Python) and editorconfig/csharpier (C#) configs are shared by editor, CLI, and CI where those languages are present.", "intentRef": "CODESTYLE.md" }, { "id": "parity.hooks", "verdict": "intent", "assert": "A local hook config file exists in the repo's tracked tree (.husky/pre-commit or .pre-commit-config.yaml), and its content, read directly rather than inferred from per-clone core.hooksPath or pre-commit install state neither of which the audit can observe, invokes at minimum the diff-scoped prose gate and the eol check via hub-fetch-run.py. The hub repo itself is the one exception, calling its own scripts/prose_lint.py and scripts/repo_gate.py directly, since it has no separate hub to fetch from. The language-formatting half (ruff/pyright or mypy, CSharpier/dotnet format) is added once the repo's own corpus passes it clean, a repo mid-convergence rather than a repo out of conformance.", "intentRef": "GOVERNANCE.md#running-the-linters-locally-known-working-invocations" } ] }, "recurring-violations": { "appliesTo": "*", "priority": "high", "checks": [ { "id": "recurring.comments", "verdict": "letter", "assert": "Comments are concise, only the non-obvious, no prose narration, and do not grow on re-edit.", "intentRef": "GOVERNANCE.md#comments" }, { "id": "recurring.charset", "verdict": "letter", "assert": "ASCII only in agent-authored text: no em-dash (use a spaced hyphen), no smart quotes, no stray non-ASCII.", "intentRef": "GOVERNANCE.md#character-set" }, { "id": "recurring.spelling", "verdict": "letter", "assert": "US English spelling; the shared cspell.json sets language en-US (a bare en accepts British spellings too).", "intentRef": "CODESTYLE.md#markdown-and-spelling" }, { "id": "recurring.eol", "verdict": "letter", "assert": "Line endings follow paired repository-wide defaults: .editorconfig carries [*] end_of_line, and .gitattributes carries * text=auto eol=. Both set *.bat/*.cmd to CRLF. A missing or mismatched global default is a drift finding. The global default is LF for release repos and Linux-native operational repos. A Windows-native app that uses CRLF requires CRLF in both defaults. Do not add per-language or per-file LF pins where the global default already applies. Edits preserve the file's endings.", "intentRef": "GOVERNANCE.md#line-endings" }, { "id": "recurring.norepoxref", "verdict": "intent", "assert": "A carried file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, .github/copilot-instructions.md, the carried AUDIT.md) carries no coordination reference: no reference to the template repo in prose or link (the coordination flow is machinery a consumer should not see - state the behavior, not the destination), and no sibling fleet repo named as an illustrative example of a rule or adoption. A contextually relevant link to a related project is NOT a coordination reference and is expected (the image that consumes this config, a library this depends on) - the test is whether the link serves a reader of this repo's content. The rule governs carried template content. A repo's own README.md and topical docs are its own content. Two bounded exceptions serve carried behavior: AGENTS.md 'Fleet Bootstrap' must name the hub and its bytes are fixed fleet-wide, and the carried AUDIT.md links hub-hosted audit inputs and procedures to their absolute hub URLs because those files are deliberately not carried. The AGENTS.md exception stops at the verbatim region boundary, and the same file's own prose is judged normally. spec/audit.py mechanically checks AGENTS.md, GOVERNANCE.md and .github/copilot-instructions.md for the template name outside their verbatim sections, and skips the hub itself, whose copies are the source.", "intentRef": "GOVERNANCE.md#documentation-style-conventions" } ] }, "readme-structure": { "appliesTo": "*", "checks": [ { "id": "readme.sections", "verdict": "letter", "assert": "The README carries the required sections and keeps the declared order, per spec/readme-sections.json, which spec/audit.py reads. Required everywhere: Build and Distribution with its Build Status, Releases and Release Notes sub-sections; Table of Contents, with no size threshold; 3rd Party Tools; License as the last section in the file. Required in a public repo: Questions or Issues. The Overview slot has no accepted aliases, so a Use Cases, Features or About heading is a rename finding. A heading the model does not name is dropped before the order comparison, so a genuinely repo-specific section sits anywhere.", "intentRef": "spec/readme-structure.md" }, { "id": "readme.shields", "verdict": "letter", "assert": "Every shield the repo's deliverables imply is present in the section that shield names, additive over the base set, per the shieldClasses in spec/readme-sections.json. Each shield carries the section it belongs in, so the license shield is an ordinary base-class shield addressed to the closing License section rather than a rule of its own, and it is marked exclusive, meaning it must render nowhere else. Matched by shields.io endpoint rather than by alt text or reference name, resolving both reference-style and inline images, and a class is a floor, so an extra shield is not a finding.", "intentRef": "spec/readme-structure.md" }, { "id": "readme.links", "verdict": "letter", "assert": "Reference definitions live at the foot of the file and are named by what they point at, per linkNaming and canonicalLinks in spec/readme-sections.json: a shield ends -shield, a URI ends -link, an in-page anchor and a repo-local path are bare. A destination every repo has carries the same name everywhere (github-link, actions-link, releases-link, commits-link, issues-link, discussions-link, docker-hub-link, nuget-link, pypi-link), keyed to the repo's own URLs so a third-party link keeps its descriptive name, and a repo publishing several of something prefixes the target rather than renaming (nxmeta-docker-hub-link). Grouping is reported as drift rather than letter, and covers the closed five-group set (Sections, Shields, Distribution, Repo, External), their order, sorting by reference name within a group, and a reference sitting in the group its kind implies.", "intentRef": "spec/readme-structure.md" }, { "id": "readme.tools", "verdict": "letter", "assert": "A 3rd Party Tools entry for a tool the fleet catalog names matches that catalog, per spec/third-party-tools.json: the same link and the same description, and the description says what the tool is rather than what this repo does with it. The catalog is a standard set rather than a complete one, so a tool it does not name produces no finding, and adding a tool to it is a hub edit. The section carries no license column.", "intentRef": "spec/readme-structure.md" }, { "id": "readme.tagline", "verdict": "letter", "assert": "The first line after the H1 is the tagline: one sentence, link-free plain text, at most 100 characters, mirrored to the GitHub About description, the HISTORY.md opening, and the Docker Hub short description. Further paragraphs below it are free prose under no length or link rule, and no mirror carries them.", "intentRef": "spec/readme-structure.md" } ] } } }