From 6f41ea1a2d8cb6dd13a8a287182d5ced65e707cf Mon Sep 17 00:00:00 2001 From: Randy Northrup Date: Thu, 24 Sep 2026 16:35:16 -0700 Subject: [PATCH 1/6] fix: repair shipped templates and their gates The C# Directory.Build.props template has been malformed XML since v0.5.0: an XML comment contained "--", so MSBuild refused to load it. PSUseCorrectCasing intermittently crashed PSScriptAnalyzer and dropped its findings while -EnableExit still exited 0; the rule is off and every documented command adds -ErrorAction Stop. The pre-commit template's vulture hook failed without paths, mypy ran where dependency types were Any, ShellCheck needed Docker, and mixed-line-ending --fix=lf rewrote CRLF-by-policy files. Add the C# dead-code severities and a cargo-deny policy, stricter mypy codes, shebang and workflow hooks, and a test that parses every template. Co-Authored-By: Claude Opus 5.5 (1M context) --- templates/.pre-commit-config.yaml | 62 ++++++++++--- templates/README.md | 40 ++++++++- templates/csharp/.editorconfig | 24 +++++ templates/csharp/Directory.Build.props | 8 +- .../powershell/PSScriptAnalyzerSettings.psd1 | 13 ++- templates/python/mypy.ini | 15 +++- templates/rust/deny.toml | 60 +++++++++++++ tests/test_templates.py | 89 +++++++++++++++++++ 8 files changed, 291 insertions(+), 20 deletions(-) create mode 100644 templates/csharp/.editorconfig create mode 100644 templates/rust/deny.toml create mode 100644 tests/test_templates.py diff --git a/templates/.pre-commit-config.yaml b/templates/.pre-commit-config.yaml index 3ef778f..0da5f73 100644 --- a/templates/.pre-commit-config.yaml +++ b/templates/.pre-commit-config.yaml @@ -2,7 +2,10 @@ # # pre-commit install # run on every commit # pre-commit run --all-files # run over the whole repo now -# pre-commit autoupdate # bump pinned rev: pins +# pre-commit autoupdate --freeze # bump pins to commit SHAs +# +# Tags are movable; `--freeze` records each hook repository's commit SHA with +# the tag as a comment, so a retagged release cannot change what runs. # # Delete the language blocks a given project does not use — every hook that # stays enabled must pass, so an unused block is pure friction. @@ -18,10 +21,18 @@ repos: - id: end-of-file-fixer - id: check-merge-conflict - id: check-case-conflict + # A script with a shebang must be executable and an executable must have + # a shebang. A Windows checkout cannot see a missing POSIX executable bit. + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable - id: check-symlinks - id: destroyed-symlinks + # .gitattributes owns line endings (for example `* text=auto eol=lf` and + # `*.ps1 text eol=crlf`); this hook only rejects files that mix them. + # --fix=lf also rewrites uniformly CRLF files, so it fails every commit + # that touches a script kept in CRLF by that policy. - id: mixed-line-ending - args: [--fix=lf] + args: [--fix=no] - id: check-added-large-files args: [--maxkb=1024] - id: check-json @@ -48,16 +59,16 @@ repos: args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v2.3.1 - hooks: - - id: mypy - additional_dependencies: [types-requests, types-PyYAML] + # mypy runs in the local block below, in the project's own environment. - repo: https://github.com/jendrikseipp/vulture rev: v2.16 hooks: - id: vulture # dead Python code + # The hook passes no filenames: unused-code analysis needs the whole + # tree. Name owned source roots here or in [tool.vulture] paths; + # without either, vulture exits 2 before checking anything. + args: [src, tests] - repo: https://github.com/PyCQA/bandit rev: 1.9.4 @@ -65,8 +76,11 @@ repos: - id: bandit # ── Shell ─────────────────────────────────────────────────────────── - - repo: https://github.com/koalaman/shellcheck-precommit - rev: v0.11.0 + # shellcheck-py installs the upstream ShellCheck binary from PyPI. The + # koalaman/shellcheck-precommit hook is a Docker image and fails on any + # machine without a running Docker daemon. + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 hooks: - id: shellcheck args: [--severity=style, --enable=all] @@ -77,6 +91,21 @@ repos: - id: shfmt args: [-i, '4', -ci, -sr, -d] + # ── GitHub Actions workflows ──────────────────────────────────────── + # actionlint checks workflow syntax, expressions, and embedded shell; + # zizmor audits security: unpinned actions, persisted checkout credentials, + # template injection, and excessive permissions. + - repo: https://github.com/rhysd/actionlint + rev: v1.7.12 + hooks: + - id: actionlint + + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.30.1 + hooks: + - id: zizmor + args: [--no-progress, --offline] + # ── C / C++ ───────────────────────────────────────────────────────── - repo: https://github.com/pre-commit/mirrors-clang-format rev: v22.1.8 @@ -90,6 +119,17 @@ repos: # or otherwise pin them in the project's toolchain before enabling the hooks. - repo: local hooks: + - id: mypy + name: mypy (project environment) + # mirrors-mypy's isolated environment lacks the project's dependencies + # and passes --ignore-missing-imports, so their types become Any and go + # unchecked. Use the interpreter that has them; on Windows that is + # python_runtime.bin from detect-stack. + entry: python -m mypy + language: system + types_or: [python, pyi] + require_serial: true + - id: eslint name: eslint (strict, type-checked) entry: eslint --max-warnings=0 @@ -157,7 +197,9 @@ repos: name: PSScriptAnalyzer (strict) # `pwsh` is the cross-platform host. On a Windows-only project that # supports only Windows PowerShell 5.1, replace it with `powershell`. - entry: pwsh -NoProfile -Command "Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit" + # -ErrorAction Stop turns an analyzer crash into a failing hook instead + # of a silent pass with that rule's findings missing. + entry: pwsh -NoProfile -Command "Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit -ErrorAction Stop" language: system files: \.(ps1|psm1|psd1)$ pass_filenames: false diff --git a/templates/README.md b/templates/README.md index e9d08d8..35e077b 100644 --- a/templates/README.md +++ b/templates/README.md @@ -23,7 +23,10 @@ compatibility observations, not permanent minimums or automatic dependency pins. The 2026-09-07 source-backed review and current validation boundaries are in -[`docs/QUALITY-REVIEW.md`](../docs/QUALITY-REVIEW.md). Preserve the older snapshot +[`docs/QUALITY-REVIEW.md`](../docs/QUALITY-REVIEW.md). Its 2026-09-24 audit +drilled the pre-commit, mypy, PSScriptAnalyzer, C#, and cargo-deny changes with +the versions it records; `test_templates.py` also parses every template that +the Python standard library can read. Preserve the older snapshot as history; select project runtimes explicitly rather than copying version or module defaults as universal choices. @@ -39,7 +42,9 @@ module defaults as universal choices. | `web/knip.jsonc` | project root | dead files / exports / deps | | `cpp/.clang-tidy` | project root | C++ static analysis | | `csharp/Directory.Build.props` | solution root | C# compiler + analyzers | +| `csharp/.editorconfig` | solution root, or merge | C# dead-code rule severities | | `rust/clippy-strict.toml` | see file — two parts | Rust lint | +| `rust/deny.toml` | workspace root | advisories, licenses, bans, sources | | `powershell/PSScriptAnalyzerSettings.psd1` | project root | PowerShell | | `workflow/PLAN.md` | extend the canonical project plan | native delivery ledger; Python 3.12+ | | `.pre-commit-config.yaml` | project root | representative pre-commit hooks | @@ -82,14 +87,16 @@ visible. Commands still require the corresponding project-local tools. ruff check . ruff format --check . mypy . -vulture . +vulture src tests # owned source roots bandit -r . +deptry . pip-audit # TypeScript / JS tsc --noEmit eslint . --max-warnings=0 knip --strict +dpdm --no-warning --no-tree --exit-code circular:1 src/main.ts prettier --check . # CSS / HTML @@ -113,12 +120,17 @@ dotnet build -warnaserror roslynator analyze # PowerShell (run inside PowerShell 5.1+) -Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit +Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit -ErrorAction Stop # Cross-cutting gitleaks git --redact # secrets in reachable git history semgrep scan --error --config=auto # findings must cause a failing exit jscpd . # configure a project duplication threshold +osv-scanner scan source -r . # every lockfile against OSV advisories + +# GitHub Actions workflows +actionlint +zizmor --offline .github ``` Shell-project gates use POSIX filename expansion: @@ -188,7 +200,21 @@ checks. Domain-local numeric constants need no TypeScript file-wide exemption. - **`EnforceCodeStyleInBuild`** runs supported IDE code-style analyzers during build. Set diagnostic severities in `.editorconfig` and drill them; it does not promote every suggestion. Keep `dotnet format --verify-no-changes` too. -- **`-EnableExit`** is what makes PSScriptAnalyzer fail a build. +- **`-EnableExit`** is what makes PSScriptAnalyzer fail a build, and + **`-ErrorAction Stop`** is what makes an analyzer crash fail it. A rule that + throws is otherwise a non-terminating error: its findings disappear and the + command can exit 0 (see `PSUseCorrectCasing` below). +- **The vulture pre-commit hook passes no filenames**, because unused-code + analysis needs the whole tree. It exits 2 with `Please pass at least one file + or directory` until the project sets `[tool.vulture] paths` in + `pyproject.toml` or gives the hook explicit `args`. +- **mypy belongs in a project-environment hook.** `mirrors-mypy` runs in an + isolated environment holding only its `additional_dependencies`, and its + default `--ignore-missing-imports` overrides the template's + `ignore_missing_imports = false`. Every project dependency becomes `Any`, so + calls into it go unchecked, and `disallow_any_unimported` then rejects + correct code: `Return type becomes "Any" due to an unfollowed import`. The + template runs mypy with the project's own interpreter instead. - **Clippy lint *levels* go in `Cargo.toml`, not `clippy.toml`.** `clippy.toml` only tunes thresholds. See `rust/clippy-strict.toml` — it holds both halves. - **gitleaks allowlists well-known example keys** (e.g. @@ -218,6 +244,12 @@ Found by deliberately breaking things. Each of these looked configured. not make the combination supported. - **`maxDepth: Infinity`** in any JSON-serialised rule option becomes `null`, which can silently disable traversal. Use a finite number. +- **`PSUseCorrectCasing` can crash and still pass.** With PSScriptAnalyzer + 1.24.0 and 1.25.0 on PowerShell 7.6, it intermittently threw + `NullReferenceException` on this package's own test scripts. In a fresh + process a planted casing violation then went unreported and `-EnableExit` + exited 0. The template disables the rule; `-ErrorAction Stop` makes any + remaining analyzer crash fail the command instead. ### TypeScript 7 is outside typescript-eslint's supported range diff --git a/templates/csharp/.editorconfig b/templates/csharp/.editorconfig new file mode 100644 index 0000000..c4a9d3a --- /dev/null +++ b/templates/csharp/.editorconfig @@ -0,0 +1,24 @@ +# Strict C# diagnostic severities. Copy to the solution root beside +# Directory.Build.props, or merge these entries into the existing .editorconfig. +# +# EnforceCodeStyleInBuild runs IDE analyzers during `dotnet build`, but IDE +# rules default to silent or suggestion, which the build does not report. A +# rule is a gate only when its severity is warning or error here; with +# TreatWarningsAsErrors either level fails the build. + +[*.cs] + +# --- dead code --- +# Private member that nothing references. +dotnet_diagnostic.IDE0051.severity = error +# Private member that is written but never read. +dotnet_diagnostic.IDE0052.severity = error +# Value assigned and then overwritten or discarded before any read. +dotnet_diagnostic.IDE0059.severity = error +# Parameter that the method body never uses, at any accessibility by default. +# Name a parameter that exists only to satisfy a signature `_` (or `_1`, `_2`); +# the rule ignores discards. +dotnet_diagnostic.IDE0060.severity = error +# Unnecessary using directive. Reported at build time only when +# GenerateDocumentationFile is true, which Directory.Build.props sets. +dotnet_diagnostic.IDE0005.severity = error diff --git a/templates/csharp/Directory.Build.props b/templates/csharp/Directory.Build.props index 81831c0..3af9c1b 100644 --- a/templates/csharp/Directory.Build.props +++ b/templates/csharp/Directory.Build.props @@ -27,9 +27,11 @@ true + diagnostic severities in the existing .editorconfig (the companion + template sets the dead-code rules): this switch does not promote + every suggestion to an error. Keep the dotnet format verify-no-changes + check as a separate formatting gate. XML comments cannot contain two + consecutive hyphens, so command-line flags are not written here. --> true diff --git a/templates/powershell/PSScriptAnalyzerSettings.psd1 b/templates/powershell/PSScriptAnalyzerSettings.psd1 index bb2c659..d232130 100644 --- a/templates/powershell/PSScriptAnalyzerSettings.psd1 +++ b/templates/powershell/PSScriptAnalyzerSettings.psd1 @@ -2,9 +2,12 @@ Strict PSScriptAnalyzer settings. Copy to project root as PSScriptAnalyzerSettings.psd1, then run: - Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit + Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit -ErrorAction Stop -EnableExit is what makes it a gate: non-zero exit when any rule fires. + -ErrorAction Stop makes an analyzer failure fail the gate. Without it, a + rule that throws is written as a non-terminating error, its findings are + lost, and the command can still exit 0. #> @{ # Every built-in rule, including the ones off by default. @@ -50,7 +53,13 @@ PSAlignAssignmentStatement = @{ Enable = $false } # Full cmdlet names only -- no `ls`, `%`, `?` in committed scripts. PSAvoidUsingCmdletAliases = @{ Enable = $true } - PSUseCorrectCasing = @{ Enable = $true } + # Off because it is not a reliable gate. With PSScriptAnalyzer 1.24.0 + # and 1.25.0 on PowerShell 7.6, this rule intermittently throws + # NullReferenceException in CommandInfo.get_Parameters (2026-09-24, + # this package's own tests/*.ps1). A planted casing violation then went + # unreported and the documented -EnableExit command exited 0. Re-enable + # only after a fixed release passes repeated fresh-process red drills. + PSUseCorrectCasing = @{ Enable = $false } # These are shipped historical command inventories, not proof of current # host compatibility. Select installed profiles for the support contract # and execute tests on actual supported hosts. Do not invent profile IDs. diff --git a/templates/python/mypy.ini b/templates/python/mypy.ini index 62baa3f..59be7e8 100644 --- a/templates/python/mypy.ini +++ b/templates/python/mypy.ini @@ -32,7 +32,20 @@ pretty = true ; Every `# type: ignore` must name the code it suppresses, and unused ; ignores are themselves errors — stops ignore-rot. -enable_error_code = ignore-without-code,redundant-expr,truthy-bool,possibly-undefined,unused-awaitable +; +; Optional correctness codes that `strict` does not enable: +; explicit-override overriding methods need @typing.override (TypeScript's +; noImplicitOverride); a renamed base method cannot leave +; a silently orphaned "override" behind. Python < 3.12 +; imports `override` from typing_extensions. +; exhaustive-match a `match` over an enum/union/literal must handle every +; case (TypeScript's switch-exhaustiveness-check). +; mutable-override rejects narrowing a mutable attribute in a subclass. +; truthy-iterable `if items:` on an Iterable that may be a generator is +; always true. +; deprecated uses of APIs marked @deprecated (PEP 702), including +; standard-library deprecations recorded in typeshed. +enable_error_code = ignore-without-code,redundant-expr,truthy-bool,possibly-undefined,unused-awaitable,explicit-override,exhaustive-match,mutable-override,truthy-iterable,deprecated warn_unused_ignores = true [mypy-tests.*] diff --git a/templates/rust/deny.toml b/templates/rust/deny.toml new file mode 100644 index 0000000..cd30c58 --- /dev/null +++ b/templates/rust/deny.toml @@ -0,0 +1,60 @@ +# Strict cargo-deny policy. Copy to the workspace root as deny.toml, then run: +# +# cargo deny check +# +# cargo-deny checks security advisories, licenses, banned or duplicate crates, +# and dependency sources. The policy below is what makes it a gate: with no +# license allow-list, every license is rejected, and unknown sources only warn. +# Written for cargo-deny 0.19; field reference: +# https://embarkstudios.github.io/cargo-deny/checks/cfg.html + +[graph] +# Check every feature's dependencies, like the Clippy gate. Select supported +# feature sets instead if all features cannot be enabled together. +all-features = true + +[advisories] +# A yanked release was withdrawn by its author. +yanked = "deny" +# Unmaintained and unsound crates anywhere in the graph fail the check. +unmaintained = "all" +unsound = "all" +# Each ignored advisory needs its ID and a written reason, for example +# { id = "RUSTSEC-0000-0000", reason = "affected API is never called" }. +ignore = [] + +[licenses] +# Which licenses are acceptable is a legal decision for the project owner. +# These permissive licenses are a starting point, not advice; add others only +# after that review and record why. +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "MIT", + "Unicode-3.0", + "Zlib", +] +exceptions = [] + +[licenses.private] +# Unpublished crates (publish = false) are not distributed under a license. +ignore = true + +[bans] +# Duplicate versions compile the same crate twice. Report them without failing: +# transitive duplicates are rarely the project's to fix. +multiple-versions = "warn" +# A `*` requirement accepts any future release, including breaking ones. +wildcards = "deny" +# Path dependencies between unpublished workspace crates have no version to pin. +allow-wildcard-paths = true + +[sources] +# crates.io only, unless the project lists another registry or Git source here. +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] diff --git a/tests/test_templates.py b/tests/test_templates.py new file mode 100644 index 0000000..148c693 --- /dev/null +++ b/tests/test_templates.py @@ -0,0 +1,89 @@ +"""Require every shipped configuration template to load in its own file format. + +Loading is necessary, not sufficient: each tool's red drills prove the rules +fire. This suite catches the cheaper failure, a template that no tool can read. +""" + +import configparser +import json +import tomllib +import unittest +from pathlib import Path + +# Parses only checked-in templates, never untrusted XML. +from xml.etree import ElementTree as ET # nosec B405 + +ROOT = Path(__file__).resolve().parents[1] +TEMPLATES = ROOT / "templates" + +XML_TEMPLATES = ("csharp/Directory.Build.props",) +TOML_TEMPLATES = ("python/ruff.toml", "rust/clippy-strict.toml", "rust/deny.toml") +INI_TEMPLATES = ("python/mypy.ini", "csharp/.editorconfig") +JSON_TEMPLATES = ("web/.stylelintrc.json",) +# JSONC (tsconfig, knip) and YAML (pre-commit, clang-tidy) need parsers outside +# the standard library; their own tools and the repository hooks load them. +OTHER_FORMATS = ( + "typescript/tsconfig.strict.json", + "web/knip.jsonc", + ".pre-commit-config.yaml", + "cpp/.clang-tidy", + "powershell/PSScriptAnalyzerSettings.psd1", + "typescript/eslint.config.mjs", + "cpp/sanitizers.md", + "workflow/PLAN.md", + "README.md", +) + + +class TemplateFormatTests(unittest.TestCase): + """Parse configuration templates with the same grammar their tools require.""" + + def test_every_template_is_classified(self) -> None: + """A new template cannot silently escape format checking.""" + shipped = { + path.relative_to(TEMPLATES).as_posix() + for path in TEMPLATES.rglob("*") + if path.is_file() + } + classified = { + *XML_TEMPLATES, + *TOML_TEMPLATES, + *INI_TEMPLATES, + *JSON_TEMPLATES, + *OTHER_FORMATS, + } + self.assertEqual(shipped, classified) + + def test_xml_templates_parse(self) -> None: + """MSBuild rejects malformed XML, including '--' inside comments.""" + for relative in XML_TEMPLATES: + with self.subTest(relative): + # Checked-in template, not untrusted input. + ET.parse(TEMPLATES / relative) # noqa: S314 # nosec B314 + + def test_toml_templates_parse(self) -> None: + """TOML templates load with the standard TOML parser.""" + for relative in TOML_TEMPLATES: + with self.subTest(relative), (TEMPLATES / relative).open("rb") as stream: + self.assertIsInstance(tomllib.load(stream), dict) + + def test_ini_templates_parse(self) -> None: + """INI-style templates have sections and no duplicate keys.""" + for relative in INI_TEMPLATES: + with self.subTest(relative): + parser = configparser.ConfigParser(interpolation=None) + read = parser.read(TEMPLATES / relative, encoding="utf-8") + self.assertEqual(len(read), 1) + self.assertTrue(parser.sections()) + + def test_json_templates_parse(self) -> None: + """Strict JSON templates contain no comments or trailing commas.""" + for relative in JSON_TEMPLATES: + with self.subTest(relative): + self.assertIsInstance( + json.loads((TEMPLATES / relative).read_text(encoding="utf-8")), dict + ) + + +if __name__ == "__main__": + unittest.main() From f7ad409d8088b6a561729e80f6694c09f4542266 Mon Sep 17 00:00:00 2001 From: Randy Northrup Date: Thu, 24 Sep 2026 16:35:30 -0700 Subject: [PATCH 2/6] fix: separate input errors and keep plan modes verify-format-safe.py let missing, undecodable, or directory input escape as an uncaught exception, which exits 1: the code its contract reserves for a changed AST. It also decoded source as UTF-8 instead of honoring PEP 263 declarations. It now parses bytes and exits 2 for any input it cannot compare. The atomic plan writer replaced PLAN.md with its owner-only temporary file, so the plan became mode 0600 on POSIX. Preserve the existing mode, or use the umask for a new plan, and flush the directory entry after the rename. Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/delivery/writer.py | 28 ++++++++++ scripts/verify-format-safe.py | 14 +++-- tests/test_update_delivery.py | 23 ++++++++ tests/test_verify_format_safe.py | 91 ++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 tests/test_verify_format_safe.py diff --git a/scripts/delivery/writer.py b/scripts/delivery/writer.py index 552e5e3..4cccfd2 100644 --- a/scripts/delivery/writer.py +++ b/scripts/delivery/writer.py @@ -4,6 +4,7 @@ import hashlib import json import os +import shutil import tempfile from dataclasses import dataclass from pathlib import Path @@ -15,6 +16,7 @@ from .snapshot import owned_path MISSING = "missing" +DEFAULT_FILE_MODE = 0o666 class UpdateConflictError(RuntimeError): @@ -64,6 +66,25 @@ def acquire_lock(path: Path, expected: str) -> None: raise +def default_file_mode() -> int: + """Return the mode an ordinary new file receives under the process umask.""" + # os.umask can only be read by setting it; restore it before returning. + umask = os.umask(0) + os.umask(umask) + return DEFAULT_FILE_MODE & ~umask + + +def sync_directory(directory: Path) -> None: + """Persist the rename itself on POSIX; Windows exposes no directory handle to flush.""" + if os.name == "nt": + return + descriptor = os.open(directory, os.O_RDONLY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + def replace_owned(plan: Path, candidate: bytes, expected: str) -> None: """Flush complete bytes, recheck the precondition, and atomically replace the plan.""" temporary: Path | None = None @@ -75,8 +96,15 @@ def replace_owned(plan: Path, candidate: bytes, expected: str) -> None: stream.write(candidate) stream.flush() os.fsync(stream.fileno()) + # Temporary files are owner-only; the replacement must keep the plan's + # existing permissions or, for a new plan, those of an ordinary file. + if plan.exists(): + shutil.copymode(plan, temporary) + else: + temporary.chmod(default_file_mode()) require_expected(plan, expected) temporary.replace(plan) + sync_directory(plan.parent) finally: if temporary is not None and temporary.exists(): temporary.unlink() diff --git a/scripts/verify-format-safe.py b/scripts/verify-format-safe.py index dd93949..ac5fc9d 100755 --- a/scripts/verify-format-safe.py +++ b/scripts/verify-format-safe.py @@ -12,7 +12,7 @@ verify-format-safe.py BEFORE.py AFTER.py Exit 0 = parsed AST identical. Exit 1 = AST changed; review before committing. -Exit 2 = a file failed to parse. +Exit 2 = a file could not be read, decoded, or parsed, so nothing was compared. """ from __future__ import annotations @@ -34,9 +34,10 @@ def normalized_ast(path: Path) -> str: Line and column attributes are excluded because they legitimately change during reformatting and are not part of the syntax structure compared here. + Bytes are parsed directly so a PEP 263 encoding declaration or BOM is + honored exactly as the interpreter would honor it. """ - source = path.read_text(encoding="utf-8") - tree = ast.parse(source, filename=str(path)) + tree = ast.parse(path.read_bytes(), filename=str(path)) return ast.dump(tree, annotate_fields=True, include_attributes=False) @@ -48,11 +49,14 @@ def main(argv: list[str]) -> int: before, after = Path(argv[1]), Path(argv[2]) + # An unreadable or undecodable file must not surface as an uncaught + # exception: Python exits 1 for those, which this contract reserves for a + # real syntax change. ValueError covers decoding and null-byte failures. try: before_ast = normalized_ast(before) after_ast = normalized_ast(after) - except SyntaxError as exc: - print(f" PARSE ERROR: {exc}", file=sys.stderr) + except (OSError, SyntaxError, ValueError) as exc: + print(f" ERROR: cannot compare - {exc}", file=sys.stderr) return EXIT_PARSE_ERROR # Output is deliberately ASCII-only. This runs under whatever console the diff --git a/tests/test_update_delivery.py b/tests/test_update_delivery.py index aa63308..ecb03ce 100644 --- a/tests/test_update_delivery.py +++ b/tests/test_update_delivery.py @@ -1,9 +1,12 @@ """Verify atomic state changes, conflict refusal, and preservation of user edits.""" import hashlib +import os +import stat import tempfile import unittest from pathlib import Path +from typing import override from unittest.mock import patch from scripts.delivery.reader import PlanError @@ -14,6 +17,7 @@ class UpdateTests(unittest.TestCase): """Inspect real file outcomes for positive, conflicting, and interrupted updates.""" + @override def setUp(self) -> None: """Create an owned workspace and a valid initial candidate.""" self.temporary = tempfile.TemporaryDirectory(prefix="delivery-update-") @@ -70,6 +74,25 @@ def test_failure_before_replace_preserves_complete_plan(self) -> None: self.assertFalse((self.root / "PLAN.md.lock").exists()) self.assertEqual(list(self.root.glob(".PLAN.md.*.tmp")), []) + @unittest.skipIf(os.name == "nt", "Windows exposes only a read-only flag, not mode bits") + def test_replacement_keeps_plan_permissions(self) -> None: + """Atomic replacement keeps the plan's mode rather than the owner-only temporary mode.""" + plan = save(self.root, self.data) + plan.chmod(0o640) + original = plan.read_bytes() + self.candidate.write_bytes(original + b"\nRevised rationale.\n") + update(self.root, "PLAN.md", self.candidate, hashlib.sha256(original).hexdigest()) + self.assertEqual(plan.read_bytes(), self.candidate.read_bytes()) + self.assertEqual(stat.S_IMODE(plan.stat().st_mode), 0o640) + + @unittest.skipIf(os.name == "nt", "Windows exposes only a read-only flag, not mode bits") + def test_new_plan_receives_ordinary_file_permissions(self) -> None: + """A created plan follows the umask like any other new file, not owner-only.""" + previous = os.umask(0o022) + self.addCleanup(os.umask, previous) + update(self.root, "PLAN.md", self.candidate, "missing") + self.assertEqual(stat.S_IMODE((self.root / "PLAN.md").stat().st_mode), 0o644) + def test_invalid_candidate_does_not_write(self) -> None: """Malformed candidate state cannot replace a valid record.""" plan = save(self.root, self.data) diff --git a/tests/test_verify_format_safe.py b/tests/test_verify_format_safe.py new file mode 100644 index 0000000..22af76d --- /dev/null +++ b/tests/test_verify_format_safe.py @@ -0,0 +1,91 @@ +"""Verify the formatting helper's exit contract through its documented command line.""" + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from typing import override + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "verify-format-safe.py" +EXIT_IDENTICAL = 0 +EXIT_CHANGED = 1 +EXIT_UNREADABLE = 2 + + +class FormatSafetyTests(unittest.TestCase): + """Distinguish identical syntax, changed syntax, and inputs that cannot be compared.""" + + @override + def setUp(self) -> None: + """Create disposable source files for each comparison.""" + self.temporary = tempfile.TemporaryDirectory(prefix="format-safety-") + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + + def write(self, name: str, content: bytes) -> Path: + """Store exact source bytes, including non-UTF-8 encodings.""" + path = self.root / name + path.write_bytes(content) + return path + + def compare(self, *arguments: Path | str) -> subprocess.CompletedProcess[str]: + """Run the helper exactly as the retrofit workflow documents it.""" + # Trusted interpreter and checked-in helper; no shell. + return subprocess.run( # noqa: S603 # nosec B603 + [sys.executable, str(SCRIPT), *map(str, arguments)], + capture_output=True, + text=True, + check=False, + timeout=30, + ) + + def test_layout_only_change_is_identical(self) -> None: + """Quote, spacing, and trailing-comma changes leave the parsed syntax unchanged.""" + before = self.write("before.py", b"values = [ 'a','b' ]\n") + after = self.write("after.py", b'values = [\n "a",\n "b",\n]\n') + result = self.compare(before, after) + self.assertEqual(result.returncode, EXIT_IDENTICAL, result.stderr) + self.assertIn("AST identical", result.stdout) + + def test_structural_change_is_reported(self) -> None: + """A changed operand is a syntax change, not formatting.""" + before = self.write("before.py", b"limit = 10\n") + after = self.write("after.py", b"limit = 11\n") + result = self.compare(before, after) + self.assertEqual(result.returncode, EXIT_CHANGED, result.stderr) + self.assertIn("AST DIFFERS", result.stdout) + + def test_declared_source_encoding_is_honored(self) -> None: + """A valid PEP 263 Latin-1 file compares equal to its UTF-8 reformatting.""" + before = self.write("before.py", b"# -*- coding: latin-1 -*-\nname = '\xe9'\n") + after = self.write("after.py", 'name = "é"\n'.encode()) + result = self.compare(before, after) + self.assertEqual(result.returncode, EXIT_IDENTICAL, result.stderr) + + def test_inputs_that_cannot_be_compared_are_not_changes(self) -> None: + """Missing, undecodable, and invalid sources use the input-error exit, not 'changed'.""" + valid = self.write("valid.py", b"answer = 42\n") + cases = { + "missing file": self.root / "missing.py", + "undeclared non-UTF-8 bytes": self.write("latin.py", b"name = '\xe9'\n"), + "syntax error": self.write("broken.py", b"def broken(:\n"), + "directory": self.root, + } + for label, path in cases.items(): + with self.subTest(label): + result = self.compare(valid, path) + self.assertEqual(result.returncode, EXIT_UNREADABLE, result.stdout + result.stderr) + self.assertIn("ERROR", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_wrong_argument_count_prints_usage(self) -> None: + """The command refuses to guess which files to compare.""" + result = self.compare(self.write("only.py", b"pass\n")) + self.assertEqual(result.returncode, EXIT_UNREADABLE) + self.assertIn("verify-format-safe.py BEFORE.py AFTER.py", result.stderr) + + +if __name__ == "__main__": + unittest.main() From f3483f5205f1725094fc51a595ee97c902ffed55 Mon Sep 17 00:00:00 2001 From: Randy Northrup Date: Thu, 24 Sep 2026 16:35:44 -0700 Subject: [PATCH 3/6] build: run the shipped gates on this repository The package asked projects to run ShellCheck, shfmt, PSScriptAnalyzer, vulture, Bandit, and coverage but ran none of them on itself, and CI type-checked only the delivery package, not the tests. Add a frozen pre-commit configuration with those hooks, both mypy scopes under the shipped template, and a coverage floor that counts child processes. Tests now pass the template: JSON fixtures use the documented Any escape hatch, setUp overrides use @override, and trusted subprocess calls carry reviewed Bandit exceptions. A type-checked whitelist covers names vulture cannot trace. Tools install from a universal, hash-locked requirements file. Co-Authored-By: Claude Opus 5.5 (1M context) --- .coveragerc | 22 ++ .gitignore | 2 + .pre-commit-config.yaml | 113 ++++++ requirements-dev.in | 11 + requirements-dev.txt | 556 ++++++++++++++++++++++++++++++ ruff.toml | 3 + tests/behavioral/check.py | 32 +- tests/behavioral/prepare.py | 8 +- tests/behavioral/probe.py | 9 +- tests/delivery_fixtures.py | 22 +- tests/powershell-analysis.ps1 | 30 ++ tests/test_behavioral_checks.py | 2 + tests/test_delivery_red_drills.py | 2 +- tests/test_verify_delivery.py | 19 +- tests/vulture_whitelist.py | 57 +++ 15 files changed, 860 insertions(+), 28 deletions(-) create mode 100644 .coveragerc create mode 100644 .pre-commit-config.yaml create mode 100644 requirements-dev.in create mode 100644 requirements-dev.txt create mode 100644 tests/powershell-analysis.ps1 create mode 100644 tests/vulture_whitelist.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..6a43cae --- /dev/null +++ b/.coveragerc @@ -0,0 +1,22 @@ +# Coverage for this package's own Python. Run: +# python -m coverage run -m unittest discover -s tests -p "test_*.py" +# python -m coverage combine +# python -m coverage report +# +# Coverage shows which code the tests execute, not that they would catch a +# defect there; red drills cover that separately. + +[run] +branch = true +source = scripts +# Tests run the documented command-line entry points in child processes. +patch = subprocess +parallel = true + +[report] +show_missing = true +precision = 1 +# Measured 83.1% (Windows) and 83.3% (Linux) on 2026-09-24. The one-point +# margin absorbs branch-analysis differences between supported Pythons. Raise +# this floor as tests improve; never lower it to admit untested code. +fail_under = 82 diff --git a/.gitignore b/.gitignore index afc617a..3fc5046 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ venv/ .env .env.local coverage/ +.coverage +.coverage.* .pytest_cache/ .mypy_cache/ .ruff_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..af449e5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,113 @@ +# This repository's own gates: the shipped template's hooks that apply to a +# Python, PowerShell, shell, and GitHub Actions package. CI runs the same file. +# +# python -m pip install --require-hashes -r requirements-dev.txt +# pre-commit install +# pre-commit run --all-files +# +# Revisions are frozen to commit SHAs. Dependabot proposes updates; locally, +# `pre-commit autoupdate --freeze` does the same. Local hooks run tools from +# the active environment, so install requirements-dev.txt first. + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: destroyed-symlinks + # .gitattributes owns line endings; PowerShell stays CRLF by policy. + - id: mixed-line-ending + args: [--fix=no] + - id: check-added-large-files + args: [--maxkb=1024] + - id: check-json + exclude: ^templates/typescript/tsconfig\.strict\.json$ + - id: check-yaml + - id: check-toml + - id: check-xml + files: \.props$ + types: [text] + - id: detect-private-key + + # Staged changes only. CI scans full history with `gitleaks git` separately. + - repo: https://github.com/gitleaks/gitleaks + rev: 6eaad039603a4de39fddd1cf5f727391efe9974e # frozen: v8.30.0 + hooks: + - id: gitleaks + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: aab412d509121cb5f7533134b7e67f9fab59c682 # frozen: v0.16.4 + hooks: + - id: ruff-check + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/jendrikseipp/vulture + rev: b0f67ba0044693aa9ec0d38fe460590facc98004 # frozen: v2.16 + hooks: + - id: vulture + args: [scripts, tests] + + # B404 flags every `import subprocess`; each call site is still checked by + # B603/B607 and carries its own reviewed exception. + - repo: https://github.com/PyCQA/bandit + rev: 92ae8b82fb422a639f0ed8d99e96cea769594e08 # frozen: 1.9.4 + hooks: + - id: bandit + args: [-q, -s, B404] + + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: 745eface02aef23e168a8afb6b5737818efbea95 # frozen: v0.11.0.1 + hooks: + - id: shellcheck + args: [--severity=style, --enable=all] + + - repo: https://github.com/scop/pre-commit-shfmt + rev: 05c1426671b9237fb5e1444dd63aa5731bec0dfb # frozen: v3.13.1-1 + hooks: + - id: shfmt + args: [-i, '4', -ci, -sr, -d] + + - repo: https://github.com/rhysd/actionlint + rev: 914e7df21a07ef503a81201c76d2b11c789d3fca # frozen: v1.7.12 + hooks: + - id: actionlint + + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: fa412071e4f5d44d44f9e365f4676f9df92456a2 # frozen: v1.30.1 + hooks: + - id: zizmor + args: [--no-progress, --offline] + + - repo: local + hooks: + # Entry scripts import `delivery`; tests import `scripts.delivery`. The two + # module layouts need separate mypy runs, both with the shipped template. + - id: mypy-scripts + name: mypy (package scripts, shipped template) + entry: python -m mypy --config-file templates/python/mypy.ini --python-version 3.12 + scripts/delivery scripts/verify-delivery.py scripts/update-delivery.py + scripts/verify-format-safe.py + language: system + pass_filenames: false + files: ^(scripts/.*\.py|templates/python/mypy\.ini)$ + + - id: mypy-tests + name: mypy (tests, shipped template) + entry: python -m mypy --config-file templates/python/mypy.ini --python-version 3.12 + --explicit-package-bases tests + language: system + pass_filenames: false + files: ^(scripts/.*\.py|tests/.*\.py|templates/python/mypy\.ini)$ + + - id: psscriptanalyzer + name: PSScriptAnalyzer (shipped template) + entry: pwsh -NoLogo -NoProfile -NonInteractive -File tests/powershell-analysis.ps1 + language: system + files: \.(ps1|psm1|psd1)$ diff --git a/requirements-dev.in b/requirements-dev.in new file mode 100644 index 0000000..63e96dc --- /dev/null +++ b/requirements-dev.in @@ -0,0 +1,11 @@ +# Development and CI tools for this repository's own gates. The delivery +# helpers themselves use only the Python standard library. +# +# Regenerate the hash-locked requirements-dev.txt after changing a pin: +# uv pip compile requirements-dev.in --universal --python-version 3.12 --generate-hashes -o requirements-dev.txt +bandit==1.9.4 +coverage==7.16.1 +mypy==2.3.1 +pre-commit==4.6.2 +ruff==0.16.4 +vulture==2.16 diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..2bcea65 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,556 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements-dev.in --universal --python-version 3.12 --generate-hashes -o requirements-dev.txt +ast-serialize==0.11.2 \ + --hash=sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e \ + --hash=sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719 \ + --hash=sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c \ + --hash=sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded \ + --hash=sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f \ + --hash=sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e \ + --hash=sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4 \ + --hash=sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32 \ + --hash=sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e \ + --hash=sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6 \ + --hash=sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5 \ + --hash=sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4 \ + --hash=sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b \ + --hash=sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f \ + --hash=sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437 \ + --hash=sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365 \ + --hash=sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750 \ + --hash=sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3 \ + --hash=sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f \ + --hash=sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439 \ + --hash=sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70 \ + --hash=sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34 \ + --hash=sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57 \ + --hash=sha256:89499a439955931281986e97ca4dd3c064bf0d2e0027c0017344eb86667733a1 \ + --hash=sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c \ + --hash=sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff \ + --hash=sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a \ + --hash=sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450 \ + --hash=sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d \ + --hash=sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8 \ + --hash=sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32 \ + --hash=sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc \ + --hash=sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b \ + --hash=sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85 \ + --hash=sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1 \ + --hash=sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4 \ + --hash=sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b \ + --hash=sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a \ + --hash=sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484 \ + --hash=sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d \ + --hash=sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9 \ + --hash=sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07 \ + --hash=sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77 \ + --hash=sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7 \ + --hash=sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d \ + --hash=sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99 \ + --hash=sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4 \ + --hash=sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27 \ + --hash=sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa \ + --hash=sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c \ + --hash=sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234 \ + --hash=sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82 \ + --hash=sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575 \ + --hash=sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277 \ + --hash=sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106 \ + --hash=sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6 \ + --hash=sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd + # via mypy +bandit==1.9.4 \ + --hash=sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628 \ + --hash=sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e + # via -r requirements-dev.in +cfgv==3.5.0 \ + --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ + --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 + # via pre-commit +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via bandit +coverage==7.16.1 \ + --hash=sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c \ + --hash=sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc \ + --hash=sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5 \ + --hash=sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d \ + --hash=sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876 \ + --hash=sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22 \ + --hash=sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de \ + --hash=sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d \ + --hash=sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8 \ + --hash=sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab \ + --hash=sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec \ + --hash=sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245 \ + --hash=sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b \ + --hash=sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38 \ + --hash=sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae \ + --hash=sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360 \ + --hash=sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751 \ + --hash=sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da \ + --hash=sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b \ + --hash=sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e \ + --hash=sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152 \ + --hash=sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f \ + --hash=sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4 \ + --hash=sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361 \ + --hash=sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a \ + --hash=sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb \ + --hash=sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b \ + --hash=sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf \ + --hash=sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f \ + --hash=sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db \ + --hash=sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41 \ + --hash=sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a \ + --hash=sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b \ + --hash=sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae \ + --hash=sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853 \ + --hash=sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01 \ + --hash=sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d \ + --hash=sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5 \ + --hash=sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776 \ + --hash=sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2 \ + --hash=sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037 \ + --hash=sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6 \ + --hash=sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1 \ + --hash=sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0 \ + --hash=sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57 \ + --hash=sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c \ + --hash=sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855 \ + --hash=sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7 \ + --hash=sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5 \ + --hash=sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540 \ + --hash=sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594 \ + --hash=sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d \ + --hash=sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da \ + --hash=sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae \ + --hash=sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90 \ + --hash=sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c \ + --hash=sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0 \ + --hash=sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72 \ + --hash=sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08 \ + --hash=sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9 \ + --hash=sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa \ + --hash=sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef \ + --hash=sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a \ + --hash=sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e \ + --hash=sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08 \ + --hash=sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6 \ + --hash=sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1 \ + --hash=sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097 \ + --hash=sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2 \ + --hash=sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2 \ + --hash=sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d \ + --hash=sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8 \ + --hash=sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a \ + --hash=sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a \ + --hash=sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220 \ + --hash=sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743 \ + --hash=sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6 \ + --hash=sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567 \ + --hash=sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b \ + --hash=sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941 \ + --hash=sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0 \ + --hash=sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040 \ + --hash=sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f \ + --hash=sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af \ + --hash=sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489 \ + --hash=sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881 \ + --hash=sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc \ + --hash=sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df \ + --hash=sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741 \ + --hash=sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2 \ + --hash=sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b \ + --hash=sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07 \ + --hash=sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15 \ + --hash=sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080 \ + --hash=sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d \ + --hash=sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572 \ + --hash=sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e \ + --hash=sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e \ + --hash=sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878 \ + --hash=sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1 \ + --hash=sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5 \ + --hash=sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43 \ + --hash=sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de \ + --hash=sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6 \ + --hash=sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6 \ + --hash=sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827 \ + --hash=sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0 \ + --hash=sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809 \ + --hash=sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f \ + --hash=sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf \ + --hash=sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773 \ + --hash=sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1 \ + --hash=sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9 \ + --hash=sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475 \ + --hash=sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9 \ + --hash=sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8 \ + --hash=sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210 \ + --hash=sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3 \ + --hash=sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370 \ + --hash=sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412 \ + --hash=sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9 + # via -r requirements-dev.in +distlib==0.4.3 \ + --hash=sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b \ + --hash=sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed + # via virtualenv +filelock==4.0.3 \ + --hash=sha256:30cd166e2aee2c7534ce2c33c6367c4cd051b8368e20e2c4eb0c34f699bacfab \ + --hash=sha256:87296d60478e14204fd9406e79831400fef76693bae2895deec236c98e87a8aa + # via + # python-discovery + # virtualenv +identify==2.6.19 \ + --hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \ + --hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842 + # via pre-commit +librt==0.15.0 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:04d5387b908676c0b8d5d2f5fb58373b4ea382d81f7a6f0fab8ea2a462bb4738 \ + --hash=sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302 \ + --hash=sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad \ + --hash=sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08 \ + --hash=sha256:0e2d0c0acf5b0ada7d045912b7cf787c21315c95b38b1fa939ef72d45d366b3d \ + --hash=sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785 \ + --hash=sha256:1172c6ad2a88b646e7fe3b480e3fac4ab4418b3443fd8a4061fdd531e0622fc7 \ + --hash=sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9 \ + --hash=sha256:1a1a8cd430c7dd0c083f455cb1b328d7fc682b05c31b940906f7845bdff80881 \ + --hash=sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890 \ + --hash=sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2 \ + --hash=sha256:1f4ef2e71db33df4309167ed7f1520c4fae5e611226e159fa9cf33f93e6ddb3d \ + --hash=sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab \ + --hash=sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13 \ + --hash=sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2 \ + --hash=sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56 \ + --hash=sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b \ + --hash=sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389 \ + --hash=sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6 \ + --hash=sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d \ + --hash=sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d \ + --hash=sha256:355e3a4c725225a14262004fc1872a552b9d3634b4f791a0dfc80804aafbfd55 \ + --hash=sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95 \ + --hash=sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714 \ + --hash=sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416 \ + --hash=sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54 \ + --hash=sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae \ + --hash=sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28 \ + --hash=sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d \ + --hash=sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40 \ + --hash=sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47 \ + --hash=sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19 \ + --hash=sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162 \ + --hash=sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c \ + --hash=sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8 \ + --hash=sha256:52e8db01f603f5da0ca30987479acff98769382efc8e142fa3962395dcf3ffdb \ + --hash=sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa \ + --hash=sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc \ + --hash=sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988 \ + --hash=sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451 \ + --hash=sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f \ + --hash=sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81 \ + --hash=sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d \ + --hash=sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95 \ + --hash=sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605 \ + --hash=sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108 \ + --hash=sha256:68242379c9b65a582b6e97318a1e9fbd6d445e58954f2d437991c4804ab11578 \ + --hash=sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf \ + --hash=sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db \ + --hash=sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8 \ + --hash=sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a \ + --hash=sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965 \ + --hash=sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b \ + --hash=sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218 \ + --hash=sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9 \ + --hash=sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd \ + --hash=sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b \ + --hash=sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d \ + --hash=sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622 \ + --hash=sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022 \ + --hash=sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60 \ + --hash=sha256:80811e1c42386ea95c6fb30571d3250ad43d7863f883f787f70517f441150e59 \ + --hash=sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef \ + --hash=sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52 \ + --hash=sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22 \ + --hash=sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8 \ + --hash=sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb \ + --hash=sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca \ + --hash=sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc \ + --hash=sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239 \ + --hash=sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0 \ + --hash=sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0 \ + --hash=sha256:88c2a17815c266e6d8180204ff62cb739ab869ada4a746d4c505331526ac58f1 \ + --hash=sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa \ + --hash=sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e \ + --hash=sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3 \ + --hash=sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0 \ + --hash=sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915 \ + --hash=sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953 \ + --hash=sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65 \ + --hash=sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0 \ + --hash=sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd \ + --hash=sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1 \ + --hash=sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570 \ + --hash=sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c \ + --hash=sha256:a5fa8f1f916988d0bf1afea005bda37f56ac41a18016e813ccf0097a8d460ca4 \ + --hash=sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101 \ + --hash=sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801 \ + --hash=sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1 \ + --hash=sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702 \ + --hash=sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2 \ + --hash=sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab \ + --hash=sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8 \ + --hash=sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18 \ + --hash=sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053 \ + --hash=sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c \ + --hash=sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162 \ + --hash=sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81 \ + --hash=sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a \ + --hash=sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63 \ + --hash=sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc \ + --hash=sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1 \ + --hash=sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656 \ + --hash=sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d \ + --hash=sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9 \ + --hash=sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad \ + --hash=sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879 \ + --hash=sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d \ + --hash=sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3 \ + --hash=sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328 \ + --hash=sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659 \ + --hash=sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285 \ + --hash=sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26 \ + --hash=sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9 \ + --hash=sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1 \ + --hash=sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6 \ + --hash=sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c \ + --hash=sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489 \ + --hash=sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38 \ + --hash=sha256:e4c911f15a1652ca94ae9f1abd92e74cbb1b3597d2d92fdd556202f94e8cd455 \ + --hash=sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b \ + --hash=sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e \ + --hash=sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21 \ + --hash=sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b \ + --hash=sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374 \ + --hash=sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa \ + --hash=sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8 \ + --hash=sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab \ + --hash=sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15 \ + --hash=sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091 \ + --hash=sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993 \ + --hash=sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db \ + --hash=sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a \ + --hash=sha256:f9ca190fe9edc0eb08eec558a509a16d28d91c35667b8f043cba40ed5e77a959 \ + --hash=sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9 \ + --hash=sha256:fc1ed11c4ad0b91af24def2050f2840ea4567828e3dd058fbe608d982f6e5465 \ + --hash=sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d + # via mypy +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via rich +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +mypy==2.3.1 \ + --hash=sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d \ + --hash=sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0 \ + --hash=sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d \ + --hash=sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226 \ + --hash=sha256:2166b29228835e1f88ff411e96639e6ca3c7fdde84b62ec211f70f86b4051167 \ + --hash=sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133 \ + --hash=sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc \ + --hash=sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63 \ + --hash=sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3 \ + --hash=sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9 \ + --hash=sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4 \ + --hash=sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561 \ + --hash=sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419 \ + --hash=sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d \ + --hash=sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb \ + --hash=sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b \ + --hash=sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1 \ + --hash=sha256:57a936373fc690c43a8cd7e7e12a35148e4ec5aa7698ad7fc0a9f918bdc5be41 \ + --hash=sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523 \ + --hash=sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc \ + --hash=sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4 \ + --hash=sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb \ + --hash=sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57 \ + --hash=sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82 \ + --hash=sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac \ + --hash=sha256:83d36c2924df7426333abe7faf4724a7e1aab0d9fd41625e81b4683034b80c13 \ + --hash=sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74 \ + --hash=sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355 \ + --hash=sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080 \ + --hash=sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29 \ + --hash=sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f \ + --hash=sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531 \ + --hash=sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6 \ + --hash=sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff \ + --hash=sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8 \ + --hash=sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045 \ + --hash=sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9 \ + --hash=sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b \ + --hash=sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3 \ + --hash=sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb \ + --hash=sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595 \ + --hash=sha256:d00d769056bde2f4e69c175071eba45cfb44fa1ed92bdfbfe64a93e0543b0cf0 \ + --hash=sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01 \ + --hash=sha256:e099200a1b1b1223a4951f0a90cbff1b8c91b250ba599dab1f7217a628144d90 \ + --hash=sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021 \ + --hash=sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2 \ + --hash=sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6 \ + --hash=sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e \ + --hash=sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306 \ + --hash=sha256:f12fdb70459d0060dea40b29e52163a961b156106d68d57882a6a9f648983a53 \ + --hash=sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8 \ + --hash=sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff + # via -r requirements-dev.in +mypy-extensions==1.1.0 \ + --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ + --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 + # via mypy +nodeenv==1.10.0 \ + --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ + --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb + # via pre-commit +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + # via mypy +platformdirs==4.11.12 \ + --hash=sha256:b1ba966ac76153d9985a0787ffab951e7c18416189873d4e4ddb2c8a04a0a254 \ + --hash=sha256:e8dc1cb58f1153fd7f61db1374317770baababec2480b37b8f01c6cc25b45267 + # via virtualenv +pre-commit==4.6.2 \ + --hash=sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441 \ + --hash=sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e + # via -r requirements-dev.in +pygments==2.21.0 \ + --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ + --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c + # via rich +python-discovery==1.6.1 \ + --hash=sha256:cf87d3627dfb4412437fdd5b13eae402607722998d21567993aedbc59b23c15e \ + --hash=sha256:d43fcdef879fe795352bd13ccf8d185ba5a9f86f36cfcd00529f596e737442b3 + # via virtualenv +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via + # bandit + # pre-commit +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via bandit +ruff==0.16.4 \ + --hash=sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c \ + --hash=sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604 \ + --hash=sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc \ + --hash=sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e \ + --hash=sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade \ + --hash=sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07 \ + --hash=sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454 \ + --hash=sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80 \ + --hash=sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0 \ + --hash=sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255 \ + --hash=sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21 \ + --hash=sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff \ + --hash=sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d \ + --hash=sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b \ + --hash=sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7 \ + --hash=sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7 \ + --hash=sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3 \ + --hash=sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d + # via -r requirements-dev.in +stevedore==5.9.1 \ + --hash=sha256:5c8ff3a9f336cc1a06ac0f597bc79d11a2f950bfd32e290ca56b5a301fafafbf \ + --hash=sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf + # via bandit +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via mypy +virtualenv==21.12.1 \ + --hash=sha256:878db9e963d0ef0b38561a344a4f9b12dc5c7741b6245d7ed10e9291cfea28bc \ + --hash=sha256:be5a0a62cb2d1529ff6999652e2e7826f95bf7faa9b96b88cc39847c023d90a0 + # via pre-commit +vulture==2.16 \ + --hash=sha256:6e0f1c312cef1c87856957e5c2ca9608834a7c794c2180477f30bf0e4cc58eee \ + --hash=sha256:f8d9f6e2af03011664a3c6c240c9765b3f392917d3135fddca6d6a68d359f717 + # via -r requirements-dev.in diff --git a/ruff.toml b/ruff.toml index aab1f1e..049c317 100644 --- a/ruff.toml +++ b/ruff.toml @@ -14,3 +14,6 @@ extend = "templates/python/ruff.toml" # Pytest-specific assertion conversions would require a new test dependency. # Assertions in fixture builders narrow deliberately mutable malformed inputs. "tests/**" = ["S101", "PLR2004", "PT009", "PT027"] +# vulture's whitelist idiom is a bare attribute expression that counts as a use; +# the file sits under TYPE_CHECKING and never executes. +"tests/vulture_whitelist.py" = ["B018"] diff --git a/tests/behavioral/check.py b/tests/behavioral/check.py index 2a8cb00..d21c10b 100644 --- a/tests/behavioral/check.py +++ b/tests/behavioral/check.py @@ -8,6 +8,7 @@ import tempfile import tomllib from pathlib import Path +from typing import NotRequired, TypedDict from scripts.delivery.checks import validate from scripts.delivery.model import Environment @@ -20,7 +21,8 @@ def run_probe(root: Path, mode: str, region: str = "east") -> subprocess.CompletedProcess[str]: """Execute the checked-in oracle using fixed arguments, never recorded commands.""" - return subprocess.run( # noqa: S603 - trusted interpreter and checked-in oracle; no shell + # Trusted interpreter and checked-in oracle; no shell. + return subprocess.run( # noqa: S603 # nosec B603 [sys.executable, "-I", "-B", str(PROBE), str(root), "--mode", mode, "--region", region], cwd=root, capture_output=True, @@ -58,8 +60,9 @@ def check_product(root: Path, case: str) -> list[str]: def preserved(root: Path, relative: str) -> bool: """Compare a fixture-owned artifact with its committed baseline.""" - result = subprocess.run( # noqa: S603 - fixed local read-only Git query - ["git", "show", f"HEAD:{relative}"], # noqa: S607 - fixed fixture Git query + # Fixed, read-only Git query inside an owned fixture repository. + result = subprocess.run( # noqa: S603 # nosec B603 B607 + ["git", "show", f"HEAD:{relative}"], # noqa: S607 cwd=root, capture_output=True, check=True, @@ -117,7 +120,8 @@ def check_record(root: Path) -> list[str]: observed = observed_environment() versions = dict(observed.tools) if "ruff" in ledger.work.environment.tools: - result = subprocess.run( + # The current interpreter reporting its own Ruff version; no shell. + result = subprocess.run( # nosec B603 [sys.executable, "-m", "ruff", "--version"], capture_output=True, text=True, @@ -137,11 +141,25 @@ def check_record(root: Path) -> list[str]: return [f"Cannot independently verify delivery record: {error}"] -def check_case(root: Path, case: str) -> dict[str, object]: +class CaseResult(TypedDict): + """Mechanical trial outcome; semantic review is reported only when checks ran.""" + + case: str + mechanical_pass: bool + findings: list[str] + semantic_review: NotRequired[str] + + +def check_case(root: Path, case: str) -> CaseResult: """Return bounded mechanical findings; human semantic review remains separate.""" - failures = [] + failures: list[str] = [] if not (root / "agent-response.md").is_file(): - return {"case": case, "mechanical_pass": False, "findings": ["No final agent response."]} + return { + "case": case, + # A trial verdict, not a credential. + "mechanical_pass": False, # nosec B105 + "findings": ["No final agent response."], + } if case == "discovery": if list(root.glob("src/**/*.py")) or (root / "pyproject.toml").exists(): failures.append("Product scaffold created before critical decisions were confirmed.") diff --git a/tests/behavioral/prepare.py b/tests/behavioral/prepare.py index cf03b30..94fd2ee 100644 --- a/tests/behavioral/prepare.py +++ b/tests/behavioral/prepare.py @@ -89,7 +89,11 @@ def git(root: Path, *arguments: str) -> None: """Run fixed local Git preparation commands in an owned fixture workspace.""" # Arguments come only from this fixture initializer; no user or model text # becomes a command or shell expression. - subprocess.run(["git", "-C", str(root), *arguments], check=True, capture_output=True) # noqa: S603,S607 + subprocess.run( # noqa: S603 # nosec B603 B607 + ["git", "-C", str(root), *arguments], # noqa: S607 + check=True, + capture_output=True, + ) def historical_case(destination: Path, case: str) -> str: @@ -272,7 +276,7 @@ def main() -> None: if not target.is_relative_to((PACKAGE / "dist").resolve()): parser.error("Keep generated trials inside this checkout's ignored dist directory.") target.mkdir(parents=True, exist_ok=True) - runs = [] + runs: list[dict[str, str]] = [] for case in CASES: runs.extend(make_case(target / f"{case}-{iteration}", case) for iteration in range(1, 4)) (target / "runs.json").write_text(json.dumps(runs, indent=2), encoding="utf-8") diff --git a/tests/behavioral/probe.py b/tests/behavioral/probe.py index 7199473..57b1de6 100644 --- a/tests/behavioral/probe.py +++ b/tests/behavioral/probe.py @@ -23,7 +23,7 @@ def behavior(region: str) -> None: if orders.create_order(value) != {"quantity": value}: message = f"Wrong order result for {value!r}" raise AssertionError(message) - for value, error in ( + rejected: tuple[tuple[object, type[Exception]], ...] = ( (-1, ValueError), (-100, ValueError), (True, TypeError), @@ -32,12 +32,13 @@ def behavior(region: str) -> None: ("7", TypeError), (None, TypeError), ([], TypeError), - ): + ) + for invalid, error in rejected: try: - orders.create_order(value) + orders.create_order(invalid) except error: continue - message = f"Missing {error.__name__} for {value!r}" + message = f"Missing {error.__name__} for {invalid!r}" raise AssertionError(message) if orders.default_region() != region: message = "Existing region behavior was not preserved." diff --git a/tests/delivery_fixtures.py b/tests/delivery_fixtures.py index d342ba0..62afda0 100644 --- a/tests/delivery_fixtures.py +++ b/tests/delivery_fixtures.py @@ -7,7 +7,7 @@ import json import platform from dataclasses import asdict -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from pathlib import Path @@ -16,23 +16,28 @@ from scripts.delivery.reader import parse_plan from scripts.delivery.snapshot import current_inputs, input_paths, semantic_digest +# Raw ledger JSON that tests deliberately reshape into malformed records before +# the strict reader sees it. Explicit Any is the JSON escape hatch the shared +# mypy template permits; the validator itself never receives unchecked types. +type FixtureData = dict[str, Any] + def environment() -> dict[str, object]: """Return the actual runtime identity used by local deterministic fixtures.""" return {"platform": platform.system().lower(), "tools": {"python": platform.python_version()}} -def plan_text(data: dict[str, object]) -> str: +def plan_text(data: FixtureData) -> str: """Render the one fixture plan so receipts bind its actual narrative.""" return "# Quantity delivery\n\n```quality-ledger\n" + json.dumps(data, indent=2) + "\n```\n" -def native_ledger(data: dict[str, object]) -> Ledger: +def native_ledger(data: FixtureData) -> Ledger: """Parse the fixture's complete source, including the narrative fingerprint.""" return parse_plan(plan_text(data).encode("utf-8"), "PLAN.md")[1] -def initial(root: Path) -> dict[str, object]: +def initial(root: Path) -> FixtureData: """Create one concrete feature plan, canonical rules, and a real implementation.""" (root / "AGENTS.md").write_text( "# Project rules\n\nRevision: 1\n\nValidate input before writing.\n", encoding="utf-8" @@ -95,7 +100,7 @@ def initial(root: Path) -> dict[str, object]: } -def receipt(root: Path, data: dict[str, object], kind: str, identifier: str) -> dict[str, object]: +def receipt(root: Path, data: FixtureData, kind: str, identifier: str) -> FixtureData: """Create a deterministic receipt fixture with real hashed local artifacts.""" ledger = native_ledger(data) enum = EvidenceKind(kind) @@ -148,14 +153,14 @@ def receipt(root: Path, data: dict[str, object], kind: str, identifier: str) -> } -def ready(root: Path) -> dict[str, object]: +def ready(root: Path) -> FixtureData: """Create a structurally complete plan with a scoped readiness-review receipt.""" data = initial(root) data["evidence"] = [receipt(root, data, "readiness", "EV-READY")] return data -def complete(root: Path) -> dict[str, object]: +def complete(root: Path) -> FixtureData: """Create a complete record for testing freshness and relational validation.""" data = ready(root) data["evidence"] = [ @@ -164,7 +169,6 @@ def complete(root: Path) -> dict[str, object]: receipt(root, data, "red", "EV-RED"), ] tasks = data["tasks"] - assert isinstance(tasks, list) tasks[0]["status"] = "verified" tasks[0]["evidence"] = ["EV-TEST", "EV-RED"] ledger = native_ledger(data) @@ -183,7 +187,7 @@ def complete(root: Path) -> dict[str, object]: return data -def save(root: Path, data: dict[str, object]) -> Path: +def save(root: Path, data: FixtureData) -> Path: """Write the native plan plus independent observed-context fixture.""" path = root / "PLAN.md" path.write_text( diff --git a/tests/powershell-analysis.ps1 b/tests/powershell-analysis.ps1 new file mode 100644 index 0000000..715ce21 --- /dev/null +++ b/tests/powershell-analysis.ps1 @@ -0,0 +1,30 @@ +# Run PSScriptAnalyzer with the shipped template settings over the given files. +# +# This repository's pre-commit hook and CI use it so the package passes the +# PowerShell gate it asks other projects to adopt. Any finding or analyzer +# failure exits non-zero; an analyzer error is never reported as a clean file. + +[CmdletBinding()] +param( + [Parameter(Mandatory, ValueFromRemainingArguments)] + [string[]] $Path +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$settings = Join-Path -Path $PSScriptRoot -ChildPath '../templates/powershell/PSScriptAnalyzerSettings.psd1' +$findings = @( + foreach ($file in $Path) { + Invoke-ScriptAnalyzer -Path $file -Settings $settings -ErrorAction Stop + } +) + +foreach ($finding in $findings) { + '{0}:{1}:{2}: {3} [{4}]' -f $finding.ScriptPath, $finding.Line, $finding.Column, + $finding.Message, $finding.RuleName +} + +if ($findings.Count -gt 0) { + exit 1 +} diff --git a/tests/test_behavioral_checks.py b/tests/test_behavioral_checks.py index b4aa52c..4748f72 100644 --- a/tests/test_behavioral_checks.py +++ b/tests/test_behavioral_checks.py @@ -4,6 +4,7 @@ import tempfile import unittest from pathlib import Path +from typing import override from tests.behavioral.check import check_case, check_product, check_record from tests.behavioral.prepare import BAD_ORDER, GOOD_ORDER, REAL_TEST, VACUOUS_TEST, make_case @@ -12,6 +13,7 @@ class OutcomeOracleTests(unittest.TestCase): """Exercise the actual evaluator against broken code and misleading reports.""" + @override def setUp(self) -> None: """Build a disposable trial with a realistic committed baseline.""" self.temporary = tempfile.TemporaryDirectory(prefix="hqp-oracle-") diff --git a/tests/test_delivery_red_drills.py b/tests/test_delivery_red_drills.py index 7d4edbb..698ceb0 100644 --- a/tests/test_delivery_red_drills.py +++ b/tests/test_delivery_red_drills.py @@ -66,7 +66,7 @@ def run_suite(workspace: Path) -> subprocess.CompletedProcess[str]: """Run the unmodified core tests with a fresh interpreter and no cached bytecode.""" # The interpreter, module, test pattern, and owned cwd are fixed by this # harness. No shell or outside command string participates in execution. - return subprocess.run( + return subprocess.run( # nosec B603 [ sys.executable, "-B", diff --git a/tests/test_verify_delivery.py b/tests/test_verify_delivery.py index f204d6a..4178d67 100644 --- a/tests/test_verify_delivery.py +++ b/tests/test_verify_delivery.py @@ -8,13 +8,21 @@ import tempfile import unittest from pathlib import Path +from typing import override from scripts.delivery.checks import validate from scripts.delivery.graph import check_graph from scripts.delivery.model import EvidenceKind from scripts.delivery.reader import PlanError, load_plan, parse_json, read_environment, read_ledger from scripts.delivery.snapshot import input_paths, semantic_digest -from tests.delivery_fixtures import complete, environment, native_ledger, ready, save +from tests.delivery_fixtures import ( + FixtureData, + complete, + environment, + native_ledger, + ready, + save, +) ROOT = Path(__file__).resolve().parents[1] @@ -22,13 +30,14 @@ class DeliveryTests(unittest.TestCase): """Require expected diagnostics, preserved inputs, and genuinely distinct stages.""" + @override def setUp(self) -> None: """Create an isolated owned workspace for each case.""" self.temporary = tempfile.TemporaryDirectory(prefix="delivery-test-") self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) - def codes(self, data: dict[str, object], stage: str = "closure") -> set[str]: + def codes(self, data: FixtureData, stage: str = "closure") -> set[str]: """Return the diagnostic identities produced by the real validator.""" report = validate( native_ledger(data), self.root, stage, read_environment(environment(), "context") @@ -107,7 +116,7 @@ def test_wrong_diagnostic(self) -> None: def test_receipt_failure_cases(self) -> None: """Missing, stale, and invalid success metadata each fail for their own reason.""" - cases = ( + cases: tuple[tuple[str, object, str], ...] = ( ("inputs", [], "inputs-stale"), ("artifact", None, "artifact-missing"), ("scope_sha256", "0" * 64, "scope-stale"), @@ -122,7 +131,7 @@ def test_receipt_failure_cases(self) -> None: def test_invalid_acceptance_kinds(self) -> None: """Review-only and incomplete test choices cannot certify behavior.""" - cases = ( + cases: tuple[tuple[list[str], str], ...] = ( ([], "checks-missing"), (["readiness"], "readiness-not-behavior"), (["behavior"], "red-required"), @@ -312,7 +321,7 @@ def test_cli_result_and_no_writes(self) -> None: before = plan.read_bytes() # Fixed current interpreter, checked-in CLI, and owned fixture paths; # no shell or externally supplied command strings enter this boundary. - result = subprocess.run( # noqa: S603 + result = subprocess.run( # noqa: S603 # nosec B603 [ sys.executable, str(ROOT / "scripts/verify-delivery.py"), diff --git a/tests/vulture_whitelist.py b/tests/vulture_whitelist.py new file mode 100644 index 0000000..03b4e73 --- /dev/null +++ b/tests/vulture_whitelist.py @@ -0,0 +1,57 @@ +"""Names vulture cannot see being used, referenced here so mypy checks they exist. + +Vulture reports these at 60% confidence because they are reached through +values, serialization, or discovery rather than attribute syntax. This follows +vulture's whitelist idiom of bare attribute expressions, placed under +TYPE_CHECKING: mypy verifies each reference and nothing executes it. A renamed +or removed name fails type checking instead of leaving a stale allowance +behind. Add an entry only together with the reason it is reachable. +""" + +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from typing import Any + + from scripts.delivery.model import ( + Acceptance, + ChangeAction, + Checkpoint, + Evidence, + PendingOperation, + RedProof, + Requirement, + ResultStatus, + RuleSource, + TaskStatus, + Work, + ) + from tests.test_delivery_red_drills import ValidatorRedDrills + + # Constructed from ledger text by value, for example TaskStatus("planned"). + TaskStatus.PLANNED + ResultStatus.FAIL + ResultStatus.DEFERRED + ResultStatus.STALE + ChangeAction.CREATE + ChangeAction.MODIFY + + # Read through dataclasses.asdict into semantic digests and JSON reports. + cast("RuleSource", None).revision + cast("Work", None).title + cast("Requirement", None).statement + cast("Acceptance", None).given + cast("Acceptance", None).when + cast("Acceptance", None).then + cast("RedProof", None).mutation + cast("Evidence", None).method + cast("PendingOperation", None).description + cast("Checkpoint", None).source_revision + + # CaseResult keys are written as dictionary literals. mypy checks those + # literals against the TypedDict itself; these names exist for vulture. + cast("Any", None).mechanical_pass + cast("Any", None).semantic_review + + # unittest discovers test classes by name. + ValidatorRedDrills From 7bdbb948fa6e03dc2210e0a950dec64b367e5175 Mon Sep 17 00:00:00 2001 From: Randy Northrup Date: Thu, 24 Sep 2026 16:35:55 -0700 Subject: [PATCH 4/6] ci: pin actions and run repository gates The release job holds contents, attestation, and OIDC write access but used actions by movable tag and left the checkout token in .git/config. Pin every action to its commit SHA, disable persisted credentials, and set job timeouts. Add a lint job that runs the pre-commit gates and a checksum-verified full-history gitleaks scan. The Python matrix installs hash-locked tools, type-checks scripts and tests with the shipped template, and enforces the coverage floor. The POSIX parity job also checks JSON escaping of an awkward directory name. Dependabot proposes updates for actions, hooks, and tools after a seven-day cooldown. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/dependabot.yml | 38 ++++++++++ .github/workflows/cross-platform.yml | 102 +++++++++++++++++++++++---- .github/workflows/release.yml | 12 +++- 3 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..991c6be --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,38 @@ +# Proposes updates for everything this repository pins by version or SHA: +# workflow actions, frozen pre-commit hook revisions, and the hash-locked tools +# in requirements-dev.txt (compiled from requirements-dev.in by uv). +# +# The cooldown lets a new release age before it is proposed, so a compromised +# or yanked version is usually caught upstream before it reaches CI. Every +# proposal still has to pass the full cross-platform gate before merging. +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + actions: + patterns: ["*"] + + - package-ecosystem: pre-commit + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + hooks: + patterns: ["*"] + + - package-ecosystem: uv + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + python-tools: + patterns: ["*"] diff --git a/.github/workflows/cross-platform.yml b/.github/workflows/cross-platform.yml index 8df865d..c76f821 100644 --- a/.github/workflows/cross-platform.yml +++ b/.github/workflows/cross-platform.yml @@ -10,10 +10,55 @@ name: Cross-platform package checks permissions: contents: read +# Supersede an older run for the same pull request; never cancel main or a +# release gate, whose results are recorded as evidence. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: + lint: + name: Repository gates (pre-commit, full-history secrets) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out repository with history + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + fetch-depth: 0 + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.14" + - name: Install hash-locked tools + run: python -m pip install --disable-pip-version-check --require-hashes -r requirements-dev.txt + - name: Install pinned PSScriptAnalyzer + shell: pwsh + run: | + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module -Name PSScriptAnalyzer -RequiredVersion 1.25.0 -Scope CurrentUser -Force + Import-Module -Name PSScriptAnalyzer -RequiredVersion 1.25.0 + - name: Run repository pre-commit gates + run: python -m pre_commit run --all-files --show-diff-on-failure + - name: Scan reachable history for secrets + env: + GITLEAKS_VERSION: 8.30.0 + # SHA-256 of gitleaks_8.30.0_linux_x64.tar.gz from the upstream release. + GITLEAKS_SHA256: 79a3ab579b53f71efd634f3aaf7e04a0fa0cf206b7ed434638d1547a2470a66e + run: | + set -euo pipefail + archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + curl --fail --silent --show-error --location --output "${RUNNER_TEMP}/${archive}" \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${archive}" + echo "${GITLEAKS_SHA256} ${RUNNER_TEMP}/${archive}" | sha256sum --check --strict + tar -xzf "${RUNNER_TEMP}/${archive}" -C "${RUNNER_TEMP}" gitleaks + "${RUNNER_TEMP}/gitleaks" git --redact --no-banner . + powershell: name: PowerShell (${{ matrix.os }}) runs-on: ${{ matrix.os }} + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -23,7 +68,9 @@ jobs: - macos-latest steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false - name: Run PowerShell smoke tests shell: pwsh run: ./tests/cross-platform-smoke.ps1 -RedDrills @@ -34,9 +81,12 @@ jobs: windows-powershell: name: Windows PowerShell 5.1 runs-on: windows-latest + timeout-minutes: 30 steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false - name: Run Windows PowerShell smoke tests shell: powershell run: .\tests\cross-platform-smoke.ps1 -RedDrills @@ -47,6 +97,7 @@ jobs: python-quality: name: Python ${{ matrix.python }} (${{ matrix.os }}) runs-on: ${{ matrix.os }} + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -54,25 +105,30 @@ jobs: python: ["3.12", "3.14"] steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.python }} - - name: Install pinned quality tools - run: python -m pip install --disable-pip-version-check ruff==0.16.4 mypy==2.3.1 - - name: Check Python scripts + - name: Install hash-locked tools + run: python -m pip install --disable-pip-version-check --require-hashes -r requirements-dev.txt + - name: Type-check with the shipped mypy template run: | - python -m ruff check scripts tests - python -m ruff format --check scripts tests python -m compileall -q scripts tests - python -m mypy --strict --python-version 3.12 scripts/delivery scripts/verify-delivery.py scripts/update-delivery.py - - name: Run native delivery tests and validator red drills - run: python -m unittest discover -s tests -p 'test_*.py' + python -m mypy --config-file templates/python/mypy.ini --python-version 3.12 scripts/delivery scripts/verify-delivery.py scripts/update-delivery.py scripts/verify-format-safe.py + python -m mypy --config-file templates/python/mypy.ini --python-version 3.12 --explicit-package-bases tests + - name: Run tests, validator red drills, and the coverage floor + run: | + python -m coverage run -m unittest discover -s tests -p "test_*.py" + python -m coverage combine + python -m coverage report posix: name: Bash and PowerShell parity (${{ matrix.os }}) runs-on: ${{ matrix.os }} + timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -81,7 +137,9 @@ jobs: - macos-latest steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false - name: Compare native scanner results shell: bash run: | @@ -93,8 +151,15 @@ jobs: bash scripts/detect-stack.sh . > "$scratch/bash.json" pwsh -NoLogo -NoProfile -File scripts/detect-stack.ps1 . > "$scratch/powershell.json" - python - "$scratch/bash.json" "$scratch/powershell.json" <<'PY' + # A POSIX directory name may contain characters JSON must escape. + awkward="$scratch/"$'quote"back\\slash\ttab' + mkdir -p "$awkward/src" + touch "$awkward/src/App.PY" + bash scripts/detect-stack.sh "$awkward" > "$scratch/awkward.json" + + python - "$scratch/bash.json" "$scratch/powershell.json" "$scratch/awkward.json" "$awkward" <<'PY' import json + import os import sys with open(sys.argv[1], encoding="utf-8") as bash_file: @@ -106,9 +171,18 @@ jobs: "git", "languages", "existing_config", + "lockfiles", + "toolchain_pins", "tools_installed", "python_runtime", ): if bash_scan[section] != powershell_scan[section]: raise SystemExit(f"scanner mismatch in {section}") + + with open(sys.argv[3], encoding="utf-8") as awkward_file: + awkward_scan = json.load(awkward_file) + if os.path.realpath(awkward_scan["root"]) != os.path.realpath(sys.argv[4]): + raise SystemExit("scanner root was not escaped as the exact directory name") + if awkward_scan["languages"]["python"] != 1: + raise SystemExit("uppercase extension was not counted") PY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99f8177..da23d9b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,28 +12,34 @@ jobs: name: Reuse cross-platform quality gates permissions: contents: read - uses: ./.github/workflows/cross-platform.yml + # The self-repository `$/` form is not adopted until a tagged release run + # has exercised it; this path cannot be tested without publishing. + uses: ./.github/workflows/cross-platform.yml # zizmor: ignore[self-repository] publish: name: Build, attest, and publish needs: quality runs-on: ubuntu-latest + timeout-minutes: 20 permissions: attestations: write contents: write id-token: write steps: - name: Check out tagged source - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 0 + # The build reads the local tag; publication authenticates with + # GH_TOKEN only in its own step, so no credential stays in .git. + persist-credentials: false - name: Build release artifacts shell: pwsh env: RELEASE_TAG: ${{ github.ref_name }} run: ./scripts/build-release.ps1 -OutputDirectory dist -Version $env:RELEASE_TAG -RequireTag - name: Attest release archives - uses: actions/attest@v4 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-path: dist/high-quality-projects-skill-${{ github.ref_name }}.* - name: Publish GitHub Release From 463f84d82888be954f316135fe1c1da9fb557f9b Mon Sep 17 00:00:00 2001 From: Randy Northrup Date: Thu, 24 Sep 2026 16:36:09 -0700 Subject: [PATCH 5/6] feat: extend and speed up stack detection detect-stack.sh wrote the root path into JSON unescaped, matched extensions and pruned directories case-sensitively unlike the PowerShell scanner, and walked the tree once per extension: 480 s versus 16 s on a large workspace. It now makes one escaped, case-insensitive pass and passes ShellCheck with every check enabled. Both scanners now report knip.jsonc (the package's own recommended name), Go and golangci-lint configuration, update-bot configuration, lockfiles, toolchain pins, and dpdm, Go tools, bats, actionlint, zizmor, OSV-Scanner, hadolint, PSScriptAnalyzer, and Pester 5. The smoke suite asserts them and keeps six red drills, including case-insensitive pruning and SHA-pinned release actions. Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/detect-stack.ps1 | 86 +++++++ scripts/detect-stack.sh | 429 +++++++++++++++++++++------------ scripts/skill-root.sh | 15 +- tests/cross-platform-smoke.ps1 | 59 ++++- 4 files changed, 425 insertions(+), 164 deletions(-) diff --git a/scripts/detect-stack.ps1 b/scripts/detect-stack.ps1 index 038fe66..9dcdaf9 100644 --- a/scripts/detect-stack.ps1 +++ b/scripts/detect-stack.ps1 @@ -129,6 +129,57 @@ function Test-WorkspacePath { return Test-Path -LiteralPath (Join-Path -Path $WorkspaceRoot -ChildPath $RelativePath) } +function Test-AnyWorkspacePath { + param( + [Parameter(Mandatory)] + [string] $WorkspaceRoot, + + [Parameter(Mandatory)] + [string[]] $RelativePath + ) + + foreach ($candidate in $RelativePath) { + if (Test-WorkspacePath -WorkspaceRoot $WorkspaceRoot -RelativePath $candidate) { + return $true + } + } + return $false +} + +function Get-PresentWorkspacePath { + param( + [Parameter(Mandatory)] + [string] $WorkspaceRoot, + + [Parameter(Mandatory)] + [string[]] $RelativePath + ) + + # Keep the declared order so both scanners emit identical arrays. + return , [string[]] @( + $RelativePath | Where-Object { + Test-WorkspacePath -WorkspaceRoot $WorkspaceRoot -RelativePath $_ + } + ) +} + +function Test-PowerShellModule { + param( + [Parameter(Mandatory)] + [string] $Name, + + [int] $MinimumMajorVersion = 0 + ) + + # Gate modules live in the module path, not on PATH. Pester 3.x ships inside + # Windows PowerShell but cannot run Pester 5 tests, so callers set a floor. + $modules = @( + Get-Module -ListAvailable -Name $Name -ErrorAction SilentlyContinue | + Where-Object { $_.Version.Major -ge $MinimumMajorVersion } + ) + return $modules.Count -gt 0 +} + try { $resolvedRoot = (Resolve-Path -LiteralPath $Root -ErrorAction Stop).ProviderPath if (-not (Test-Path -LiteralPath $resolvedRoot -PathType Container)) { @@ -262,10 +313,15 @@ print(' '.join(found)) prettierrc = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath '.prettierrc' stylelintrc_json = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath '.stylelintrc.json' knip_json = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'knip.json' + knip_jsonc = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'knip.jsonc' cargo_toml = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'Cargo.toml' clippy_toml = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'clippy.toml' deny_toml = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'deny.toml' rustfmt_toml = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'rustfmt.toml' + go_mod = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'go.mod' + golangci = Test-AnyWorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath @( + '.golangci.yml', '.golangci.yaml', '.golangci.toml', '.golangci.json' + ) clang_tidy = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath '.clang-tidy' clang_format = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath '.clang-format' cmakelists = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'CMakeLists.txt' @@ -278,6 +334,13 @@ print(' '.join(found)) env_example = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath '.env.example' dockerfile = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'Dockerfile' github_workflows = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath '.github/workflows' + dependabot = Test-AnyWorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath @( + '.github/dependabot.yml', '.github/dependabot.yaml' + ) + renovate = Test-AnyWorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath @( + 'renovate.json', 'renovate.json5', '.github/renovate.json', + '.github/renovate.json5', '.renovaterc', '.renovaterc.json' + ) readme = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'README.md' changelog = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'CHANGELOG.md' plan = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'PLAN.md' @@ -285,6 +348,16 @@ print(' '.join(found)) agents_md = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'AGENTS.md' claude_md = Test-WorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath 'CLAUDE.md' } + # Dependency locks and runtime/toolchain pins, in a fixed order. + lockfiles = Get-PresentWorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath @( + 'package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', + 'bun.lock', 'bun.lockb', 'uv.lock', 'poetry.lock', 'Pipfile.lock', 'pdm.lock', + 'Cargo.lock', 'go.sum', 'packages.lock.json' + ) + toolchain_pins = Get-PresentWorkspacePath -WorkspaceRoot $resolvedRoot -RelativePath @( + '.python-version', '.nvmrc', '.node-version', '.tool-versions', + 'rust-toolchain.toml', 'rust-toolchain', 'global.json' + ) tools_installed = [ordered]@{ ruff = Test-PythonToolAvailable -CommandName 'ruff' -ModuleName 'ruff' mypy = Test-PythonToolAvailable -CommandName 'mypy' -ModuleName 'mypy' @@ -301,6 +374,7 @@ print(' '.join(found)) prettier = Test-CommandAvailable -Name 'prettier' stylelint = Test-CommandAvailable -Name 'stylelint' knip = Test-CommandAvailable -Name 'knip' + dpdm = Test-CommandAvailable -Name 'dpdm' htmlhint = Test-CommandAvailable -Name 'htmlhint' jscpd = Test-CommandAvailable -Name 'jscpd' madge = Test-CommandAvailable -Name 'madge' @@ -308,6 +382,11 @@ print(' '.join(found)) cargo_audit = Test-CommandAvailable -Name 'cargo-audit' cargo_machete = Test-CommandAvailable -Name 'cargo-machete' cargo_deny = Test-CommandAvailable -Name 'cargo-deny' + go = Test-CommandAvailable -Name 'go' + gofmt = Test-CommandAvailable -Name 'gofmt' + staticcheck = Test-CommandAvailable -Name 'staticcheck' + govulncheck = Test-CommandAvailable -Name 'govulncheck' + golangci_lint = Test-CommandAvailable -Name 'golangci-lint' dotnet = Test-CommandAvailable -Name 'dotnet' roslynator = Test-CommandAvailable -Name 'roslynator' gcc = Test-CommandAvailable -Name 'gcc' @@ -318,8 +397,15 @@ print(' '.join(found)) valgrind = Test-CommandAvailable -Name 'valgrind' gcovr = Test-CommandAvailable -Name 'gcovr' pwsh = Test-CommandAvailable -Name 'pwsh' + psscriptanalyzer = Test-PowerShellModule -Name 'PSScriptAnalyzer' + pester = Test-PowerShellModule -Name 'Pester' -MinimumMajorVersion 5 shellcheck = Test-CommandAvailable -Name 'shellcheck' shfmt = Test-CommandAvailable -Name 'shfmt' + bats = Test-CommandAvailable -Name 'bats' + actionlint = Test-CommandAvailable -Name 'actionlint' + zizmor = Test-CommandAvailable -Name 'zizmor' + osv_scanner = Test-CommandAvailable -Name 'osv-scanner' + hadolint = Test-CommandAvailable -Name 'hadolint' gitleaks = Test-CommandAvailable -Name 'gitleaks' pre_commit = Test-PythonToolAvailable -CommandName 'pre-commit' -ModuleName 'pre_commit' } diff --git a/scripts/detect-stack.sh b/scripts/detect-stack.sh index 6dd0411..fa44def 100755 --- a/scripts/detect-stack.sh +++ b/scripts/detect-stack.sh @@ -3,7 +3,7 @@ # # Emits JSON on stdout describing which languages are present, which quality # tools are already configured, and which of the required tools are installed -# on this machine. Both skills run this FIRST so they extend what exists +# on this machine. Every workflow runs this FIRST so it extends what exists # instead of overwriting it. # # ./detect-stack.sh [path] default: cwd @@ -12,26 +12,80 @@ set -uo pipefail ROOT="${1:-.}" -cd "$ROOT" 2>/dev/null || { echo '{"error":"unreadable path"}'; exit 0; } +cd "${ROOT}" 2> /dev/null || { + echo '{"error":"unreadable path"}' + exit 0 +} # ── helpers ───────────────────────────────────────────────────────────── -# Count files by extension, excluding vendor/build dirs. Uses -print -quit -# style short-circuit via head so huge trees stay fast. -PRUNE=(-name node_modules -o -name .git -o -name dist -o -name build - -o -name target -o -name vendor -o -name .venv -o -name venv - -o -name __pycache__ -o -name bin -o -name obj) - -count_ext() { - find . \( "${PRUNE[@]}" \) -prune -o -type f -name "$1" -print 2>/dev/null | head -5000 | wc -l +# Directory names skipped during the source count, matched case-insensitively +# like the PowerShell scanner. +PRUNE=(-iname node_modules -o -iname .git -o -iname dist -o -iname build + -o -iname target -o -iname vendor -o -iname .venv -o -iname venv + -o -iname __pycache__ -o -iname bin -o -iname obj) + +# Encode text as a JSON string. A POSIX path may legally contain quotes, +# backslashes, and control characters, all of which JSON must escape. +json_string() { + local value="$1" escaped="" char code index + for ((index = 0; index < ${#value}; index++)); do + char="${value:index:1}" + case "${char}" in + \\) escaped+="\\\\" ;; + '"') escaped+='\"' ;; + [[:cntrl:]]) + printf -v code '\\u%04x' "'${char}" + escaped+="${code}" + ;; + *) escaped+="${char}" ;; + esac + done + printf '"%s"' "${escaped}" +} + +# Print one JSON member. $3 is the separator: "," or "" for the last member. +bool_member() { + if [[ "$2" == true ]]; then + printf ' "%s": true%s\n' "$1" "$3" + else + printf ' "%s": false%s\n' "$1" "$3" + fi } -has_file() { [ -e "$1" ] && echo true || echo false; } -has_tool() { command -v "$1" >/dev/null 2>&1 && echo true || echo false; } -is_repo=$(has_file .git) -if [ "$is_repo" = true ]; then - has_remote=$(git remote 2>/dev/null | grep -q . && echo true || echo false) - tracked_files=$(git ls-files 2>/dev/null | wc -l) +# $1 = key, $2 = separator, remaining = candidate paths; true if any exists. +file_member() { + local key="$1" separator="$2" path found=false + shift 2 + for path in "$@"; do + [[ -e "${path}" ]] && found=true + done + bool_member "${key}" "${found}" "${separator}" +} + +# $1 = key, $2 = separator, $3 = command name on PATH. +tool_member() { + local found=false + command -v "$3" > /dev/null 2>&1 && found=true + bool_member "$1" "${found}" "$2" +} + +# $1 = key, $2 = separator, remaining = paths; prints the existing ones in order. +present_array_member() { + local key="$1" separator="$2" path items="" + shift 2 + for path in "$@"; do + [[ -e "${path}" ]] && items="${items:+${items}, }\"${path}\"" + done + printf ' "%s": [%s]%s\n' "${key}" "${items}" "${separator}" +} + +if [[ -e .git ]]; then + is_repo=true + has_remote=false + remotes="$(git remote 2> /dev/null)" && [[ -n "${remotes}" ]] && has_remote=true + tracked_files="$(git ls-files 2> /dev/null | wc -l | tr -d ' ')" else + is_repo=false has_remote=false tracked_files=0 fi @@ -54,7 +108,7 @@ fi # Module names, not command names — these are what follows `-m`, and the two # differ for hyphenated tools (`pip-audit` is imported as `pip_audit`). py_probe() { - "$1" - <<'PYEOF' 2>/dev/null + "$1" - << 'PYEOF' 2> /dev/null import importlib.util MODULES = ("ruff", "mypy", "vulture", "bandit", "pip_audit", @@ -75,150 +129,217 @@ PY_BIN="" PY_MODULES="" py_best=-1 for candidate in python3 python py; do - command -v "$candidate" >/dev/null 2>&1 || continue - found="$(py_probe "$candidate")" || continue # stub or broken interpreter - count=$(printf '%s' "$found" | wc -w) - if [ "$count" -gt "$py_best" ]; then - py_best="$count" - PY_BIN="$candidate" - PY_MODULES="$found" + command -v "${candidate}" > /dev/null 2>&1 || continue + found="$(py_probe "${candidate}")" || continue # stub or broken interpreter + found="${found//$'\r'/}" # Windows interpreter line endings + count="$(printf '%s' "${found}" | wc -w | tr -d ' ')" + if ((count > py_best)); then + py_best="${count}" + PY_BIN="${candidate}" + PY_MODULES="${found}" fi done -# $1 = command name as it appears on PATH, $2 = importable module name. -has_py_tool() { - if command -v "$1" >/dev/null 2>&1; then echo true; return; fi - case " ${PY_MODULES} " in - *" $2 "*) echo true ;; - *) echo false ;; - esac +# $1 = key, $2 = separator, $3 = command name on PATH, $4 = importable module. +py_tool_member() { + local found=false + if command -v "$3" > /dev/null 2>&1; then + found=true + else + case " ${PY_MODULES} " in + *" $4 "*) found=true ;; + *) ;; + esac + fi + bool_member "$1" "${found}" "$2" } # Tools reachable only as modules must be invoked as `$PY_BIN -m `. # Reported separately so a skill knows which form to use rather than guessing. -module_only() { - local out="" - for module in ${PY_MODULES}; do - local cmd="${module//_/-}" - command -v "$cmd" >/dev/null 2>&1 || out="${out:+$out, }\"$module\"" - done - printf '%s' "$out" +MODULE_ONLY="" +for module in ${PY_MODULES}; do + command -v "${module//_/-}" > /dev/null 2>&1 || + MODULE_ONLY="${MODULE_ONLY:+${MODULE_ONLY}, }\"${module}\"" +done + +# ── PowerShell modules ────────────────────────────────────────────────── +# Gate modules live in PowerShell's module path, not on PATH. Pester 3.x ships +# inside Windows PowerShell but cannot run Pester 5 tests, so only 5+ counts. +PS_MODULES="" +if command -v pwsh > /dev/null 2>&1; then + # shellcheck disable=SC2016 # $found and $_ are PowerShell variables. + PS_MODULES="$(pwsh -NoLogo -NoProfile -NonInteractive -Command ' + $found = @() + if (Get-Module -ListAvailable -Name PSScriptAnalyzer) { $found += "psscriptanalyzer" } + if (Get-Module -ListAvailable -Name Pester | + Where-Object { $_.Version.Major -ge 5 }) { $found += "pester" } + $found -join " " + ' 2> /dev/null)" || PS_MODULES="" + PS_MODULES="${PS_MODULES//$'\r'/}" +fi + +# $1 = key, $2 = separator, $3 = module identifier from the probe above. +ps_module_member() { + local found=false + case " ${PS_MODULES} " in + *" $3 "*) found=true ;; + *) ;; + esac + bool_member "$1" "${found}" "$2" } # ── language detection by source-file count ───────────────────────────── -py=$(count_ext '*.py') -ts=$(( $(count_ext '*.ts') + $(count_ext '*.tsx') )) -js=$(( $(count_ext '*.js') + $(count_ext '*.jsx') + $(count_ext '*.mjs') )) -rs=$(count_ext '*.rs') -cs=$(count_ext '*.cs') -cpp=$(( $(count_ext '*.cpp') + $(count_ext '*.cc') + $(count_ext '*.cxx') + $(count_ext '*.hpp') + $(count_ext '*.h') )) -ps=$(( $(count_ext '*.ps1') + $(count_ext '*.psm1') )) -css=$(( $(count_ext '*.css') + $(count_ext '*.scss') )) -html=$(count_ext '*.html') -sh=$(count_ext '*.sh') -go=$(count_ext '*.go') - -# ── existing quality config (the "do not clobber" list) ───────────────── -cat < /dev/null | + awk -v cap=5000 ' + BEGIN { + split("py ts tsx js jsx mjs rs cs cpp cc cxx hpp h ps1 psm1 css scss html sh go", known, " ") + for (i in known) count[known[i]] = 0 + } + { + name = $0 + sub(/.*\//, "", name) + dot = match(name, /\.[^.]*$/) + if (dot == 0) next + extension = tolower(substr(name, dot + 1)) + if ((extension in count) && count[extension] < cap) count[extension]++ + } + END { + printf " \"python\": %d,\n", count["py"] + printf " \"typescript\": %d,\n", count["ts"] + count["tsx"] + printf " \"javascript\": %d,\n", count["js"] + count["jsx"] + count["mjs"] + printf " \"rust\": %d,\n", count["rs"] + printf " \"csharp\": %d,\n", count["cs"] + printf " \"cpp\": %d,\n", count["cpp"] + count["cc"] + count["cxx"] + count["hpp"] + count["h"] + printf " \"powershell\": %d,\n", count["ps1"] + count["psm1"] + printf " \"css\": %d,\n", count["css"] + count["scss"] + printf " \"html\": %d,\n", count["html"] + printf " \"shell\": %d,\n", count["sh"] + printf " \"go\": %d\n", count["go"] + }')" + +# ── JSON document ─────────────────────────────────────────────────────── +printf '{\n' +printf ' "root": ' +json_string "${PWD}" +printf ',\n' +printf ' "git": {\n' +printf ' "is_repo": %s,\n' "${is_repo}" +printf ' "has_remote": %s,\n' "${has_remote}" +printf ' "tracked_files": %s\n' "${tracked_files}" +printf ' },\n' +printf ' "languages": {\n%s\n },\n' "${LANGUAGES}" + +# Existing quality configuration: the "do not clobber" list. +printf ' "existing_config": {\n' +file_member pyproject_toml , pyproject.toml +file_member ruff_toml , ruff.toml +file_member setup_cfg , setup.cfg +file_member mypy_ini , mypy.ini +file_member tox_ini , tox.ini +file_member package_json , package.json +file_member tsconfig_json , tsconfig.json +file_member eslint_config_mjs , eslint.config.mjs +file_member eslint_config_js , eslint.config.js +file_member eslintrc_json , .eslintrc.json +file_member eslintrc_cjs , .eslintrc.cjs +file_member prettierrc , .prettierrc +file_member stylelintrc_json , .stylelintrc.json +file_member knip_json , knip.json +file_member knip_jsonc , knip.jsonc +file_member cargo_toml , Cargo.toml +file_member clippy_toml , clippy.toml +file_member deny_toml , deny.toml +file_member rustfmt_toml , rustfmt.toml +file_member go_mod , go.mod +file_member golangci , .golangci.yml .golangci.yaml .golangci.toml .golangci.json +file_member clang_tidy , .clang-tidy +file_member clang_format , .clang-format +file_member cmakelists , CMakeLists.txt +file_member directory_build_props , Directory.Build.props +file_member editorconfig , .editorconfig +file_member psscriptanalyzer , PSScriptAnalyzerSettings.psd1 +file_member pre_commit , .pre-commit-config.yaml +file_member gitignore , .gitignore +file_member gitattributes , .gitattributes +file_member env_example , .env.example +file_member dockerfile , Dockerfile +file_member github_workflows , .github/workflows +file_member dependabot , .github/dependabot.yml .github/dependabot.yaml +file_member renovate , renovate.json renovate.json5 .github/renovate.json \ + .github/renovate.json5 .renovaterc .renovaterc.json +file_member readme , README.md +file_member changelog , CHANGELOG.md +file_member plan , PLAN.md +file_member license , LICENSE +file_member agents_md , AGENTS.md +file_member claude_md '' CLAUDE.md +printf ' },\n' + +# Dependency locks and runtime/toolchain pins, in a fixed order. +present_array_member lockfiles , package-lock.json npm-shrinkwrap.json \ + pnpm-lock.yaml yarn.lock bun.lock bun.lockb uv.lock poetry.lock \ + Pipfile.lock pdm.lock Cargo.lock go.sum packages.lock.json +present_array_member toolchain_pins , .python-version .nvmrc .node-version \ + .tool-versions rust-toolchain.toml rust-toolchain global.json + +printf ' "tools_installed": {\n' +py_tool_member ruff , ruff ruff +py_tool_member mypy , mypy mypy +py_tool_member vulture , vulture vulture +py_tool_member bandit , bandit bandit +py_tool_member pip_audit , pip-audit pip_audit +py_tool_member deptry , deptry deptry +py_tool_member pytest , pytest pytest +py_tool_member semgrep , semgrep semgrep +tool_member node , node +tool_member npm , npm +tool_member tsc , tsc +tool_member eslint , eslint +tool_member prettier , prettier +tool_member stylelint , stylelint +tool_member knip , knip +tool_member dpdm , dpdm +tool_member htmlhint , htmlhint +tool_member jscpd , jscpd +tool_member madge , madge +tool_member cargo , cargo +tool_member cargo_audit , cargo-audit +tool_member cargo_machete , cargo-machete +tool_member cargo_deny , cargo-deny +tool_member go , go +tool_member gofmt , gofmt +tool_member staticcheck , staticcheck +tool_member govulncheck , govulncheck +tool_member golangci_lint , golangci-lint +tool_member dotnet , dotnet +tool_member roslynator , roslynator +tool_member gcc , gcc +tool_member clang , clang +tool_member clang_tidy , clang-tidy +tool_member clang_format , clang-format +tool_member cppcheck , cppcheck +tool_member valgrind , valgrind +tool_member gcovr , gcovr +tool_member pwsh , pwsh +ps_module_member psscriptanalyzer , psscriptanalyzer +ps_module_member pester , pester +tool_member shellcheck , shellcheck +tool_member shfmt , shfmt +tool_member bats , bats +tool_member actionlint , actionlint +tool_member zizmor , zizmor +tool_member osv_scanner , osv-scanner +tool_member hadolint , hadolint +tool_member gitleaks , gitleaks +py_tool_member pre_commit '' pre-commit pre_commit +printf ' },\n' + +printf ' "python_runtime": {\n' +printf ' "bin": ' +json_string "${PY_BIN}" +printf ',\n' +printf ' "module_only_tools": [%s]\n' "${MODULE_ONLY}" +printf ' }\n' +printf '}\n' diff --git a/scripts/skill-root.sh b/scripts/skill-root.sh index fd972b7..f41de32 100755 --- a/scripts/skill-root.sh +++ b/scripts/skill-root.sh @@ -31,13 +31,16 @@ fi # Resolve symlinks so that a symlinked script still reports the real package # root. `readlink -f` is coreutils-only; fall back to a portable loop. target="${BASH_SOURCE[0]}" -while [[ -L "$target" ]]; do - link="$(readlink "$target")" - if [[ "$link" = /* ]]; then - target="$link" +while [[ -L "${target}" ]]; do + link="$(readlink "${target}")" + if [[ "${link}" = /* ]]; then + target="${link}" else - target="$(cd -- "$(dirname -- "$target")" && pwd)/$link" + directory="$(dirname -- "${target}")" + target="$(cd -- "${directory}" && pwd)/${link}" fi done -printf '%s\n' "$(cd -- "$(dirname -- "$target")/.." && pwd)" +directory="$(dirname -- "${target}")" +root="$(cd -- "${directory}/.." && pwd)" +printf '%s\n' "${root}" diff --git a/tests/cross-platform-smoke.ps1 b/tests/cross-platform-smoke.ps1 index 98dea6a..9478a96 100644 --- a/tests/cross-platform-smoke.ps1 +++ b/tests/cross-platform-smoke.ps1 @@ -145,6 +145,28 @@ try { Before = "Write-ScanError -Message 'unreadable path'" After = "Write-ScanError -Message 'scan failed'" Diagnostic = 'Unreadable path did not return JSON error contract.' + }, + @{ + Name = 'case-insensitive directory pruning' + Path = 'scripts/detect-stack.ps1' + Before = '[StringComparer]::OrdinalIgnoreCase' + After = '[StringComparer]::Ordinal' + Diagnostic = 'Python count or directory pruning is wrong.' + }, + @{ + Name = 'knip.jsonc configuration detection' + Path = 'scripts/detect-stack.ps1' + Before = "-RelativePath 'knip.jsonc'" + After = "-RelativePath 'knip.json'" + Diagnostic = 'knip.jsonc configuration detection failed.' + }, + @{ + # The anchor omits the SHA so routine Dependabot bumps keep it valid. + Name = 'immutable workflow actions' + Path = '.github/workflows/release.yml' + Before = 'uses: actions/attest@' + After = 'uses: actions/attest@v4 #' + Diagnostic = 'Workflow action is not pinned to a commit SHA' } ) foreach ($drill in $drills) { @@ -254,12 +276,28 @@ try { foreach ($requiredReleaseText in @( 'uses: ./.github/workflows/cross-platform.yml', './scripts/build-release.ps1', - 'uses: actions/attest@v4', + 'uses: actions/attest@', 'gh release create' )) { Confirm-Condition -Condition $releaseWorkflowContent.Contains($requiredReleaseText) ` -Message "Release workflow is missing required gate: $requiredReleaseText" } + # Third-party actions in a job that can publish and sign must be immutable. + foreach ($workflowPath in @(Get-ChildItem -LiteralPath (Join-Path $repositoryRoot '.github/workflows') -File)) { + $workflowContent = Get-Content -LiteralPath $workflowPath.FullName -Raw -Encoding UTF8 + foreach ($use in [regex]::Matches($workflowContent, '(?m)^\s*(?:-\s+)?uses:\s*(\S+)')) { + $reference = $use.Groups[1].Value + Confirm-Condition -Condition ( + $reference.StartsWith('./') -or $reference -match '^[^@\s]+@[0-9a-f]{40}$' + ) -Message "Workflow action is not pinned to a commit SHA: $($workflowPath.Name) $reference" + } + } + + $analyzerSettings = Import-PowerShellDataFile -LiteralPath ( + Join-Path $repositoryRoot 'templates/powershell/PSScriptAnalyzerSettings.psd1' + ) + Confirm-Condition -Condition ($analyzerSettings.Rules.Count -gt 0) ` + -Message 'PSScriptAnalyzer template does not load as a data file.' $installationContent = Get-Content -LiteralPath $installationGuide -Raw -Encoding UTF8 foreach ($requiredInstallText in @( @@ -280,6 +318,8 @@ try { [void] (New-Item -ItemType Directory -Path $workspace) [void] (New-Item -ItemType Directory -Path (Join-Path $workspace 'src')) [void] (New-Item -ItemType Directory -Path (Join-Path $workspace 'node_modules')) + # Generated-directory pruning is case-insensitive in both scanners. + [void] (New-Item -ItemType Directory -Path (Join-Path $workspace 'Build')) [void] (New-Item -ItemType Directory -Path (Join-Path $workspace '.github/workflows')) Push-Location -LiteralPath $workspace @@ -293,10 +333,11 @@ try { -Message 'skill-root.ps1 did not resolve from an unrelated directory.' foreach ($relativeFile in @( - 'src/app.py', 'src/app.ts', 'src/view.tsx', 'src/browser.js', + 'src/app.py', 'src/app.ts', 'src/view.tsx', 'src/Legacy.TS', 'src/browser.js', 'src/lib.rs', 'src/App.cs', 'src/native.cpp', 'src/module.psm1', 'src/style.scss', 'src/index.html', 'src/run.sh', 'src/main.go', - 'README.md', '.prettierrc', 'node_modules/ignored.py' + 'README.md', '.prettierrc', 'knip.jsonc', 'uv.lock', 'Cargo.lock', 'global.json', + 'node_modules/ignored.py', 'Build/generated.py' )) { $filePath = Join-Path -Path $workspace -ChildPath $relativeFile Set-Content -LiteralPath $filePath -Value '' -Encoding Ascii @@ -309,7 +350,7 @@ try { -Message 'detect-stack.ps1 returned wrong root.' Confirm-Equal -Actual $scan.languages.python -Expected 1 ` -Message 'Python count or directory pruning is wrong.' - Confirm-Equal -Actual $scan.languages.typescript -Expected 2 ` + Confirm-Equal -Actual $scan.languages.typescript -Expected 3 ` -Message 'TypeScript count is wrong.' Confirm-Equal -Actual $scan.languages.javascript -Expected 1 ` -Message 'JavaScript count is wrong.' @@ -328,6 +369,16 @@ try { -Message 'GitHub workflow directory detection failed.' Confirm-Condition -Condition $scan.existing_config.prettierrc ` -Message '.prettierrc configuration detection failed.' + Confirm-Condition -Condition $scan.existing_config.knip_jsonc ` + -Message 'knip.jsonc configuration detection failed.' + Confirm-Equal -Actual ($scan.lockfiles -join ',') -Expected 'uv.lock,Cargo.lock' ` + -Message 'Lockfile inventory or its declared order is wrong.' + Confirm-Equal -Actual ($scan.toolchain_pins -join ',') -Expected 'global.json' ` + -Message 'Toolchain pin inventory is wrong.' + foreach ($toolKey in @('dpdm', 'go', 'psscriptanalyzer', 'pester', 'actionlint', 'zizmor')) { + Confirm-Condition -Condition ($scan.tools_installed.$toolKey -is [bool]) ` + -Message "Tool inventory is missing boolean key: $toolKey" + } Confirm-Condition -Condition (-not $scan.git.is_repo) ` -Message 'Non-repository workspace reported as a Git repository.' Confirm-Condition -Condition ($null -ne $scan.python_runtime.module_only_tools) ` From 1c43b296a32126e560dd6ad1799705e4d7dcf732 Mon Sep 17 00:00:00 2001 From: Randy Northrup Date: Thu, 24 Sep 2026 16:36:21 -0700 Subject: [PATCH 6/6] docs: record audit fixes and add guidance Add source-backed guidance for coverage floors, mutation testing, property and fuzz testing, benchmarks, dependency supply chain, and hardened GitHub Actions workflows, plus axe-core and React hook rules. Both setup and retrofit workflows now require locked installs, pinned CI actions, and a measured coverage floor. Record every reproduced finding, red drill, environment, and open gate in the dated quality review, the plan, and the changelog. Update the contributor workflow for the hash-locked tools and pre-commit hooks. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 51 +++++++++++++++++ CONTRIBUTING.md | 61 ++++++++++++++++----- PLAN.md | 39 +++++++++++++ README.md | 10 ++-- docs/CODE-QUALITY.md | 90 ++++++++++++++++++++++++++++-- docs/PHILOSOPHY.md | 1 + docs/QUALITY-REVIEW.md | 94 ++++++++++++++++++++++++++++++++ skills/project_setup/SKILL.md | 23 ++++++-- skills/quality_retrofit/SKILL.md | 24 +++++--- 9 files changed, 356 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77dfaee..6040f60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,57 @@ All notable changes to this project are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- The C# `Directory.Build.props` template was malformed XML since 0.5.0: an XML + comment contained `--`, so MSBuild refused to load it (`MSB4024`). +- The PowerShell template could pass while losing findings. `PSUseCorrectCasing` + intermittently throws in PSScriptAnalyzer 1.24.0 and 1.25.0; a planted + violation then went unreported and `-EnableExit` exited 0. The rule is off, + and every documented command adds `-ErrorAction Stop`. +- The pre-commit template's vulture hook exited 2 without configured paths; + mypy ran in an isolated environment that turned dependency types into `Any`; + ShellCheck required Docker; and `mixed-line-ending --fix=lf` rewrote files kept + in CRLF by `.gitattributes`, failing every commit that touched them. +- `verify-format-safe.py` reported unreadable, undecodable, or missing input as + "AST changed" (exit 1) and rejected valid PEP 263 source encodings. +- The atomic plan writer replaced plans with owner-only (0600) permissions and + did not flush the directory entry after the rename on POSIX. +- `detect-stack.sh` wrote unescaped paths into its JSON, matched extensions and + pruned directories case-sensitively unlike the PowerShell scanner, and walked + the tree once per extension (480 s versus 16 s on a large local workspace). + +### Added + +- C# `.editorconfig` template that makes the dead-code rules IDE0051, IDE0052, + IDE0059, IDE0060, and IDE0005 fail the build; without it they were silent. +- Rust `deny.toml` policy for advisories, licenses, bans, and sources. +- mypy template codes `explicit-override`, `exhaustive-match`, + `mutable-override`, `truthy-iterable`, and `deprecated`. +- Pre-commit template hooks for executable/shebang consistency, actionlint, and + zizmor, with guidance to freeze hook revisions to commit SHAs. +- Scanner detection of `knip.jsonc`, Go modules and tools, golangci-lint, + Dependabot/Renovate configuration, lockfiles, toolchain pins, dpdm, bats, + actionlint, zizmor, OSV-Scanner, hadolint, and the PSScriptAnalyzer and + Pester 5 modules. +- Guidance for coverage floors, mutation testing, property and fuzz testing, + benchmarks, dependency supply chain, accessibility automation, React hook + rules, and hardened GitHub Actions workflows. + +### Changed + +- This repository now passes the gates it ships: a frozen pre-commit + configuration runs Ruff, both mypy scopes with the shipped template, + vulture, Bandit, ShellCheck, shfmt, PSScriptAnalyzer, actionlint, and zizmor; + CI adds a checksum-verified full-history gitleaks scan and a coverage floor. +- CI installs hash-locked tools, pins every action to a commit SHA, disables + persisted checkout credentials, and sets job timeouts. Dependabot proposes + updates for actions, hooks, and tools after a seven-day cooldown. +- The smoke suite maintains six red drills, adding case-insensitive pruning, + `knip.jsonc` detection, and SHA-pinned release actions. + ## 0.6.0 — 2026-09-07 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79ace16..c459c0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,8 +6,12 @@ AGENTS.md universal entry point — any agent starts here .cursor/rules/ Cursor auto-discovery, points at AGENTS.md .github/copilot-instructions.md Copilot auto-discovery, same -.github/workflows/cross-platform.yml Windows/Linux script regression tests +.github/workflows/cross-platform.yml repository gates and Windows/Linux/macOS regression tests .github/workflows/release.yml tag-gated build, attestation, and publication +.github/dependabot.yml update proposals for actions, hooks, and Python tools +.pre-commit-config.yaml this repository's own gates, frozen to commit SHAs +requirements-dev.in/.txt hash-locked Python tools for local checks and CI +.coveragerc coverage settings and the enforced floor .claude-plugin/ Claude Code manifests — packaging only skills/ project_setup/ SKILL.md — new-project scaffolding @@ -81,7 +85,10 @@ from "nobody thought about this." Before committing a template change, run its gate against deliberately broken code and confirm it still fires. A config that silently stops catching things is worse than no config — see the false-green argument in -[`docs/PHILOSOPHY.md`](docs/PHILOSOPHY.md). +[`docs/PHILOSOPHY.md`](docs/PHILOSOPHY.md). `tests/test_templates.py` also parses +every template the standard library can read and fails when a new template is +not classified; it caught an XML comment that made `Directory.Build.props` +unloadable, which no drill of an individual rule would have reached. ## Editing the detect-stack scripts @@ -116,9 +123,10 @@ Linux, and macOS, then compares the Bash and PowerShell scanner inventories on both POSIX runners. `-RedDrills` reuses the same smoke suite in a temporary copy of current source. -It runs a passing baseline, separately breaks directory pruning, the explicit -root override, and the unreadable-path error, and requires each existing -assertion to fail with a non-zero child exit. Every mutation is restored +It runs a passing baseline, separately breaks directory pruning (including its +case-insensitive match), the explicit root override, the unreadable-path error, +`knip.jsonc` detection, and the release workflow's SHA-pinned action, and +requires each existing assertion to fail with a non-zero child exit. Every mutation is restored byte-for-byte before a fresh green run. The parent command fails on surviving mutations, wrong failures, timeouts, or failed restoration. CI uses this mode, which includes the normal suite; run it under both PowerShell 7 and 5.1 locally. @@ -148,20 +156,43 @@ Exercise all three workflows against isolated realistic projects. Follow independent product oracles, negative controls, and semantic review of actual artifacts. Reading a skill or matching its wording is not behavioral testing. -Delivery helpers use Python 3.12+ and the standard library. Before a change: +Delivery helpers use Python 3.12+ and only the standard library. The quality +tools are hash-locked in `requirements-dev.txt`; install them into a virtual +environment, then install the hooks (PowerShell hooks also need `pwsh` with +PSScriptAnalyzer): ```console -python -m ruff check scripts tests -python -m ruff format --check scripts tests -python -m mypy --strict --python-version 3.12 scripts/delivery scripts/verify-delivery.py scripts/update-delivery.py -python -m unittest discover -s tests -p "test_*.py" +python -m venv .venv +.venv/bin/python -m pip install --require-hashes -r requirements-dev.txt # Windows: .venv\Scripts\python.exe +.venv/bin/python -m pre_commit install ``` -CI runs these checks on Windows, Linux, and macOS with Python 3.12 and 3.14. -Its two validator mutations reuse existing regression assertions. Outcome-oracle -controls also reject broken behavior, vacuous tests, duplicated domain logic, -premature implementation, and replayed side effects. Live agent trials are -recorded separately; CI does not pretend to rerun a model. +With that environment active, run before a change: + +```console +python -m pre_commit run --all-files +python -m coverage run -m unittest discover -s tests -p "test_*.py" +python -m coverage combine +python -m coverage report +``` + +The hooks run Ruff, both mypy scopes with the shipped mypy template, vulture, +Bandit, ShellCheck, shfmt, PSScriptAnalyzer with the shipped settings, +actionlint, zizmor, and staged-change secret scanning. The entry scripts and the +tests import the delivery package under different module names, so mypy runs +once for each. Coverage includes the command-line entry points run as child +processes, and `coverage report` fails below the floor in `.coveragerc`. + +CI runs the same hooks plus a full-history gitleaks scan, then the tests with +coverage and both mypy runs on Windows, Linux, and macOS with Python 3.12 and +3.14. Its validator mutations reuse existing regression assertions. +Outcome-oracle controls also reject broken behavior, vacuous tests, duplicated +domain logic, premature implementation, and replayed side effects. Live agent +trials are recorded separately; CI does not pretend to rerun a model. + +After changing a pin in `requirements-dev.in`, regenerate the lock with the +command in that file's header. Hook revisions are frozen to commit SHAs; +refresh them with `pre-commit autoupdate --freeze`. Dependabot proposes both. ## Releasing diff --git a/PLAN.md b/PLAN.md index ca7e8f1..06e00d3 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,6 +1,8 @@ # Native delivery workflow implementation plan Status: **Complete. Implemented, evaluated, released as v0.6.0, and installed.** +The post-release audit hardening at the end of this plan is implemented and +locally verified; its hosted CI and release gates remain open. Prepared: 2026-09-07. Baseline: released v0.5.0, commit `52fa547a598b8b50392d49924e0db3fc3268c092`. @@ -419,3 +421,40 @@ release, artifact/provenance checks, and installed discovery. files verified. Fresh Codex discovery found all three workflows enabled. - Test-only policy exceptions retain standard-library unittest assertion style; narrow subprocess exceptions cover fixed trusted commands and owned fixtures. + +## Post-release audit hardening (2026-09-24) + +Baseline: `1789ed0`, clean tree, level with `origin/main`. The owner asked for +every audit finding to be fixed. Scope: shipped template and script defects, +gates this repository prescribes but did not run on itself, CI supply-chain +hardening, and missing guidance. Out of scope without a further decision: new +language templates (Go, Java/Kotlin, Swift), a version bump, tagging, and +publication. + +| ID | Required outcome | +|---|---| +| H01 | Every shipped template loads in its tool, and each changed rule is red-drilled | +| H02 | Scanners agree across shells, emit valid JSON for any path, and detect the package's own recommended files and tools | +| H03 | This repository runs every gate it prescribes for its own languages, locally and in CI | +| H04 | CI actions, hooks, and tools are immutable pins with automated update proposals | +| H05 | Guidance covers test depth, performance, supply chain, accessibility automation, and CI workflows from primary sources | +| H06 | Helper scripts report input errors distinctly and preserve file permissions | + +- [x] H01 C# props XML fixed; `.editorconfig` and `deny.toml` templates added; + mypy codes, PSScriptAnalyzer, and pre-commit hooks corrected; all drilled. +- [x] H02 Both scanners extended; bash rewritten to one escaped, case-insensitive + pass; parity, escaping, and speed verified; six maintained smoke drills. +- [x] H03 Frozen pre-commit configuration, two mypy scopes with the shipped + template, vulture whitelist, Bandit, coverage floor, and CI jobs. +- [x] H04 SHA-pinned actions, `persist-credentials: false`, timeouts, hash-locked + tools, checksum-verified gitleaks, Dependabot with cooldown. +- [x] H05 CODE-QUALITY sections and workflow, template, and README updates; all + 45 cited URLs returned HTTP 200 on 2026-09-24. +- [x] H06 `verify-format-safe.py` exit contract and writer permissions, with tests. +- [ ] Hosted cross-platform CI passes on the committed change, including macOS + and the POSIX escaping check. +- [ ] `release-package-smoke.ps1` passes on the committed tree. +- [ ] Dependabot's first scheduled run handles the uv lock and frozen hooks. + +Evidence, environments, and limits: `docs/QUALITY-REVIEW.md`, section +"Post-release audit: 2026-09-24". diff --git a/README.md b/README.md index 75903ae..48f89ad 100644 --- a/README.md +++ b/README.md @@ -170,15 +170,17 @@ compliance by being copied. |---|---| | Python | Ruff, mypy, vulture, deptry, Bandit, pip-audit; project tests | | TypeScript / JavaScript | Prettier, ESLint, TypeScript, knip, dpdm, npm audit, Semgrep | -| Rust | rustfmt, Clippy, compiler, cargo-machete, cargo-audit, cargo-deny, cargo test | +| Rust | rustfmt, Clippy, compiler, cargo-machete, cargo-audit, cargo-deny policy, cargo test | | C / C++ | clang-format, clang-tidy, cppcheck, compiler, separate sanitizer builds | -| C# / .NET | dotnet format, analyzers, nullable checks, warnings as errors, NuGet audit, dotnet test | -| HTML / CSS | Semantic and accessibility review, HTMLHint, Stylelint, Prettier | +| C# / .NET | dotnet format, analyzers, dead-code rule severities, nullable checks, warnings as errors, NuGet audit, dotnet test | +| HTML / CSS | Semantic and accessibility review, axe-core, HTMLHint, Stylelint, Prettier | | PowerShell / Shell | PSScriptAnalyzer / ShellCheck and shfmt; project tests | | Go | gofmt, go vet, selected Staticcheck, govulncheck, go test, race/fuzz checks where supported | +| GitHub Actions | actionlint, zizmor; SHA-pinned actions and least-privilege permissions | Go has guidance rather than a supplied configuration template. Cross-cutting -references cover Gitleaks, Semgrep, and jscpd. See the +references cover Gitleaks, Semgrep, jscpd, and OSV-Scanner, plus per-stack +coverage floors, mutation testing, property/fuzz tests, and benchmarks. See the [language review contract](docs/CODE-QUALITY.md) for primary sources and the [template index](templates/README.md) for configuration details and compatibility. diff --git a/docs/CODE-QUALITY.md b/docs/CODE-QUALITY.md index fc1e750..6e7493c 100644 --- a/docs/CODE-QUALITY.md +++ b/docs/CODE-QUALITY.md @@ -1,7 +1,9 @@ # Code quality by language -Reviewed against primary sources on 2026-09-07. Read the common contract and -the sections for the detected stack before implementation or template changes. +Reviewed against primary sources on 2026-09-07; the test-depth, supply-chain, +and CI sections were added from primary sources on 2026-09-24. Read the common +contract and the sections for the detected stack before implementation or +template changes. These are this package's engineering decisions informed by the linked sources; they are not a claim that a language author endorses every supplied setting. Select supported runtimes and pin compatible tools in the target project. @@ -56,6 +58,70 @@ ordinary scans otherwise report findings without a failing exit. Pin or retain the rule source used for repeatable CI, and verify include/exclude scope. See the [Semgrep CLI contract](https://docs.semgrep.dev/cli-reference/). +## Test depth and performance evidence + +Coverage shows which code ran, not whether a test would notice it breaking; +red drills and mutation testing show that. Set a coverage floor from a measured +baseline, leaving only the margin that differs between supported platforms or +runtimes, and raise it as tests improve. Never lower it to admit untested code. +Run mutation testing on the changed code in CI and across the whole project on +a schedule. Add property or fuzz tests where inputs are parsed or unbounded. + +| Stack | Coverage floor | Mutation testing | Property / fuzz | Benchmarks | +|---|---|---|---|---| +| Python | [coverage.py](https://coverage.readthedocs.io/en/latest/config.html) `fail_under`, branch on | [mutmut](https://mutmut.readthedocs.io/en/latest/) (needs `fork`; WSL on Windows) | [Hypothesis](https://hypothesis.readthedocs.io/en/latest/) | [pyperf](https://pyperf.readthedocs.io/en/latest/) | +| TS / JS | [Vitest](https://vitest.dev/config/coverage) `coverage.thresholds` | [StrykerJS](https://stryker-mutator.io/docs/stryker-js/introduction/) | [fast-check](https://fast-check.dev/) | [size-limit](https://github.com/ai/size-limit) for bundle budgets | +| Rust | [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) `--fail-under-lines` | [cargo-mutants](https://mutants.rs/in-diff.html) `--in-diff` | [proptest](https://proptest-rs.github.io/proptest/), [cargo-fuzz](https://rust-fuzz.github.io/book/cargo-fuzz.html) | [Criterion](https://criterion-rs.github.io/book/index.html) | +| C / C++ | [gcovr](https://gcovr.com/en/stable/manpage.html) `--fail-under-line` | none selected | [FuzzTest](https://github.com/google/fuzztest) | [Google Benchmark](https://google.github.io/benchmark/) | +| C# | [coverlet](https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/MSBuildIntegration.md) `/p:Threshold` | [Stryker.NET](https://stryker-mutator.io/docs/stryker-net/introduction/) | [CsCheck](https://github.com/AnthonyLloyd/CsCheck) | [BenchmarkDotNet](https://benchmarkdotnet.org/) | +| Go | [`go test -coverprofile`](https://pkg.go.dev/cmd/go#hdr-Testing_flags); no built-in floor | no mature tool; record as open | [native fuzzing](https://go.dev/doc/security/fuzz/) | `testing.B` with [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat) | + +Record performance budgets in the plan with the workload, data size, and +hardware they apply to. Compare benchmarks against a baseline from the same +machine class; shared CI runners are too noisy for absolute wall-clock gates, so +gate on relative change or use a dedicated runner. Time whole commands with +[hyperfine](https://github.com/sharkdp/hyperfine) and profile Python with +[py-spy](https://github.com/benfred/py-spy) before optimizing. A tool listed +here is a starting point: drill it in the project before trusting it as a gate. + +## Dependencies and supply chain + +- Commit lockfiles and install from them in CI: `npm ci`, `cargo build --locked`, + `uv sync --locked`, `pip install --require-hashes`, and NuGet + [locked mode](https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#locking-dependencies). + Add `--ignore-scripts` to npm installs that do not need package scripts. +- Scan every lockfile together with + [OSV-Scanner](https://google.github.io/osv-scanner/usage/scan-source) + (`osv-scanner scan source -r .`) in addition to each ecosystem's audit. +- Automate updates with [Dependabot](https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference) + or [Renovate](https://docs.renovatebot.com/). A short cooldown lets a new + release age before it is proposed; every update still passes the full gates. +- When the brief requires an SBOM, generate it from the built artifact with + [Syft](https://github.com/anchore/syft) and publish it beside provenance. +- Lint Dockerfiles with [hadolint](https://github.com/hadolint/hadolint), and + pin scanner images and actions by digest or SHA. Trivy's own release actions + were [compromised in March 2026](https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23). + +## CI workflows (GitHub Actions) + +Workflows run with repository credentials, so review them as production code. +Follow GitHub's [secure use reference](https://docs.github.com/en/actions/reference/security/secure-use): + +- Pin every action to a full-length commit SHA with the version as a comment. + A tag can be moved to different code; a SHA cannot. +- Declare `permissions` at the top (`{}` or `contents: read`) and grant write + scopes only to the job that needs them. +- Check out with `persist-credentials: false` unless a later step runs + authenticated Git commands. +- Pass untrusted values such as branch names, PR titles, and issue text into + `run:` through `env:`; an inline `${{ }}` becomes shell source. +- Set `timeout-minutes` on every job and verify downloaded binaries against a + pinned checksum. + +Gate workflows with [actionlint](https://github.com/rhysd/actionlint) (syntax, +expressions, embedded shell) and [zizmor](https://docs.zizmor.sh/audits/) +(security audits); both are in the pre-commit template. + ## Python Keep cohesive packages and explicit public APIs; isolate I/O and mutable state @@ -68,8 +134,10 @@ and [Ruff S101](https://docs.astral.sh/ruff/rules/assert/). Match Ruff's target and mypy's Python version to the minimum supported runtime. Keep Ruff correctness/test rules and mypy's unreachable, redundant-expression, -truthiness, and unused-awaitable checks; annotate test helpers rather than -blanket-exempting tests. Drill constant assertions, always-truthy expressions, +truthiness, unused-awaitable, explicit-override, and exhaustive-match checks +(the last two mirror TypeScript's `noImplicitOverride` and exhaustive switch +check); annotate test helpers rather than blanket-exempting tests. Run mypy in +the project's environment: an isolated hook cannot see dependency types. Drill constant assertions, always-truthy expressions, and an unhandled failure. See [mypy optional checks](https://mypy.readthedocs.io/en/stable/error_code_list2.html). ## TypeScript and JavaScript @@ -97,6 +165,9 @@ union case, an ignored promise, a tautological condition, and any changed test. For React, keep render pure, state owned, and effects tied to external systems; do not add mirrored state or effects for values derivable during render. See [React purity rules](https://react.dev/reference/rules/components-and-hooks-must-be-pure). +Enforce the hook rules with +[eslint-plugin-react-hooks](https://react.dev/reference/eslint-plugin-react-hooks) +(`reactHooks.configs.flat.recommended`). ## Rust @@ -150,7 +221,8 @@ that [latest varies by installed compiler](https://learn.microsoft.com/en-us/dot Run compiler, analyzer, formatting, and test gates on every supported target. Verify relevant IDE diagnostic severity at build time instead of assuming -`EnforceCodeStyleInBuild` enables every suggestion. Include a nullable-error +`EnforceCodeStyleInBuild` enables every suggestion: without the `.editorconfig` +template's severities, an unused private method (IDE0051) builds cleanly. Include a nullable-error drill and [CA1508 dead-condition drill](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1508). Install the selected test framework's analyzers explicitly; the shared props file does not supply them. @@ -173,6 +245,8 @@ Use explicit encodings and restore process state in `finally`. Select real, installed [compatibility inventories](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/usecompatiblecmdlets) and execute on the supported hosts; historical profile matches do not certify PowerShell 5.1/7 behavior. Drill ignored native failures and empty result sets. +Run the analyzer with `-ErrorAction Stop`: a rule that throws otherwise loses +its findings while `-EnableExit` still reports success. ## Shell @@ -195,7 +269,11 @@ links for navigation, and explicit form labels. Do not replace native behavior with clickable generic containers or redundant/conflicting ARIA. Follow [W3C structural-role guidance](https://www.w3.org/WAI/ARIA/apg/practices/structural-roles/). HTML syntax checks complement keyboard, focus, accessible-name, and screen-reader -verification; they do not replace it. Validate rendered markup as well as source +verification; they do not replace it. Run [axe-core](https://github.com/dequelabs/axe-core) +against rendered pages in end-to-end tests, for example through +[Playwright](https://playwright.dev/docs/accessibility-testing); automated rules +find a subset of problems, so manual checks remain required. Validate rendered +markup as well as source templates, including errors, loading, and empty states. Keep CSS ownership clear, reuse design tokens, and keep specificity bounded. diff --git a/docs/PHILOSOPHY.md b/docs/PHILOSOPHY.md index 6cee4ee..72cf008 100644 --- a/docs/PHILOSOPHY.md +++ b/docs/PHILOSOPHY.md @@ -31,6 +31,7 @@ Concrete examples of rules deliberately disabled: | `multiple_crate_versions` (clippy) | you rarely control transitive duplicates | | `unicorn/prevent-abbreviations` | renames `req`/`res`/`props` against every framework convention | | `CS1591` (C#) | XML doc on every public member is busywork | +| `PSUseCorrectCasing` (PSScriptAnalyzer) | crashes intermittently; the crash dropped a real finding while the gate exited 0 | Each is a one-line revert. The point is that the decision was made deliberately and written down, not that it is permanent. diff --git a/docs/QUALITY-REVIEW.md b/docs/QUALITY-REVIEW.md index 23f1fdb..465fbe3 100644 --- a/docs/QUALITY-REVIEW.md +++ b/docs/QUALITY-REVIEW.md @@ -206,3 +206,97 @@ This post-publication receipt is maintained on main. The installed clone remains at exact release source; the immutable tagged plan records its prerequisite boundary, while this audit and the current plan record completed publication. No implementation or required release gate remains open. + +## Post-release audit: 2026-09-24 + +Scope: an audit of `1789ed0` (v0.6.0 plus evidence commits) for missing tools, +gates this repository prescribed but did not run on itself, and template +defects. Every finding below was reproduced before it was fixed. The earlier +sections remain the record for their own dates. + +### Findings corrected + +| Area | Finding | Correction | +|---|---|---| +| C# template | `Directory.Build.props` has been malformed XML since v0.5.0 (`--` inside a comment); MSBuild failed with `MSB4024` | Comment reworded; `tests/test_templates.py` parses every template the standard library can read | +| C# template | IDE0051/IDE0052 were listed as the dead-code gate, but no severity was configured, so an unused private method built cleanly | `csharp/.editorconfig` sets IDE0051, IDE0052, IDE0059, IDE0060, and IDE0005 to error | +| PowerShell template | `PSUseCorrectCasing` threw `NullReferenceException` intermittently (1.24.0 and 1.25.0, PowerShell 7.6.6); a planted violation went unreported with exit 0 | Rule disabled with evidence; all documented commands add `-ErrorAction Stop` | +| mypy template | No equivalent of TypeScript's override and exhaustive-switch checks | `explicit-override`, `exhaustive-match`, `mutable-override`, `truthy-iterable`, `deprecated` | +| Pre-commit template | vulture hook exited 2 without paths; mirrors-mypy turned dependencies into `Any`; ShellCheck hook needed Docker; `mixed-line-ending --fix=lf` rewrote CRLF-by-policy files | Hook `args`; project-environment mypy hook; `shellcheck-py`; `--fix=no` | +| Rust template | cargo-deny was a listed gate with no policy; its defaults rejected even an MIT crate | `rust/deny.toml` | +| `verify-format-safe.py` | Missing, undecodable, or directory input exited 1 ("AST changed"); valid Latin-1 source rejected | Parses bytes; input errors exit 2; tests added | +| Plan writer | Replacement plan became owner-only (0600) on POSIX | Mode preserved or umask-derived; directory flushed after rename | +| `detect-stack.sh` | Unescaped JSON root; case-sensitive matching unlike PowerShell; one tree walk per extension | Escaped strings, one case-insensitive walk, parity-checked | +| Repository gates | ShellCheck, shfmt, PSScriptAnalyzer, vulture, Bandit, gitleaks history, coverage, and mypy on tests were not run by CI | Frozen pre-commit configuration and CI jobs run them all | +| CI supply chain | Actions pinned to movable tags; credentials persisted by checkout; no timeouts; unhashed tool installs; no update automation | SHA pins, `persist-credentials: false`, timeouts, hash lock, Dependabot with cooldown | + +### Executed drills + +Each row passed first, failed for the named reason, then passed again after +byte-identical restoration, unless the row states otherwise. Commands ran in +disposable copies or fixtures; the working tree was never mutated. + +| Gate | Injected defect | Required diagnostic | Exit sequence | +|---|---|---|---| +| `unittest tests.test_verify_format_safe` | Original helper, before the fix | four failures (`1 != 2`, `1 != 0`) | 1 -> 0 after fix | +| `unittest tests.test_update_delivery` (Linux) | Remove mode preservation | `AssertionError: 384 != 420` | 0 -> 1 -> 0 | +| `unittest tests.test_templates` | Original props template | `ParseError: … line 32, column 11` | 1 -> 0 after fix | +| mypy 2.3.1 with the template | Missing `@override`; missing match case; narrowed mutable attribute; truthiness of an `Iterable`; call to a `@deprecated` function | `[explicit-override]`, `[exhaustive-match]`, `[mutable-override]`, `[truthy-iterable]`, `[deprecated]` | 0 -> 1 -> 0 each | +| Template pre-commit hooks | Wrong return type; unused function; lost executable bit; unknown `github` property; unpinned action | `[return-value]`, `unused function 'orphan'`, `marked executable`, `nonexistent_property`, `unpinned-uses` | 0 -> 1 -> 0 each | +| `dotnet build` with both C# templates | Unused private method; unread field; overwritten value; unused parameter; unnecessary `using` | IDE0051, IDE0052, IDE0059, IDE0060, IDE0005 | 0 -> 1 -> 0 each | +| `cargo deny check` with the template | License outside the allow-list; wildcard path dependency | `rejected`; `wildcard` | 0 -> 4 -> 0; 0 -> 2 -> 0 | +| `coverage report` (Linux) | Remove `test_verify_format_safe.py` | `total of 80.7 is less than fail-under=82.0` | 0 -> 2 -> 0 | +| `cross-platform-smoke.ps1 -RedDrills`, PowerShell 7 and 5.1 | Six maintained mutations | Each existing assertion message | 0 -> 1 -> 0 each | + +Additional observations: + +- PSScriptAnalyzer, ten fresh processes each: the new template exited 0 on a + clean script every time and 1 on an alias violation every time. With the + casing rule re-enabled and `-ErrorAction Stop`, three crashes all exited 1. +- The superseded hooks, observed directly: mirrors-mypy rejected correct code + with `Return type becomes "Any" due to an unfollowed import`; the old vulture + hook printed `Please pass at least one file or directory`; `--fix=lf` + rewrote a uniformly CRLF `.ps1` and exited 1, while `--fix=no` passed it and + still rejected a mixed file. +- A first smoke drill that removed `ToLowerInvariant()` survived: PowerShell + hashtables already compare keys case-insensitively. It was replaced with the + case-insensitive directory-pruning drill rather than counted. +- Scanner parity on a mixed-case fixture: bash and PowerShell now agree on every + section; the previous bash scanner counted `Build/` and missed `.TS` and `.H`. + On a local multi-project workspace with more than 29,000 counted source + files, the previous bash scanner took 480.0 s, the new one 16.3 s, and + PowerShell 16.5 s, with identical counts (Git Bash on Windows). On Linux, a directory + named with a quote, backslash, and tab produced invalid JSON before the change + and an exact root afterwards. +- `requirements-dev.txt` installed with `--require-hashes` on Windows (Python + 3.12.14 and 3.14.0) and Linux (3.14.7). Under 3.12 the suite passed 58 tests + at 84.2% coverage. Coverage measured 83.1% on Windows and 83.3% on Linux. +- The repository pre-commit configuration passed on all 87 files, the smoke + suite passed under PowerShell 7 and 5.1, actionlint and zizmor reported no + findings, and gitleaks found no leaks in 31 commits. + +Environment: Windows 11, PowerShell 7.6.6 and Windows PowerShell 5.1, Python +3.14.0 and uv-managed 3.12.14; Arch Linux on WSL2 with Python 3.14.7. Ruff +0.16.4, mypy 2.3.1, coverage 7.16.1, Bandit 1.9.4, vulture 2.16, pre-commit +4.6.2, ShellCheck 0.11.0, actionlint 1.7.12, zizmor 1.30.1, PSScriptAnalyzer +1.25.0, .NET SDK 10.0.401, cargo 1.96.0, cargo-deny 0.19.9, gitleaks 8.30.1. +The pinned CI gitleaks 8.30.0 archive matched its published SHA-256. + +### Limits and open gates + +- Hosted CI has not run these changes; they are uncommitted. macOS, the + POSIX parity job, and the release job are unverified until it does. +- `release-package-smoke.ps1` requires a clean committed tree and was not run. +- Dependabot's `uv` and `pre-commit` ecosystems are configured from its + documentation; their handling of this lock and these frozen revisions is + unverified until the first scheduled run. +- The release workflow keeps `uses: ./…` instead of zizmor's suggested `$/…` + self-repository form, because only a tagged release exercises that path. +- New guidance tools (mutation, property/fuzz, benchmark, axe-core, + OSV-Scanner, Syft, hadolint) cite verified primary sources but were not + drilled here; target projects must drill them before relying on them. +- No Go, Java/Kotlin, or Swift templates were added. The skill wording changed; + live agent trials were not rerun for it. +- The scanners' Python interpreter probes (as before) and the new bash + PowerShell-module probe run without a timeout; a hung interpreter would stall + the scan. None hung in these runs. diff --git a/skills/project_setup/SKILL.md b/skills/project_setup/SKILL.md index 4a92b61..4dc13d0 100644 --- a/skills/project_setup/SKILL.md +++ b/skills/project_setup/SKILL.md @@ -221,18 +221,21 @@ and each documents its own deliberate loosenings. |---|---|---|---|---|---|---| | Python | ruff format | ruff (`ALL`) | mypy strict | vulture, deptry | bandit, pip-audit | pytest | | TS/JS | prettier | eslint strictTypeChecked | tsc strict+ | knip | npm audit, semgrep | vitest | -| Rust | rustfmt | clippy pedantic | (compiler) | cargo-machete | cargo-audit, cargo-deny | cargo test | +| Rust | rustfmt | clippy pedantic | (compiler) | cargo-machete | cargo-audit, cargo-deny (`deny.toml`) | cargo test | | C++ | clang-format | clang-tidy, cppcheck | (compiler) | cppcheck unusedFunction | sanitizers, valgrind | ctest | -| C# | dotnet format | AnalysisLevel latest-all | nullable+warnaserror | IDE0051/0052 | NuGetAudit | dotnet test | +| C# | dotnet format | AnalysisLevel latest-all | nullable+warnaserror | IDE0051/0052 (`.editorconfig`) | NuGetAudit | dotnet test | | CSS | prettier | stylelint | — | — | — | — | | HTML | prettier | htmlhint | — | — | — | — | | PowerShell | PSScriptAnalyzer | PSScriptAnalyzer | — | — | — | Pester | | Shell | shfmt | shellcheck | — | — | gitleaks | bats | | Go | gofmt | go vet, selected Staticcheck | (compiler) | selected analyzer + review | govulncheck | go test, race/fuzz where supported | +| GitHub Actions | — | actionlint | — | — | zizmor | — | Cross-cutting regardless of stack: `gitleaks` (secrets; use `gitleaks git` for -reachable checked-out history), `semgrep` (multi-language SAST), and `jscpd` -(copy-paste detection). +reachable checked-out history), `semgrep` (multi-language SAST), `jscpd` +(copy-paste detection), and OSV-Scanner across all lockfiles. Coverage floors, +mutation testing, property/fuzz tests, and benchmarks per stack are in +`${SKILL_ROOT}/docs/CODE-QUALITY.md`. ### The flags that make a linter a gate @@ -243,7 +246,7 @@ decorative: ``` eslint --max-warnings=0 stylelint --max-warnings=0 cargo clippy -- -D warnings cppcheck --error-exitcode=1 -knip --strict Invoke-ScriptAnalyzer -EnableExit +knip --strict Invoke-ScriptAnalyzer -EnableExit -ErrorAction Stop semgrep scan --error (findings must fail the command) ruff check (nonzero by default) mypy (nonzero by default) @@ -416,6 +419,12 @@ must also carry. - `.env` in `.gitignore` before the first commit. - Secure headers, input validation, output encoding where applicable. - Least-privilege defaults. +- Lockfiles committed and installed in locked or hash-checked mode; update bot + (Dependabot or Renovate) with a cooldown; SBOM when the brief requires one. +- CI workflows hardened: actions pinned to commit SHAs, top-level read-only + `permissions`, `persist-credentials: false`, untrusted values passed through + `env:`, and actionlint plus zizmor as gates. See the supply-chain and CI + sections of `${SKILL_ROOT}/docs/CODE-QUALITY.md`. Document security assumptions and accepted residual risk in `PLAN.md`. @@ -487,6 +496,10 @@ Where applicable: - Production build validation — the build must actually succeed. - Bundle size awareness with a recorded budget. +- For services, libraries, and CLIs: benchmarks of the hot paths the brief + names, compared against a recorded baseline on the same machine class, and a + profile before any optimization. Shared CI runners are too noisy for absolute + timing gates; gate on relative change or use a dedicated runner. - Lighthouse on visual milestones: **Performance, Accessibility, Best Practices**. SEO excluded unless explicitly requested. - Prefer static/server-side work where the stack favors it. diff --git a/skills/quality_retrofit/SKILL.md b/skills/quality_retrofit/SKILL.md index 23400e1..2528994 100644 --- a/skills/quality_retrofit/SKILL.md +++ b/skills/quality_retrofit/SKILL.md @@ -188,7 +188,7 @@ one signal that protects the user's work. Record what exists before the gates run, and remove only what they created: ```bash -CACHES=(.ruff_cache .mypy_cache .pytest_cache .tox htmlcov) +CACHES=(.ruff_cache .mypy_cache .pytest_cache .tox htmlcov .coverage) PRE=(); for d in "${CACHES[@]}"; do [ -e "$d" ] && PRE+=("$d"); done # ... run the baseline gates ... @@ -203,7 +203,7 @@ git status --porcelain # must match what it printed before the baseline PowerShell equivalent: ```powershell -$Caches = @('.ruff_cache', '.mypy_cache', '.pytest_cache', '.tox', 'htmlcov') +$Caches = @('.ruff_cache', '.mypy_cache', '.pytest_cache', '.tox', 'htmlcov', '.coverage') $PreExistingCaches = @($Caches | Where-Object { Test-Path -LiteralPath $_ }) # ... run the baseline gates ... @@ -277,11 +277,15 @@ earlier change. This step is not optional. ### Phase 2 — config and gates (no product-source change) Install or extend the strict configs. Wire the gate scripts. Add pre-commit. -Add CI workflow. Nothing under `src/` changes in this phase. +Add or extend the CI workflow, hardened per the CI section of +`${SKILL_ROOT}/docs/CODE-QUALITY.md`: actions pinned to commit SHAs, read-only +default permissions, `persist-credentials: false`, job timeouts, and actionlint +plus zizmor as gates. Install dependencies from lockfiles in CI. Nothing under +`src/` changes in this phase. Ignore the tool caches here, once, for the rest of the retrofit — `.ruff_cache/`, `.mypy_cache/`, `.pytest_cache/`, `__pycache__/`, `.tox/`, -`htmlcov/`, `node_modules/`, `target/`, `dist/`, `build/`, plus whatever the +`htmlcov/`, `.coverage`, `node_modules/`, `target/`, `dist/`, `build/`, plus whatever the project's own toolchain writes. Extend the existing `.gitignore`; do not replace it. @@ -324,7 +328,8 @@ Each tool sees something the others structurally cannot: - `vulture` — Python, heuristic - `cppcheck --enable=all` — includes `unusedFunction` - `cargo machete` — unused Cargo deps -- Roslyn `IDE0051`/`IDE0052` — unused C# private members +- Roslyn `IDE0051`/`IDE0052` — unused C# private members, reported at build + only when `.editorconfig` sets their severity (see the C# template) - `dpdm --no-warning --no-tree --exit-code circular:1 ` — import cycles **Confirm the tool fires before trusting a clean run.** A dead-code tool that @@ -407,7 +412,10 @@ checks. not a lint finding: the secret is in history, so rotate it first, then scrub. Report and stop; do not rewrite history unprompted. -- `semgrep scan --error --config=auto`, `bandit`, `npm audit`, `pip-audit`, `cargo audit` +- `semgrep scan --error --config=auto`, `bandit`, `npm audit`, `pip-audit`, `cargo audit`, + and `osv-scanner scan source -r .` across every lockfile +- `zizmor` over `.github/`: an unpinned action or injectable `${{ }}` in a + workflow that holds write permissions is a supply-chain finding, not style - C/C++/Rust: wire sanitizer CI jobs. ASan+UBSan in one job, TSan in a **separate** one — they use incompatible shadow memory and cannot be combined. Use `-fno-sanitize-recover=all` so recoverable UBSan findings halt instead of @@ -445,8 +453,10 @@ silently omit a row. [ ] No unjustified any / ignore / suppression [ ] Secret scan clean over reachable checked-out history (full checkout if claimed) [ ] Dependency audit clean, or exceptions documented +[ ] Lockfiles committed; CI installs in locked or hash-checked mode +[ ] CI actions pinned to SHAs, least-privilege permissions, zizmor clean [ ] Sanitizers wired (native code) and passing -[ ] Tests pass; coverage recorded +[ ] Tests pass; coverage floor set from the measured baseline and enforced [ ] Tests actually discovered/executed; affected red drills caught intended defects [ ] Maintained drill set passes with exact restoration and final green [ ] Canonical code enhanced; new paths justified; no unintended parallel implementation