Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions registry/repos.schema.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@
"url": { "type": "string", "format": "uri" },
"status": { "enum": ["cataloged", "backlog"] },
"types": { "type": "array", "items": { "type": "string" } },
"profiles": { "type": "object", "additionalProperties": { "enum": ["lint-only", "build"] } },
"classificationPending": { "type": "boolean" },
"groundTruthBranch": { "type": "string" },
"workflowModel": { "$ref": "#/$defs/workflowModel" },
Expand Down
26 changes: 18 additions & 8 deletions spec/project-types.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,14 @@
"types": {
"csharp": {
"detect": ["*.csproj", "*.slnx", "*.sln"],
"profiles": ["build"],
"intentRefs": ["CODESTYLE.md", "AGENTS.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" }
{ "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": {
Expand All@@ -31,17 +32,26 @@
},
"python": {
"detect": ["pyproject.toml", "setup.py"],
"profiles": ["build", "lint-only"],
"canonicalPlacement": "pyproject.toml",
"profileNote": "Two structurally-detected profiles share this type (CODESTYLE.md Python 'Two profiles'). PROJECT: the Python has third-party runtime dependencies or is the repo's deliverable - a PEP 621 uv project (pyproject [project]+deps+[build-system], committed uv.lock, uv sync --frozen + uv run in CI). SCRIPTS: 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: [project]+deps/[build-system] + uv.lock -> PROJECT; tool-config-only, no [project]/[build-system], no uv.lock -> SCRIPTS. The profile changes which checks apply: see python.profile.detect and the per-check N/A notes.",
"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 profile is read from pyproject.toml: a [project] table with runtime dependencies (or a [build-system]) is the PROJECT profile; a pyproject carrying only [tool.*] config with no [project]/[build-system] and no uv.lock is the SCRIPTS profile. A SCRIPTS subtree must not carry a uv.lock or project/build metadata (that would misrepresent it as a shippable package); a PROJECT must.", "intentRef": "CODESTYLE.md" },
{ "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": "PROJECT 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 SCRIPTS profile, whose type checker is mypy over stdlib-only code (python.mypy.allowed).", "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); required for a Home Assistant integration (platinum strict-typing) and is the SCRIPTS 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 SCRIPTS profile (lint/type-checked only, no pytest); 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" },
{ "id": "python.uvlock.pinned", "verdict": "letter", "assert": "PROJECT profile: the committed uv.lock is pinned to LF in both .editorconfig ([uv.lock]) and .gitattributes (uv.lock text eol=lf); uv regenerates it LF on every platform, so a CRLF-default repo otherwise reds editorconfig-checker on every uv lock/sync. N/A for a non-uv Python repo (a Home Assistant integration on pip/requirements) and for the SCRIPTS profile (no uv.lock by definition).", "intentRef": "AGENTS.md#line-endings" },
{ "id": "python.scripts.uvx", "verdict": "letter", "assert": "SCRIPTS profile only: the tools run via uvx (no project install, no lockfile). A uvx <tool>@<ver> 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 PROJECT profile (which pins tool versions via uv.lock + uv sync --frozen 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 (lint/type-checked only, no pytest). 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 is pinned to LF in both .editorconfig ([uv.lock]) and .gitattributes (uv.lock text eol=lf). uv regenerates it LF on every platform, so a CRLF-default repo otherwise reds editorconfig-checker on every uv lock/sync. 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": "AGENTS.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 <tool>@<ver> 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" }
]
},
"console": {
Expand Down
4 changes: 3 additions & 1 deletion spec/project-types.schema.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,14 +20,16 @@
"verdict": { "enum": ["letter", "intent"] },
"assert": { "type": "string" },
"intentRef": { "type": "string" },
"workflowRef": { "type": "string" }
"workflowRef": { "type": "string" },
"minProfile": { "enum": ["lint-only", "build"] }
}
},
"typeDef": {
"type": "object",
"additionalProperties": true,
"properties": {
"checks": { "type": "array", "items": { "$ref": "#/$defs/check" } },
"profiles": { "type": "array", "items": { "enum": ["lint-only", "build"] } },
"appliesTo": { "type": ["string", "array"] },
"priority": { "enum": ["high", "normal"] }
}
Expand Down
12 changes: 12 additions & 0 deletions spec/validate.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,6 +139,18 @@ def check_secret_set(label, entry, need_kind):
for t in repo_types:
if t not in known_types:
errors.append(f"{name}: type '{t}' not defined in project-types.json")
# A declared profile must name one of the repo's types and a profile that type allows (spec/type-model.md).
# CI runs no JSON-schema validation, so guard the shape here rather than crash on .items().
profiles_decl = repo.get("profiles", {})
if not isinstance(profiles_decl, dict):
errors.append(f"{name}: profiles must be an object mapping a type to its profile")
profiles_decl = {}
for tname, prof in profiles_decl.items():
allowed = types["types"].get(tname, {}).get("profiles", [])
if tname not in repo_types:
errors.append(f"{name}: profile declared for '{tname}', not one of the repo's types")
elif tname in known_types and prof not in allowed:
errors.append(f"{name}: type '{tname}' profile '{prof}' not in its allowed profiles {allowed or '[]'}")

model = repo.get("workflowModel")
if model is not None and model not in WORKFLOW_MODELS:
Expand Down