diff --git a/.project/threatmodel/mitigations.yaml b/.project/threatmodel/mitigations.yaml index 1f8f6afe..3c206a04 100644 --- a/.project/threatmodel/mitigations.yaml +++ b/.project/threatmodel/mitigations.yaml @@ -146,7 +146,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.sink.dangerous_attr - file_hint: scripts/create-example-test-repo.py:142 + file_hint: scripts/create-example-test-repo.py:142 # removed in 0.2.0, #487 - fingerprint: sha256:39dcf5bd1ab43e91 status: accepted note: Test fixture, example, or build script — not production code. Commands are @@ -155,7 +155,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.sink.dangerous_attr - file_hint: scripts/create-example-test-repo.py:308 + file_hint: scripts/create-example-test-repo.py:308 # removed in 0.2.0, #487 - fingerprint: sha256:8da33a516f06a8ce status: mitigated note: Opengrep/Semgrep binary invocation with fully static argv. Binary path is @@ -470,7 +470,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.sink.dangerous_attr - file_hint: scripts/create-example-test-repo.py:136 + file_hint: scripts/create-example-test-repo.py:136 # removed in 0.2.0, #487 - fingerprint: sha256:d8b8307b8c16479f status: accepted note: Test fixture, example, or build script — not production code. Commands are @@ -479,7 +479,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.sink.dangerous_attr - file_hint: scripts/create-example-test-repo.py:139 + file_hint: scripts/create-example-test-repo.py:139 # removed in 0.2.0, #487 - fingerprint: sha256:2603b50ec203f5cc status: accepted note: Test fixture, example, or build script — not production code. Commands are @@ -488,7 +488,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.sink.dangerous_attr - file_hint: scripts/create-example-test-repo.py:280 + file_hint: scripts/create-example-test-repo.py:280 # removed in 0.2.0, #487 - fingerprint: sha256:18143895dff565cf status: mitigated note: TOML-config-driven command execution using list-form subprocess. The base @@ -837,7 +837,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.info_disc.open_call - file_hint: packages/darnit-example/src/darnit_example/tools.py:89 + file_hint: packages/darnit-example/src/darnit_example/tools.py:89 # removed in 0.2.0, #487 - fingerprint: sha256:a29e1edb04ab00ed status: mitigated note: 'All file paths originate from trusted sources: TOML configuration, computed @@ -847,7 +847,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.info_disc.open_call - file_hint: packages/darnit-example/src/darnit_example/handlers.py:35 + file_hint: packages/darnit-example/src/darnit_example/handlers.py:35 # removed in 0.2.0, #487; moved to darnit_testchecks/handlers.py - fingerprint: sha256:f488c060d5b16b9c status: mitigated note: 'All file paths originate from trusted sources: TOML configuration, computed @@ -857,7 +857,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.info_disc.open_call - file_hint: packages/darnit-example/src/darnit_example/handlers.py:85 + file_hint: packages/darnit-example/src/darnit_example/handlers.py:85 # removed in 0.2.0, #487; moved to darnit_testchecks/handlers.py - fingerprint: sha256:1efd167695e4ea87 status: accepted note: Test fixture, example, or build script — not production code. Commands are @@ -866,7 +866,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.sink.dangerous_attr - file_hint: scripts/create-example-test-repo.py:297 + file_hint: scripts/create-example-test-repo.py:297 # removed in 0.2.0, #487 - fingerprint: sha256:74cd6b4caf19c0bc status: accepted note: Test fixture, example, or build script — not production code. Commands are @@ -875,7 +875,7 @@ entries: reviewer: claude-opus-4-6 reviewed_at: '2026-04-14' query_id: python.sink.dangerous_attr - file_hint: scripts/create-example-test-repo.py:329 + file_hint: scripts/create-example-test-repo.py:329 # removed in 0.2.0, #487 - fingerprint: sha256:2ef91acfef1096bd status: mitigated note: Git/gh CLI invocation with fixed binary name and list-form arguments. No shell=True, diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 62f36366..896b5bfe 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -120,8 +120,7 @@ baseline-mcp/ │ │ ├── threat_model/ # STRIDE analysis engine │ │ └── formatters/ # SARIF output generation │ │ -│ ├── darnit-example/ # Example implementation (docs reference) -│ └── darnit-testchecks/ # Test implementation (for testing) +│ └── darnit-testchecks/ # Test-only plugin (not a template; see darnit-hello) │ ├── docs/ │ ├── WORKFLOW.md # Mermaid diagrams (audit, remediation, context, startup) diff --git a/CHANGELOG.md b/CHANGELOG.md index 176c1131..7e28cdf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,6 +136,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 discovered as before. The in-tree implementations drop them, and `darnit-example` drops its `_RULES` catalog, `remediation/registry.py`, and empty `controls` package (#487). +- The `darnit-example` workspace package (never published) and + `scripts/create-example-test-repo.py`. Plugin authors start from + `darnit-hello`; the custom step types the tests used moved to the test-only + `darnit-testchecks` package (#487). ### Added diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 6fc07613..852bbf69 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -722,7 +722,7 @@ Darnit is a compliance auditing tool — its core purpose is to read repository - **Sieve handlers** (builtin_handlers.py): Read repository files to check compliance patterns. Paths come from TOML control definitions (file_exists, pattern handlers). - **Remediation pipeline** (executor.py, helpers.py, github.py, orchestrator.py): Read templates and write remediation files. Paths from package resources or MCP `local_path` parameter. - **Threat model generators** (remediation.py, dependencies.py): Read source files for structural analysis. Paths from directory traversal within `local_path`. -- **Example/test code** (darnit_example, test_repository.py): Not production — example and test fixtures. +- **Example/test code** (test_repository.py; darnit_example removed in 0.2.0, #487): Not production — example and test fixtures. - **Cache and verification** (audit_cache.py, verification.py): Read/write cache files in known locations. The `local_path` MCP parameter is the primary trust boundary — the user (MCP client) chooses which repository to audit. Reading files within that path is the intended behavior. Path traversal beyond `local_path` would be a valid concern but is not structurally present in these code paths. @@ -733,13 +733,13 @@ The `local_path` MCP parameter is the primary trust boundary — the user (MCP c | # | Title | Location | Score | |---|-------|----------|-------| -| TM-D-001 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:136` | 1.80 | -| TM-D-002 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:139` | 1.80 | -| TM-D-003 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:142` | 1.80 | -| TM-D-004 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:280` | 1.80 | -| TM-D-005 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:297` | 1.80 | -| TM-D-006 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:308` | 1.80 | -| TM-D-007 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:329` | 1.80 | +| TM-D-001 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:136` (removed in 0.2.0, #487) | 1.80 | +| TM-D-002 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:139` (removed in 0.2.0, #487) | 1.80 | +| TM-D-003 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:142` (removed in 0.2.0, #487) | 1.80 | +| TM-D-004 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:280` (removed in 0.2.0, #487) | 1.80 | +| TM-D-005 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:297` (removed in 0.2.0, #487) | 1.80 | +| TM-D-006 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:308` (removed in 0.2.0, #487) | 1.80 | +| TM-D-007 | No timeout on subprocess.run() | `scripts/create-example-test-repo.py:329` (removed in 0.2.0, #487) | 1.80 | | TM-D-008 | No timeout on subprocess.run() | `packages/darnit-baseline/src/darnit_baseline/attestation/git.py:24` | 1.80 | | TM-D-009 | No timeout on subprocess.run() | `packages/darnit-baseline/src/darnit_baseline/attestation/git.py:48` | 1.80 | | TM-D-010 | No timeout on subprocess.run() | `packages/darnit-baseline/src/darnit_baseline/attestation/git.py:60` | 1.80 | diff --git a/docs/HANDLER_AUTHORING.md b/docs/HANDLER_AUTHORING.md index c82fa246..b570f474 100644 --- a/docs/HANDLER_AUTHORING.md +++ b/docs/HANDLER_AUTHORING.md @@ -213,8 +213,7 @@ def readme_description_handler( ... ``` -The real example package in `packages/darnit-example/` already contains a good -reference implementation: +A complete version of that handler: ```python import os @@ -356,7 +355,7 @@ That catches bugs faster and avoids depending on unrelated controls: from pathlib import Path from darnit.sieve.handler_registry import HandlerContext, HandlerResultStatus -from darnit_example.handlers import readme_description_handler +from your_package.handlers import readme_description_handler def test_readme_description_handler(tmp_path: Path) -> None: @@ -423,9 +422,11 @@ These files are the best companions while authoring handlers: - `docs/IMPLEMENTATION_GUIDE.md` - `CLAUDE.md` sections `Sieve Pattern` and `TOML Schema Features` -- `packages/darnit-example/example-hygiene.toml` -- `packages/darnit-example/src/darnit_example/handlers.py` -- `packages/darnit-example/src/darnit_example/implementation.py` +- `packages/darnit-hello/` -- the minimal plugin template +- `packages/darnit-reproducibility/src/darnit_reproducibility/handlers.py` and + `implementation.py` -- a real plugin's custom step types and their + registration in `register_handlers()` +- `packages/darnit-gittuf/src/darnit_gittuf/handlers.py` and `implementation.py` - `packages/darnit/src/darnit/sieve/builtin_handlers.py` - `packages/darnit/src/darnit/sieve/handler_registry.py` diff --git a/docs/IMPLEMENTATION_GUIDE.md b/docs/IMPLEMENTATION_GUIDE.md index 261824e6..fdf942e1 100644 --- a/docs/IMPLEMENTATION_GUIDE.md +++ b/docs/IMPLEMENTATION_GUIDE.md @@ -22,10 +22,10 @@ against a fictional "My Compliance Standard". Along the way, we'll reference how - Familiarity with Python packaging (pyproject.toml, entry points) - A local clone of the darnit repository for reference -> **Working example**: The `packages/darnit-example/` package is a complete, -> installable implementation that follows every step in this guide. You can -> study it alongside these instructions — see `packages/darnit-example/README.md` -> for a mapping between guide sections and example files. +> **Starting point**: `packages/darnit-hello/` is the minimal plugin template: +> one control, the entry points, and a framework TOML inside the package. Copy +> it and grow it with this guide. For real plugins with custom step types, see +> `packages/darnit-reproducibility/` and `packages/darnit-gittuf/`. ## Architecture Overview @@ -1339,8 +1339,9 @@ steps = [ ] ``` -> **Reference**: See `packages/darnit-example/src/darnit_example/handlers.py` for -> real custom handler examples (readme analysis, CI config detection). +> **Reference**: See `packages/darnit-reproducibility/src/darnit_reproducibility/handlers.py` +> and `packages/darnit-gittuf/src/darnit_gittuf/handlers.py` for real custom +> handlers, registered in each package's `implementation.py`. --- @@ -1767,10 +1768,9 @@ from darnit.core.handlers import get_handler_registry | MCP tool handler registry | `packages/darnit/src/darnit/core/handlers.py` | | Reference implementation | `packages/darnit-baseline/src/darnit_baseline/implementation.py` | | Reference TOML | `packages/darnit-baseline/src/darnit_baseline/openssf-baseline.toml` | -| Example implementation | `packages/darnit-example/src/darnit_example/implementation.py` | -| Example TOML config | `packages/darnit-example/example-hygiene.toml` | -| Example custom handlers | `packages/darnit-example/src/darnit_example/handlers.py` | -| Example tests | `tests/darnit_example/` | +| Plugin template | `packages/darnit-hello/` | +| Plugin with custom step types | `packages/darnit-reproducibility/src/darnit_reproducibility/implementation.py` | +| Custom step type tests | `tests/darnit_reproducibility/test_handlers.py` | | Framework spec | `docs/architecture/framework-design.md` | | Composition resolver | `packages/darnit/src/darnit/core/composition.py` | | Composition spec | `specs/013-plugin-composition/spec.md` | diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 7e52704a..c6d1194b 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -12,7 +12,6 @@ These are static reference docs, not in-flight feature specs (those live under ` - [Sieve handler authoring](./sieve-handler-authoring.md) -- contract for writing new sieve handlers - [Shared handlers](./shared-handlers.md) -- built-in handlers usable across implementations - [Implementation-provided tools](./implementation-provided-tools.md) -- MCP tool exposure model -- [Example plugin](./example-plugin.md) -- canonical worked example ## Audit lifecycle diff --git a/docs/architecture/example-plugin.md b/docs/architecture/example-plugin.md deleted file mode 100644 index 28013cf1..00000000 --- a/docs/architecture/example-plugin.md +++ /dev/null @@ -1,111 +0,0 @@ -## ADDED Requirements - -### Requirement: Package satisfies ComplianceImplementation protocol -The `darnit-example` package SHALL export a `register()` function that returns an object satisfying the `ComplianceImplementation` protocol. The implementation SHALL pass `isinstance(register(), ComplianceImplementation)`. - -#### Scenario: Protocol compliance check -- **WHEN** `register()` is called -- **THEN** the returned object satisfies all required protocol properties (`name`, `display_name`, `version`, `spec_version`) and the method `get_framework_config_path` - -#### Scenario: Entry point discovery -- **WHEN** the package is installed via `uv sync` -- **THEN** darnit's plugin discovery system finds it under the `darnit.implementations` entry point group with key `example-hygiene` - -### Requirement: Package defines 8 controls across 2 levels -The framework TOML SHALL define exactly 8 controls: 6 at level 1 and 2 at level 2. Control IDs SHALL follow the `PH-{DOMAIN}-NN` format. - -#### Scenario: Level 1 controls -- **WHEN** the controls are loaded from `get_framework_config_path()` with `load_controls_from_framework` -- **THEN** 6 controls have level 1, with IDs `PH-DOC-01`, `PH-DOC-02`, `PH-DOC-03`, `PH-SEC-01`, `PH-CFG-01`, `PH-CFG-02` - -#### Scenario: Level 2 controls -- **WHEN** the controls are loaded from `get_framework_config_path()` with `load_controls_from_framework` -- **THEN** 2 controls have level 2, with IDs `PH-QA-01`, `PH-CI-01` - -#### Scenario: Total control count -- **WHEN** the controls are loaded from `get_framework_config_path()` with `load_controls_from_framework` -- **THEN** exactly 8 `ControlSpec` instances are returned - -### Requirement: TOML-defined controls use file_exists pass -Controls `PH-DOC-01`, `PH-DOC-02`, `PH-SEC-01`, `PH-CFG-01`, `PH-CFG-02`, and `PH-QA-01` SHALL be defined declaratively in the TOML configuration file using `file_exists` deterministic passes. - -#### Scenario: TOML control for README -- **WHEN** the sieve evaluates `PH-DOC-01` against a directory containing `README.md` -- **THEN** the deterministic pass returns `PASS` - -#### Scenario: TOML control for missing file -- **WHEN** the sieve evaluates `PH-CFG-01` against a directory without `.gitignore` -- **THEN** the deterministic pass returns `FAIL` - -### Requirement: Python controls use factory function pattern -Controls `PH-DOC-03` and `PH-CI-01` SHALL be defined in Python using the factory function pattern (`_create_*_check() -> Callable[[CheckContext], PassResult]`) and registered via `register_control()`. - -#### Scenario: README description check passes -- **WHEN** `PH-DOC-03` is evaluated against a README with substantive content (>20 chars beyond title) -- **THEN** the deterministic pass returns `PASS` - -#### Scenario: README description check fails for title-only -- **WHEN** `PH-DOC-03` is evaluated against a README containing only a heading -- **THEN** the deterministic pass returns `FAIL` - -#### Scenario: CI config detection via glob -- **WHEN** `PH-CI-01` is evaluated against a directory with `.github/workflows/ci.yml` -- **THEN** the deterministic pass returns `PASS` - -#### Scenario: CI config missing -- **WHEN** `PH-CI-01` is evaluated against a directory with no CI configuration files -- **THEN** the deterministic pass returns `FAIL` - -### Requirement: Multi-phase sieve demonstration -Control `PH-SEC-01` SHALL define deterministic, pattern, and manual passes to demonstrate the multi-phase sieve pipeline. Control `PH-DOC-03` SHALL define deterministic, pattern (custom analyzer), and manual passes. - -#### Scenario: Security policy found by file existence -- **WHEN** `PH-SEC-01` is evaluated and `SECURITY.md` exists -- **THEN** the deterministic pass returns `PASS` and subsequent passes are not needed - -#### Scenario: README quality pattern analysis -- **WHEN** `PH-DOC-03` pattern pass runs against a README with "Installation" and "Usage" sections -- **THEN** the pattern pass returns `PASS` - -### Requirement: Remediation actions create missing files -The package SHALL provide remediation actions `create_readme` and `create_gitignore` that create template files. Both SHALL support `dry_run` mode and SHALL skip creation if the target file already exists. - -#### Scenario: Dry run does not write -- **WHEN** `create_readme(path, dry_run=True)` is called -- **THEN** no file is created and the result status is `"dry_run"` - -#### Scenario: File creation -- **WHEN** `create_readme(path, dry_run=False)` is called on a directory without README.md -- **THEN** a README.md file is created with the project name as title - -#### Scenario: Skip existing file -- **WHEN** `create_gitignore(path, dry_run=False)` is called on a directory that already has `.gitignore` -- **THEN** the existing file is not modified and the result status is `"skipped"` - -### Requirement: Handler registration with plugin context -The implementation SHALL provide a `register_handlers()` method, its only handler registration method, that registers its sieve step types and at least one MCP tool handler with the framework's handler registries. The handler SHALL be tagged with the plugin name `"example-hygiene"`. - -#### Scenario: Handler appears in registry -- **WHEN** `register_handlers()` is called -- **THEN** the handler `"example_hygiene_check"` is present in the handler registry with plugin context `"example-hygiene"` - -### Requirement: Framework config path resolves to existing file -`get_framework_config_path()` SHALL return a `Path` object pointing to `example-hygiene.toml` that exists on disk. - -#### Scenario: Config path exists -- **WHEN** `get_framework_config_path()` is called -- **THEN** the returned path has filename `example-hygiene.toml` and `path.exists()` is `True` - -### Requirement: Framework integration with minimal changes -The package SHALL integrate into the darnit workspace with only one framework-side change: adding `darnit-example` to the root `pyproject.toml` workspace sources and ruff config. Its modules are importable by handler path because `darnit_example` is the module of its `darnit.implementations` entry point (framework-design.md 6.5); no list in the framework names it. - -#### Scenario: Module path permitted by the entry point -- **WHEN** handler resolution attempts to load a `darnit_example.*` module -- **THEN** the module resolution policy permits the import - -### Requirement: Documentation cross-references -The package README SHALL map each section of `docs/IMPLEMENTATION_GUIDE.md` to its corresponding example file. The implementation guide SHALL reference `packages/darnit-example/` as a working companion example. - -#### Scenario: Guide references example -- **WHEN** a reader opens `docs/IMPLEMENTATION_GUIDE.md` -- **THEN** a callout after Prerequisites points to `packages/darnit-example/` and the Key file paths table includes example package entries diff --git a/docs/packaging-plugins.md b/docs/packaging-plugins.md index cf93bbb4..16d62a70 100644 --- a/docs/packaging-plugins.md +++ b/docs/packaging-plugins.md @@ -340,7 +340,7 @@ See `CLAUDE.md` "Three-Layer Architecture" for the canonical reference. | [`packages/darnit-hello/`](../packages/darnit-hello/) | Single-control toy plugin | The minimum viable shape — copy this to bootstrap | | [`packages/darnit-baseline/`](../packages/darnit-baseline/) | OpenSSF Baseline (production) | TOML+Python handlers, multi-level scoring, custom MCP tools, remediation suite | | [`packages/darnit-gittuf/`](../packages/darnit-gittuf/) | Gittuf policy checks | Compact real-world plugin (3 controls), Python handlers | -| [`packages/darnit-example/`](../packages/darnit-example/) | Example/teaching framework | Custom controls, custom tools, remediation patterns | +| [`packages/darnit-reproducibility/`](../packages/darnit-reproducibility/) | Scientific reproducibility checks | Custom step types registered in `register_handlers()`, corpus-tested handlers | --- diff --git a/docs/threatmodel/findings/python-info_disc-open_call.md b/docs/threatmodel/findings/python-info_disc-open_call.md index 8b3eecac..13efd5ac 100644 --- a/docs/threatmodel/findings/python-info_disc-open_call.md +++ b/docs/threatmodel/findings/python-info_disc-open_call.md @@ -107,6 +107,8 @@ ## All Instances +> **Note (#487):** `packages/darnit-example/` was removed in 0.2.0; instances 46-48 below no longer exist. The two `handlers.py` reads moved with the step types to `packages/darnit-testchecks/src/darnit_testchecks/handlers.py`, a test-only package. + | # | File | Line | Severity | Confidence | Status | |---|------|------|----------|------------|--------| | 1 | `docs/examples/python-framework/example_framework/implementation.py` | 175 | MEDIUM | 0.40 | Mitigated | diff --git a/docs/threatmodel/findings/python-sink-dangerous_attr.md b/docs/threatmodel/findings/python-sink-dangerous_attr.md index 29fed237..6b59bab6 100644 --- a/docs/threatmodel/findings/python-sink-dangerous_attr.md +++ b/docs/threatmodel/findings/python-sink-dangerous_attr.md @@ -40,6 +40,8 @@ **Examples:** `docs/examples/python-framework/example_framework/implementation.py:400`, `docs/examples/python-framework/example_framework/implementation.py:603`, `scripts/create-example-test-repo.py:142` ...and 14 more. +> **Note (#487):** `scripts/create-example-test-repo.py` was removed in 0.2.0; its instances below no longer exist. + ### Strategy 6 (1 instances) > Single git-remote-get-url invocation to derive repository display name. Fixed command, no user input, 5-second timeout, failure gracefully falls back to directory basename. diff --git a/packages/darnit-example/README.md b/packages/darnit-example/README.md deleted file mode 100644 index 12b40a95..00000000 --- a/packages/darnit-example/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# darnit-example - -A working example of a [darnit](../darnit/) compliance plugin that implements a -simple "Project Hygiene Standard" with 8 controls across 2 maturity levels. - -This package is the companion reference implementation for -[docs/IMPLEMENTATION_GUIDE.md](../../docs/IMPLEMENTATION_GUIDE.md). Every pattern -described in that guide has a concrete counterpart here. - -## Controls - -### Level 1 — Basic Project Setup (6 controls) - -| ID | Name | Defined In | Description | -|----|------|-----------|-------------| -| `PH-DOC-01` | ReadmeExists | TOML | Project has a README file | -| `PH-DOC-02` | LicenseExists | TOML | Project has a LICENSE file | -| `PH-DOC-03` | ReadmeHasDescription | Python | README contains a description | -| `PH-SEC-01` | SecurityPolicyExists | TOML | Project has a security policy | -| `PH-CFG-01` | GitignoreExists | TOML | Project has a .gitignore | -| `PH-CFG-02` | EditorConfigExists | TOML | Project has an .editorconfig | - -### Level 2 — Quality Practices (2 controls) - -| ID | Name | Defined In | Description | -|----|------|-----------|-------------| -| `PH-QA-01` | ContributingGuideExists | TOML | Project has a CONTRIBUTING guide | -| `PH-CI-01` | CIConfigExists | Python | Project has CI/CD configuration | - -## Mapping to IMPLEMENTATION_GUIDE.md - -| Guide Section | Example File | -|--------------|-------------| -| Step 1: Package skeleton | `pyproject.toml`, `src/darnit_example/__init__.py` | -| Step 2: Implementation class | `src/darnit_example/implementation.py` | -| Step 3: TOML config | `example-hygiene.toml` | -| Step 4: Python handlers | `src/darnit_example/handlers.py` | -| Step 5: Remediation | `src/darnit_example/remediation/` | -| Step 6: Handler registration | `src/darnit_example/tools.py` | -| Step 7: Testing | `tests/darnit_example/` | - -## Design Choices - -- **6 TOML + 2 Python controls** — shows that most checks need no Python code -- **PH-SEC-01 has 3 pass types** — demonstrates the multi-phase sieve - (deterministic → pattern → manual) -- **PH-DOC-03 uses a custom analyzer** — shows factory function pattern -- **PH-CI-01 uses glob patterns** — shows dynamic file matching in Python -- **No API checks** — keeps the example runnable offline - -## Running Tests - -```bash -# From the repository root -uv run pytest tests/darnit_example/ -v -``` diff --git a/packages/darnit-example/example-hygiene.toml b/packages/darnit-example/example-hygiene.toml deleted file mode 100644 index 10ad3b5c..00000000 --- a/packages/darnit-example/example-hygiene.toml +++ /dev/null @@ -1,436 +0,0 @@ -# Project Hygiene Standard Framework Definition -# Example darnit implementation with 8 controls across 2 maturity levels. -# -# This file accompanies the darnit-example package and demonstrates how to -# define controls declaratively in TOML. See docs/IMPLEMENTATION_GUIDE.md. - -[metadata] -name = "example-hygiene" -display_name = "Project Hygiene Standard (Example)" -version = "0.1.0" -schema_version = "0.1.0-alpha" -spec_version = "PH v1.0" -description = "Example project hygiene controls for open source projects" - -# ============================================================================= -# Templates for Remediation -# ============================================================================= - -[templates.readme_standard] -description = "Standard README.md template for new projects" -content = """# $REPO - -A brief description of what this project does. - -## Installation - -```bash -# Add installation instructions here -``` - -## Usage - -```bash -# Add usage examples here -``` - -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute. - -## License - -See [LICENSE](LICENSE) for details. -""" - -[templates.gitignore_standard] -description = "Standard .gitignore template" -content = """# OS files -.DS_Store -Thumbs.db - -# Editor files -*.swp -*.swo -*~ -.idea/ -.vscode/ - -# Build artifacts -build/ -dist/ -*.egg-info/ - -# Virtual environments -.venv/ -venv/ -env/ - -# Byte-compiled / optimized files -__pycache__/ -*.py[cod] -*$py.class -""" - -[templates.license_mit] -description = "MIT License template" -content = """MIT License - -Copyright (c) $YEAR $OWNER - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -[templates.security_policy] -description = "Basic security policy template" -content = """# Security Policy - -## Reporting a Vulnerability - -To report a security vulnerability, please open an issue or email the maintainers. - -We will acknowledge receipt within 48 hours and provide a timeline for a fix. -""" - -[templates.editorconfig_standard] -description = "Standard .editorconfig template" -content = """root = true - -[*] -indent_style = space -indent_size = 4 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true -""" - -[templates.contributing_standard] -description = "Basic CONTRIBUTING guide template" -content = """# Contributing to $REPO - -## How to Contribute - -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Open a pull request - -## Code of Conduct - -Be respectful and constructive in all interactions. -""" - -[templates.ci_github_actions] -description = "Minimal GitHub Actions CI workflow" -content = """name: CI -on: [push, pull_request] -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 -""" - -# ============================================================================= -# Context Definitions -# ============================================================================= - -[context.project_name] -type = "string" -prompt = "What is the project name?" -hint = "Used for README template rendering" -affects = ["PH-DOC-01"] -store_as = "project.name" -auto_detect = true -required = false - -# ============================================================================= -# Level 1 Controls — Basic Project Setup (6 controls) -# ============================================================================= - -# --- PH-DOC-01: ReadmeExists --- -[controls."PH-DOC-01"] -name = "ReadmeExists" -description = "Project has a README file" -tags = { level = 1, domain = "DOC", documentation = true } -help_md = """Every project should have a README explaining what it does. - -**Remediation:** -1. Create a README.md in the project root -2. Include project name, description, and basic usage -""" - -[[controls."PH-DOC-01".passes]] -handler = "file_exists" -files = ["README.md", "README", "README.rst", "README.txt"] - -[[controls."PH-DOC-01".passes]] -handler = "manual" -steps = [ - "Check that a README file exists in the project root", - "Verify it contains meaningful content (not just a title)", -] - -[controls."PH-DOC-01".on_pass] -project_update = { "documentation.readme.path" = "README.md" } - -[controls."PH-DOC-01".remediation] -handler = "create_readme" -safe = true - -[controls."PH-DOC-01".remediation.file_create] -path = "README.md" -template = "readme_standard" -overwrite = false - -[controls."PH-DOC-01".remediation.project_update] -set = { "documentation.readme.path" = "README.md" } - -# --- PH-DOC-02: LicenseExists --- -[controls."PH-DOC-02"] -name = "LicenseExists" -description = "Project has a LICENSE file" -tags = { level = 1, domain = "DOC", documentation = true, legal = true } -help_md = """A LICENSE file clarifies how others can use the project. - -**Remediation:** -1. Choose a license (e.g., MIT, Apache-2.0, GPL) -2. Create a LICENSE file in the project root -""" - -[[controls."PH-DOC-02".passes]] -handler = "file_exists" -files = ["LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md"] - -[[controls."PH-DOC-02".passes]] -handler = "manual" -steps = [ - "Check that a LICENSE file exists in the project root", - "Verify it contains a recognized open source license", -] - -[controls."PH-DOC-02".remediation] -safe = true - -[controls."PH-DOC-02".remediation.file_create] -path = "LICENSE" -template = "license_mit" -overwrite = false - -[controls."PH-DOC-02".remediation.project_update] -set = { "legal.license.path" = "LICENSE" } - -# --- PH-DOC-03: ReadmeHasDescription (custom sieve handlers) --- -[controls."PH-DOC-03"] -name = "ReadmeHasDescription" -description = "README contains a project description" -tags = { level = 1, domain = "DOC", documentation = true } -help_md = """The README should contain a meaningful description of the project, -not just a title. - -**Remediation:** -1. Open the README file -2. Add a paragraph describing the project purpose and usage -""" - -[[controls."PH-DOC-03".passes]] -handler = "readme_description" - -[[controls."PH-DOC-03".passes]] -handler = "readme_quality" - -[[controls."PH-DOC-03".passes]] -handler = "manual" -steps = [ - "Open the README file", - "Verify it contains a meaningful project description", - "Check that it explains what the project does and how to use it", -] - -# --- PH-SEC-01: SecurityPolicyExists (multi-phase) --- -[controls."PH-SEC-01"] -name = "SecurityPolicyExists" -description = "Project has a security policy for vulnerability reporting" -tags = { level = 1, domain = "SEC", security = true } -help_md = """A SECURITY.md tells users how to report vulnerabilities. - -**Remediation:** -1. Create SECURITY.md in the project root or .github/ directory -2. Include instructions for reporting vulnerabilities -3. Specify a response timeline -""" - -[[controls."PH-SEC-01".passes]] -handler = "file_exists" -files = ["SECURITY.md", ".github/SECURITY.md", "docs/SECURITY.md"] - -[[controls."PH-SEC-01".passes]] -handler = "regex" -files = ["README.md", "README.rst", "README"] -pattern = { patterns = { security_section = "(?i)(security|vulnerability|report.*vulnerabilit)" } } -pass_if_any = true - -[[controls."PH-SEC-01".passes]] -handler = "manual" -steps = [ - "Look for SECURITY.md in the repository root or .github/ directory", - "Verify it contains vulnerability reporting instructions", - "Check for a security contact or email address", -] - -[controls."PH-SEC-01".on_pass] -project_update = { "security.policy.path" = "SECURITY.md" } - -[controls."PH-SEC-01".remediation] -safe = true - -[controls."PH-SEC-01".remediation.file_create] -path = "SECURITY.md" -template = "security_policy" -overwrite = false - -[controls."PH-SEC-01".remediation.project_update] -set = { "security.policy.path" = "SECURITY.md" } - -# --- PH-CFG-01: GitignoreExists --- -[controls."PH-CFG-01"] -name = "GitignoreExists" -description = "Project has a .gitignore file" -tags = { level = 1, domain = "CFG", configuration = true } -help_md = """A .gitignore prevents committing build artifacts and secrets. - -**Remediation:** -1. Create a .gitignore in the project root -2. Add patterns for your language/framework -""" - -[[controls."PH-CFG-01".passes]] -handler = "file_exists" -files = [".gitignore"] - -[controls."PH-CFG-01".remediation] -handler = "create_gitignore" -safe = true - -[controls."PH-CFG-01".remediation.file_create] -path = ".gitignore" -template = "gitignore_standard" -overwrite = false - -# --- PH-CFG-02: EditorConfigExists --- -[controls."PH-CFG-02"] -name = "EditorConfigExists" -description = "Project has an .editorconfig file" -tags = { level = 1, domain = "CFG", configuration = true } -help_md = """An .editorconfig ensures consistent formatting across editors. - -**Remediation:** -1. Create an .editorconfig in the project root -2. Define indent style, charset, and line endings -""" - -[[controls."PH-CFG-02".passes]] -handler = "file_exists" -files = [".editorconfig"] - -[controls."PH-CFG-02".remediation] -safe = true - -[controls."PH-CFG-02".remediation.file_create] -path = ".editorconfig" -template = "editorconfig_standard" -overwrite = false - -# ============================================================================= -# Level 2 Controls — Quality Practices (2 controls) -# ============================================================================= - -# --- PH-QA-01: ContributingGuideExists --- -[controls."PH-QA-01"] -name = "ContributingGuideExists" -description = "Project has a CONTRIBUTING guide" -tags = { level = 2, domain = "QA", documentation = true, quality = true } -help_md = """A CONTRIBUTING guide helps new contributors get started. - -**Remediation:** -1. Create CONTRIBUTING.md in the project root -2. Include setup instructions, coding standards, and PR process -""" - -[[controls."PH-QA-01".passes]] -handler = "file_exists" -files = ["CONTRIBUTING.md", "CONTRIBUTING", "CONTRIBUTING.rst", ".github/CONTRIBUTING.md"] - -[controls."PH-QA-01".remediation] -safe = true - -[controls."PH-QA-01".remediation.file_create] -path = "CONTRIBUTING.md" -template = "contributing_standard" -overwrite = false - -[controls."PH-QA-01".remediation.project_update] -set = { "governance.contributing.path" = "CONTRIBUTING.md" } - -# --- PH-CI-01: CIConfigExists (custom sieve handler) --- -[controls."PH-CI-01"] -name = "CIConfigExists" -description = "Project has CI/CD configuration" -tags = { level = 2, domain = "CI", quality = true } -help_md = """A CI/CD pipeline ensures code quality through automated testing. - -**Remediation:** -1. Create a CI workflow for your platform (GitHub Actions, GitLab CI, etc.) -2. Configure it to run tests on pull requests -""" - -[[controls."PH-CI-01".passes]] -handler = "ci_config" - -[[controls."PH-CI-01".passes]] -handler = "manual" -steps = [ - "Check for CI/CD configuration (GitHub Actions, GitLab CI, etc.)", - "Verify the CI pipeline runs tests on pull requests", -] - -[controls."PH-CI-01".remediation] -safe = true - -[controls."PH-CI-01".remediation.file_create] -path = ".github/workflows/ci.yml" -template = "ci_github_actions" -overwrite = false - -# ============================================================================= -# MCP Server Configuration -# ============================================================================= - -[mcp] -name = "example-hygiene" -description = "Project Hygiene Standard compliance tools (example)" - -[mcp.tools.example_hygiene_check] -handler = "example_hygiene_check" -description = "Run a project hygiene audit. Checks all PH controls via the sieve pipeline and returns a markdown report with pass/fail status and remediation guidance." - -[mcp.tools.remediate_hygiene] -handler = "remediate_hygiene" -description = "Auto-fix failing hygiene controls by creating missing files from TOML-defined templates. Run example_hygiene_check first to see what will be fixed. Use dry_run=true (default) to preview changes." diff --git a/packages/darnit-example/pyproject.toml b/packages/darnit-example/pyproject.toml deleted file mode 100644 index 8a1a7e92..00000000 --- a/packages/darnit-example/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[project] -name = "darnit-example" -version = "0.1.0" -description = "Example 'Project Hygiene Standard' implementation for darnit" -readme = "README.md" -requires-python = ">=3.11" -dependencies = [ - "darnit-core>=0.1.0", -] - -[project.entry-points."darnit.implementations"] -example-hygiene = "darnit_example:register" - -[project.entry-points."darnit.frameworks"] -example-hygiene = "darnit_example:get_framework_path" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/darnit_example"] diff --git a/packages/darnit-example/src/darnit_example/__init__.py b/packages/darnit-example/src/darnit_example/__init__.py deleted file mode 100644 index d12a7f92..00000000 --- a/packages/darnit-example/src/darnit_example/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -"""darnit-example - Example 'Project Hygiene Standard' implementation for darnit. - -This package demonstrates how to build a darnit compliance plugin using the -modern ComplianceImplementation protocol. It implements a simple "Project -Hygiene Standard" with 8 controls across 2 maturity levels. - -See packages/darnit-example/README.md and docs/IMPLEMENTATION_GUIDE.md for -a walkthrough of how this package is structured. - -Usage: - # Automatic registration via entry points - from darnit.core.discovery import discover_implementations - implementations = discover_implementations() - hygiene = implementations.get("example-hygiene") - - # Direct access - from darnit_example import register - implementation = register() - - # Framework path for declarative config system - from darnit_example import get_framework_path - path = get_framework_path() # Returns Path to example-hygiene.toml -""" - -__version__ = "0.1.0" - -from pathlib import Path - - -def get_framework_path() -> Path | None: - """Get the path to the example hygiene framework TOML file. - - Returns: - Path: Absolute path to example-hygiene.toml, or None if not found. - """ - from .implementation import ExampleHygieneImplementation - - return ExampleHygieneImplementation().get_framework_config_path() - - -def register(): - """Register the Example Hygiene implementation with darnit. - - This function is called by darnit's plugin discovery system via entry points. - - Returns: - ExampleHygieneImplementation: The registered implementation instance. - """ - from .implementation import ExampleHygieneImplementation - - return ExampleHygieneImplementation() diff --git a/packages/darnit-example/src/darnit_example/implementation.py b/packages/darnit-example/src/darnit_example/implementation.py deleted file mode 100644 index 6d1db3a9..00000000 --- a/packages/darnit-example/src/darnit_example/implementation.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Example Hygiene implementation for darnit. - -This module provides the ExampleHygieneImplementation class that implements -the darnit ComplianceImplementation protocol for a simple "Project Hygiene -Standard" with 8 controls across 2 maturity levels. -""" - -from pathlib import Path - - -class ExampleHygieneImplementation: - """Example 'Project Hygiene Standard' implementation for darnit. - - This implementation provides 8 controls across 2 maturity levels: - - Level 1: 6 controls (basic project setup) - - Level 2: 2 controls (quality practices) - """ - - @property - def name(self) -> str: - return "example-hygiene" - - @property - def display_name(self) -> str: - return "Project Hygiene Standard (Example)" - - @property - def version(self) -> str: - return "0.1.0" - - @property - def spec_version(self) -> str: - return "PH v1.0" - - def get_framework_config_path(self) -> Path | None: - """Get path to the example hygiene framework TOML file. - - Returns: - Path to example-hygiene.toml in the package root. - """ - # Navigate from implementation.py -> darnit_example -> src -> darnit-example -> toml - return Path(__file__).parent.parent.parent / "example-hygiene.toml" - - def register_handlers(self) -> None: - """Register the plugin's sieve step types and its MCP tool handlers.""" - from darnit.core.handlers import get_handler_registry - from darnit.sieve.handler_registry import get_sieve_handler_registry - - from . import handlers, tools - - sieve_registry = get_sieve_handler_registry() - sieve_registry.set_plugin_context(self.name) - - sieve_registry.register( - "readme_description", - phase="deterministic", - handler_fn=handlers.readme_description_handler, - description="Check README has substantive content", - settings={"readme_names"}, - ) - sieve_registry.register( - "readme_quality", - phase="pattern", - handler_fn=handlers.readme_quality_handler, - description="Heuristic check for common README sections", - settings={"sections", "min_sections"}, - ) - sieve_registry.register( - "ci_config", - phase="deterministic", - handler_fn=handlers.ci_config_handler, - description="Glob-based search for CI/CD configuration files", - settings={"patterns"}, - ) - - sieve_registry.set_plugin_context(None) - - registry = get_handler_registry() - registry.set_plugin_context(self.name) - - registry.register_handler("example_hygiene_check", tools.example_hygiene_check) - registry.register_handler("remediate_hygiene", tools.remediate_hygiene) - - registry.set_plugin_context(None) - - -__all__ = ["ExampleHygieneImplementation"] diff --git a/packages/darnit-example/src/darnit_example/remediation/__init__.py b/packages/darnit-example/src/darnit_example/remediation/__init__.py deleted file mode 100644 index ddd72429..00000000 --- a/packages/darnit-example/src/darnit_example/remediation/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Remediation actions for the Project Hygiene Standard.""" diff --git a/packages/darnit-example/src/darnit_example/remediation/actions.py b/packages/darnit-example/src/darnit_example/remediation/actions.py deleted file mode 100644 index 38f735fd..00000000 --- a/packages/darnit-example/src/darnit_example/remediation/actions.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Remediation action implementations for the Project Hygiene Standard. - -Each function creates a missing file to satisfy a control. -""" - -import os -from pathlib import Path - - -def create_readme(local_path: str, dry_run: bool = True, **kwargs: object) -> dict: - """Create a README.md file in the project root. - - Args: - local_path: Path to the repository root. - dry_run: If True, report what would be done without writing. - - Returns: - Dict with remediation result. - """ - target = Path(local_path) / "README.md" - if target.exists(): - return { - "status": "skipped", - "message": "README.md already exists", - "path": str(target), - } - - repo_name = os.path.basename(os.path.abspath(local_path)) - content = f"# {repo_name}\n\nA brief description of what this project does.\n" - - if dry_run: - return { - "status": "dry_run", - "message": f"Would create {target}", - "path": str(target), - } - - target.write_text(content, encoding="utf-8") - return { - "status": "created", - "message": f"Created {target}", - "path": str(target), - } - - -def create_gitignore(local_path: str, dry_run: bool = True, **kwargs: object) -> dict: - """Create a .gitignore file in the project root. - - Args: - local_path: Path to the repository root. - dry_run: If True, report what would be done without writing. - - Returns: - Dict with remediation result. - """ - target = Path(local_path) / ".gitignore" - if target.exists(): - return { - "status": "skipped", - "message": ".gitignore already exists", - "path": str(target), - } - - content = """\ -# OS files -.DS_Store -Thumbs.db - -# Editor files -*.swp -*.swo -*~ -.idea/ -.vscode/ - -# Build artifacts -build/ -dist/ -*.egg-info/ - -# Virtual environments -.venv/ -venv/ - -# Byte-compiled files -__pycache__/ -*.py[cod] -""" - - if dry_run: - return { - "status": "dry_run", - "message": f"Would create {target}", - "path": str(target), - } - - target.write_text(content, encoding="utf-8") - return { - "status": "created", - "message": f"Created {target}", - "path": str(target), - } diff --git a/packages/darnit-example/src/darnit_example/tools.py b/packages/darnit-example/src/darnit_example/tools.py deleted file mode 100644 index 4168d271..00000000 --- a/packages/darnit-example/src/darnit_example/tools.py +++ /dev/null @@ -1,375 +0,0 @@ -"""MCP tool handlers for the Project Hygiene Standard. - -This module provides: -- example_hygiene_check: audit all PH controls via the sieve pipeline -- remediate_hygiene: auto-fix failing controls using TOML-defined templates -""" - -from __future__ import annotations - -from pathlib import Path - -# ============================================================================= -# Shared helpers -# ============================================================================= - - -def _load_all_controls(repo_path: Path, level: int): - """Load TOML + Python controls, filter by level, sort.""" - from darnit.config import ( - load_controls_from_effective, - load_effective_config_by_name, - ) - from darnit.sieve.registry import get_control_registry - - config = load_effective_config_by_name("example-hygiene") - toml_controls = load_controls_from_effective(config) - - registry = get_control_registry() - toml_ids = {c.control_id for c in toml_controls} - python_controls = [ - spec - for spec in registry.get_all_specs() - if spec.control_id.startswith("PH-") and spec.control_id not in toml_ids - ] - - all_controls = toml_controls + python_controls - all_controls = [c for c in all_controls if (c.level or 0) <= level] - all_controls.sort(key=lambda c: c.control_id) - return all_controls - - -def _run_audit(repo_path: Path, controls): - """Run sieve on a list of controls, return legacy result dicts.""" - from darnit.sieve import CheckContext, SieveOrchestrator - - orchestrator = SieveOrchestrator() - results: list[dict] = [] - - for control in controls: - context = CheckContext( - owner="", - repo=repo_path.name, - local_path=str(repo_path), - default_branch="main", - control_id=control.control_id, - control_metadata={ - "name": control.name, - "description": control.description, - }, - ) - result = orchestrator.verify(control, context) - results.append(result.to_legacy_dict()) - - return results - - -def _load_framework_config(): - """Load the example-hygiene TOML as a FrameworkConfig.""" - import tomllib - - from darnit.config.framework_schema import FrameworkConfig - from darnit.core.discovery import get_implementation - - impl = get_implementation("example-hygiene") - if not impl: - msg = "example-hygiene implementation not found" - raise RuntimeError(msg) - - toml_path = impl.get_framework_config_path() - if not toml_path or not toml_path.exists(): - msg = f"Framework TOML not found: {toml_path}" - raise RuntimeError(msg) - - with open(toml_path, "rb") as f: - raw = tomllib.load(f) - - return FrameworkConfig(**raw) - - -# ============================================================================= -# Audit tool -# ============================================================================= - - -async def example_hygiene_check( - local_path: str = ".", - level: int = 2, -) -> str: - """Run a project hygiene audit via the sieve pipeline. - - Loads all PH controls (both TOML-defined and Python-defined), runs each - through the sieve orchestrator, and returns a markdown audit report. - - Args: - local_path: Path to the repository to check. - level: Maximum maturity level to check (1 or 2). - - Returns: - Markdown-formatted audit report with pass/fail details. - """ - repo_path = Path(local_path).resolve() - if not repo_path.exists(): - return f"Error: Repository path not found: {repo_path}" - - try: - controls = _load_all_controls(repo_path, level) - except Exception as e: - return f"Error loading controls: {e}" - - if not controls: - return "No controls found for the requested level." - - results = _run_audit(repo_path, controls) - return _format_hygiene_report(str(repo_path), level, results) - - -# ============================================================================= -# Remediation tool -# ============================================================================= - - -async def remediate_hygiene( - local_path: str = ".", - dry_run: bool = True, -) -> str: - """Auto-fix failing hygiene controls using TOML-defined templates. - - Runs the audit, then for each failing control that has a remediation - config with file_create, uses the framework's RemediationExecutor to - create the missing file from its template. - - Args: - local_path: Path to the repository to remediate. - dry_run: If True (default), show what would be created without writing. - - Returns: - Markdown-formatted remediation report. - """ - from darnit.config.framework_schema import ( - HandlerInvocation, - RemediationConfig, - ) - from darnit.remediation.executor import RemediationExecutor - - repo_path = Path(local_path).resolve() - if not repo_path.exists(): - return f"Error: Repository path not found: {repo_path}" - - # Load framework config for templates + remediation configs - try: - fw = _load_framework_config() - except Exception as e: - return f"Error loading framework config: {e}" - - # Run audit to find failures - try: - controls = _load_all_controls(repo_path, level=2) - except Exception as e: - return f"Error loading controls: {e}" - - results = _run_audit(repo_path, controls) - failed_ids = {r["id"] for r in results if r.get("status") == "FAIL"} - - if not failed_ids: - return "All controls pass — nothing to remediate." - - # Set up executor with templates from TOML - executor = RemediationExecutor( - local_path=str(repo_path), - repo=repo_path.name, - templates=fw.templates, - ) - - # Remediation configs for Python-defined controls that aren't in the TOML - # [controls] section but whose templates ARE in TOML [templates]. - _python_remediations: dict[str, RemediationConfig] = { - "PH-CI-01": RemediationConfig( - handlers=[ - HandlerInvocation( - handler="file_create", - path=".github/workflows/ci.yml", - template="ci_github_actions", - ), - ], - ), - # PH-DOC-03 is fixed implicitly when PH-DOC-01 creates README.md - # with the readme_standard template (which has description content). - } - - # Apply remediations for each failed control - remediation_results = [] - skipped = [] - - for control_id in sorted(failed_ids): - # Try TOML control config first, then Python-control fallback - rem_cfg = None - control_cfg = fw.controls.get(control_id) - if control_cfg and control_cfg.remediation and control_cfg.remediation.handlers: - rem_cfg = control_cfg.remediation - elif control_id in _python_remediations: - rem_cfg = _python_remediations[control_id] - - if rem_cfg is None: - skipped.append((control_id, "no file_create remediation (fixed by another control)")) - continue - - result = executor.execute(control_id, rem_cfg, dry_run=dry_run) - remediation_results.append(result) - - return _format_remediation_report( - str(repo_path), dry_run, remediation_results, skipped, - ) - - -# ============================================================================= -# Formatters -# ============================================================================= - - -def _format_hygiene_report( - repo_path: str, - level: int, - results: list[dict], -) -> str: - """Format audit results as a markdown report.""" - passed = [r for r in results if r.get("status") == "PASS"] - failed = [r for r in results if r.get("status") == "FAIL"] - other = [r for r in results if r.get("status") not in ("PASS", "FAIL")] - - lines: list[str] = [] - lines.append("# Project Hygiene Audit Report") - lines.append("") - lines.append(f"**Path:** {repo_path}") - lines.append(f"**Level Assessed:** {level}") - lines.append("") - - # -- Summary table -------------------------------------------------------- - lines.append("## Summary") - lines.append("") - lines.append("| Status | Count |") - lines.append("|--------|-------|") - lines.append(f"| Pass | {len(passed)} |") - lines.append(f"| Fail | {len(failed)} |") - if other: - lines.append(f"| Other | {len(other)} |") - lines.append(f"| **Total** | **{len(results)}** |") - lines.append("") - - # -- Results by status ---------------------------------------------------- - lines.append("## Results") - lines.append("") - - if failed: - lines.append(f"### FAIL ({len(failed)})") - lines.append("") - for r in failed: - cid = r.get("id", "?") - lvl = r.get("level", "?") - detail = r.get("details", "No details") - lines.append(f"- **{cid}** (L{lvl}): {detail}") - lines.append("") - - if passed: - lines.append(f"### PASS ({len(passed)})") - lines.append("") - for r in passed: - cid = r.get("id", "?") - lvl = r.get("level", "?") - detail = r.get("details", "") - lines.append(f"- **{cid}** (L{lvl}): {detail}") - lines.append("") - - if other: - lines.append(f"### OTHER ({len(other)})") - lines.append("") - for r in other: - cid = r.get("id", "?") - lvl = r.get("level", "?") - status = r.get("status", "?") - detail = r.get("details", "") - lines.append(f"- **{cid}** (L{lvl}) [{status}]: {detail}") - lines.append("") - - # -- Remediation guidance ------------------------------------------------- - if failed: - lines.append("## Remediation") - lines.append("") - lines.append( - "Run `remediate_hygiene` to auto-fix controls with " - "TOML-defined templates." - ) - lines.append("") - lines.append("| Control | What to Create |") - lines.append("|---------|---------------|") - remediation_map = { - "PH-DOC-01": "README.md", - "PH-DOC-02": "LICENSE", - "PH-DOC-03": "Add a description section to README.md", - "PH-SEC-01": "SECURITY.md", - "PH-CFG-01": ".gitignore", - "PH-CFG-02": ".editorconfig", - "PH-QA-01": "CONTRIBUTING.md", - "PH-CI-01": ".github/workflows/ci.yml", - } - for r in failed: - cid = r.get("id", "?") - fix = remediation_map.get(cid, "See control description") - lines.append(f"| {cid} | {fix} |") - lines.append("") - - return "\n".join(lines) - - -def _format_remediation_report( - repo_path: str, - dry_run: bool, - results: list, - skipped: list[tuple[str, str]], -) -> str: - """Format remediation results as markdown.""" - mode = "DRY RUN" if dry_run else "APPLIED" - succeeded = [r for r in results if r.success] - errored = [r for r in results if not r.success] - - lines: list[str] = [] - lines.append(f"# Hygiene Remediation Report ({mode})") - lines.append("") - lines.append(f"**Path:** {repo_path}") - lines.append("") - - if succeeded: - verb = "Would create" if dry_run else "Created" - lines.append(f"## {verb} ({len(succeeded)})") - lines.append("") - for r in succeeded: - lines.append(f"- **{r.control_id}**: {r.message}") - lines.append("") - - if errored: - lines.append(f"## Errors ({len(errored)})") - lines.append("") - for r in errored: - lines.append(f"- **{r.control_id}**: {r.message}") - lines.append("") - - if skipped: - lines.append(f"## Skipped ({len(skipped)})") - lines.append("") - for cid, reason in skipped: - lines.append(f"- **{cid}**: {reason}") - lines.append("") - - if not dry_run and succeeded: - lines.append( - "Run `example_hygiene_check` to verify the fixes." - ) - lines.append("") - - if dry_run and succeeded: - lines.append( - "Run `remediate_hygiene` with `dry_run=false` to apply these changes." - ) - lines.append("") - - return "\n".join(lines) diff --git a/packages/darnit-hello/README.md b/packages/darnit-hello/README.md index e9fe437c..83ac7ca7 100644 --- a/packages/darnit-hello/README.md +++ b/packages/darnit-hello/README.md @@ -40,11 +40,9 @@ darnit audit --implementation hello 3. Replace `hello.toml` with your own framework + controls. The TOML schema is documented in [`docs/packaging-plugins.md`](https://github.com/kusari-oss/darnit/blob/main/docs/packaging-plugins.md). 4. Publish to PyPI (or a private index), and darnit will discover it on any host where both are installed. -## Why this exists separately from `darnit-example` +## Where to go next -`darnit-example` is a fuller reference implementation that exercises Python control handlers, custom tools, remediation actions, and multi-level scoring. It's a learning tool but it's a lot to read. - -`darnit-hello` is deliberately the smallest plugin that the framework will discover, audit, and report against. Read this one first; once you understand the entry-point + ComplianceImplementation surface, look at `darnit-example` for the richer patterns. +`darnit-hello` is the plugin template: deliberately the smallest plugin that the framework will discover, audit, and report against. Read this one first. Once you understand the entry-point + ComplianceImplementation surface, look at `darnit-reproducibility` and `darnit-gittuf` for custom step types registered in `register_handlers()`, and `darnit-baseline` for a full framework. (`darnit-testchecks` is a test-only plugin, not a template.) ## License diff --git a/packages/darnit-testchecks/README.md b/packages/darnit-testchecks/README.md index 52dc604c..387303ce 100644 --- a/packages/darnit-testchecks/README.md +++ b/packages/darnit-testchecks/README.md @@ -1,28 +1,19 @@ # darnit-testchecks -A test compliance framework for [darnit](https://github.com/kusaridev/baseline-mcp) with trivial checks for testing and demonstration purposes. +The test-only plugin for [darnit](https://github.com/kusari-oss/darnit). The test suite uses it; it is not published and is not a template. To write a plugin, start from [`darnit-hello`](../darnit-hello/). -## Overview +It provides two frameworks, both defined in TOML inside `src/darnit_testchecks/`: -This package demonstrates how to create a custom compliance framework using the darnit declarative configuration system. It includes: +- **`testchecks`** (`testchecks.toml`): 12 trivial controls across 3 levels, built only from built-in step types. The CLI and harness tests audit it. +- **`testchecks-steps`** (`testchecks-steps.toml`): 2 controls built on step types this package registers in `register_handlers()` (`testchecks_readme_description`, `testchecks_readme_quality`, `testchecks_ci_config`, in `handlers.py`). Tests use it as the plugin with custom step types. -- **12 trivial controls** across 3 maturity levels -- **Declarative framework definition** in `testchecks.toml` -- **Simple remediations** for basic controls +It also provides in-memory store backends (`darnit_testchecks.stores`). ## Installation -```bash -pip install darnit-testchecks -``` - -Or for development: +It is in the workspace `dev` dependency group, so `uv sync` installs it. -```bash -pip install -e packages/darnit-testchecks -``` - -## Controls +## testchecks controls ### Level 1 - Basic Project Setup @@ -82,15 +73,6 @@ controls: reason: TODOs are acceptable in this project ``` -## Creating Your Own Framework - -Use this package as a template: - -1. Copy the package structure -2. Edit `testchecks.toml` with your controls -3. Declare each control's `passes` in the TOML -4. Update `pyproject.toml` entry points - ## License Apache-2.0 diff --git a/packages/darnit-testchecks/pyproject.toml b/packages/darnit-testchecks/pyproject.toml index f70ab367..5306b341 100644 --- a/packages/darnit-testchecks/pyproject.toml +++ b/packages/darnit-testchecks/pyproject.toml @@ -43,9 +43,15 @@ dev = [ Homepage = "https://github.com/kusari-oss/darnit" Repository = "https://github.com/kusari-oss/darnit" -# Entry points for darnit plugin discovery +# Entry points for darnit plugin discovery. `testchecks` uses only built-in +# step types; `testchecks-steps` is the implementation that registers plugin +# step types (handlers.py). +[project.entry-points."darnit.implementations"] +testchecks-steps = "darnit_testchecks:register" + [project.entry-points."darnit.frameworks"] testchecks = "darnit_testchecks:get_framework_path" +testchecks-steps = "darnit_testchecks:get_steps_framework_path" # In-memory reference store backends (feature 033 T020) [project.entry-points."darnit.stores.project"] @@ -66,7 +72,6 @@ packages = ["src/darnit_testchecks"] [tool.hatch.build.targets.sdist] include = [ "src/", - "testchecks.toml", "README.md", ] diff --git a/packages/darnit-testchecks/src/darnit_testchecks/__init__.py b/packages/darnit-testchecks/src/darnit_testchecks/__init__.py index bf34128c..c634402a 100644 --- a/packages/darnit-testchecks/src/darnit_testchecks/__init__.py +++ b/packages/darnit-testchecks/src/darnit_testchecks/__init__.py @@ -1,10 +1,8 @@ """Test Checks Framework for darnit. -A simple framework with trivial checks for testing the declarative -configuration system. - -This package demonstrates how to create a custom compliance framework -using the darnit declarative configuration system. +The test-only plugin: a framework of trivial checks (testchecks) and one +whose controls use step types this package registers (testchecks-steps). +It is not a template; plugin authors start from darnit-hello. Example usage: ```python @@ -17,10 +15,11 @@ ``` """ +from importlib.resources import files from pathlib import Path __version__ = "0.1.0" -__all__ = ["get_framework_path", "__version__"] +__all__ = ["get_framework_path", "get_steps_framework_path", "register", "__version__"] def get_framework_path() -> Path: @@ -32,16 +31,21 @@ def get_framework_path() -> Path: Returns: Path to testchecks.toml """ - # Framework TOML is in the package root (parent of src/) - package_dir = Path(__file__).parent - # Go up: src/darnit_testchecks -> src -> darnit-testchecks - framework_path = package_dir.parent.parent / "testchecks.toml" - - if not framework_path.exists(): - # Fallback: check if it's installed as a package - # In that case, it might be in the package data - alt_path = package_dir / "testchecks.toml" - if alt_path.exists(): - return alt_path - - return framework_path + path = Path(str(files(__package__) / "testchecks.toml")) + if not path.is_file(): + raise FileNotFoundError(f"testchecks.toml not found in the installed darnit_testchecks package at {path}") + return path + + +def get_steps_framework_path() -> Path: + """Get the path to the testchecks-steps.toml framework definition.""" + from .implementation import CustomStepsImplementation + + return CustomStepsImplementation().get_framework_config_path() + + +def register(): + """Entry point for the testchecks-steps implementation, which registers plugin step types.""" + from .implementation import CustomStepsImplementation + + return CustomStepsImplementation() diff --git a/packages/darnit-example/src/darnit_example/handlers.py b/packages/darnit-testchecks/src/darnit_testchecks/handlers.py similarity index 91% rename from packages/darnit-example/src/darnit_example/handlers.py rename to packages/darnit-testchecks/src/darnit_testchecks/handlers.py index 899d025f..dfae7fee 100644 --- a/packages/darnit-example/src/darnit_example/handlers.py +++ b/packages/darnit-testchecks/src/darnit_testchecks/handlers.py @@ -1,10 +1,12 @@ -"""Custom sieve handlers for the Project Hygiene Standard. +"""Plugin step types for the testchecks-steps framework. -These handlers implement verification logic that requires Python beyond what -built-in TOML handlers can express: -- readme_description: Checks README has substantive content beyond the title -- readme_quality: Heuristic check for common README sections -- ci_config: Glob-based search for CI/CD configuration files +Tests use these to exercise a plugin that registers its own sieve step +types (registration, strict loading of declared settings, discovery). They +register no ceiling, so their results are evidence only: + +- testchecks_readme_description: README has substantive content beyond the title +- testchecks_readme_quality: README mentions common sections +- testchecks_ci_config: a CI/CD configuration file exists """ import glob as glob_module diff --git a/packages/darnit-testchecks/src/darnit_testchecks/implementation.py b/packages/darnit-testchecks/src/darnit_testchecks/implementation.py new file mode 100644 index 00000000..51fbea00 --- /dev/null +++ b/packages/darnit-testchecks/src/darnit_testchecks/implementation.py @@ -0,0 +1,71 @@ +"""The testchecks-steps implementation: a test plugin with its own step types. + +The trivial ``testchecks`` framework uses only built-in step types and needs +no implementation. ``testchecks-steps`` is the fixture for a plugin that +registers custom sieve step types through ``register_handlers()``. +""" + +from importlib.resources import files +from pathlib import Path + + +class CustomStepsImplementation: + """Two controls built on three plugin step types.""" + + @property + def name(self) -> str: + return "testchecks-steps" + + @property + def display_name(self) -> str: + return "Test Checks: Plugin Step Types" + + @property + def version(self) -> str: + return "0.1.0" + + @property + def spec_version(self) -> str: + return "test-steps-v1" + + def get_framework_config_path(self) -> Path | None: + path = Path(str(files(__package__) / "testchecks-steps.toml")) + if not path.is_file(): + raise FileNotFoundError(f"testchecks-steps.toml not found in the installed darnit_testchecks package at {path}") + return path + + def register_handlers(self) -> None: + """Register the plugin's sieve step types.""" + from darnit.sieve.handler_registry import get_sieve_handler_registry + + from . import handlers + + registry = get_sieve_handler_registry() + registry.set_plugin_context(self.name) + + registry.register( + "testchecks_readme_description", + phase="deterministic", + handler_fn=handlers.readme_description_handler, + description="Check README has substantive content", + settings={"readme_names"}, + ) + registry.register( + "testchecks_readme_quality", + phase="pattern", + handler_fn=handlers.readme_quality_handler, + description="Heuristic check for common README sections", + settings={"sections", "min_sections"}, + ) + registry.register( + "testchecks_ci_config", + phase="deterministic", + handler_fn=handlers.ci_config_handler, + description="Glob-based search for CI/CD configuration files", + settings={"patterns"}, + ) + + registry.set_plugin_context(None) + + +__all__ = ["CustomStepsImplementation"] diff --git a/packages/darnit-testchecks/src/darnit_testchecks/testchecks-steps.toml b/packages/darnit-testchecks/src/darnit_testchecks/testchecks-steps.toml new file mode 100644 index 00000000..075bab59 --- /dev/null +++ b/packages/darnit-testchecks/src/darnit_testchecks/testchecks-steps.toml @@ -0,0 +1,42 @@ +# Test Checks: Plugin Step Types +# A test framework whose controls use step types the darnit-testchecks +# plugin registers in register_handlers(). The step types register no +# ceiling, so each control falls through to manual. + +[metadata] +name = "testchecks-steps" +display_name = "Test Checks: Plugin Step Types" +version = "0.1.0" +schema_version = "0.1.0-alpha" +spec_version = "test-steps-v1" +description = "Controls built on plugin-registered step types, for testing" + +[controls."TCS-DOC-01"] +name = "ReadmeHasDescription" +level = 1 +domain = "DOC" +description = "README contains a project description" + +[[controls."TCS-DOC-01".passes]] +handler = "testchecks_readme_description" + +[[controls."TCS-DOC-01".passes]] +handler = "testchecks_readme_quality" +min_sections = 2 + +[[controls."TCS-DOC-01".passes]] +handler = "manual" +steps = ["Verify the README describes what the project does"] + +[controls."TCS-CI-01"] +name = "CIConfigExists" +level = 1 +domain = "CI" +description = "Project has CI/CD configuration" + +[[controls."TCS-CI-01".passes]] +handler = "testchecks_ci_config" + +[[controls."TCS-CI-01".passes]] +handler = "manual" +steps = ["Check for CI/CD configuration (GitHub Actions, GitLab CI, etc.)"] diff --git a/packages/darnit-testchecks/testchecks.toml b/packages/darnit-testchecks/src/darnit_testchecks/testchecks.toml similarity index 97% rename from packages/darnit-testchecks/testchecks.toml rename to packages/darnit-testchecks/src/darnit_testchecks/testchecks.toml index 70e20502..882ad332 100644 --- a/packages/darnit-testchecks/testchecks.toml +++ b/packages/darnit-testchecks/src/darnit_testchecks/testchecks.toml @@ -33,10 +33,6 @@ files = [ "README", ] -[controls."TEST-DOC-01".remediation] -handler = "create_readme" -safe = true - [controls."TEST-DOC-02"] name = "HasChangelog" level = 1 @@ -81,10 +77,6 @@ tags = ["configuration", "git"] handler = "file_exists" files = [".gitignore"] -[controls."TEST-IGN-01".remediation] -handler = "create_gitignore" -safe = true - # ============================================================================= # Level 2 Controls - Code Quality # ============================================================================= diff --git a/packaging/README.md b/packaging/README.md index 1a87edd3..8d263956 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -34,7 +34,7 @@ All releases are tag-driven. Tag patterns: `v` (stable) or `vrc ## Public package set -Authoritative list: [`packaging/pypi/public-packages.txt`](pypi/public-packages.txt). The release workflow refuses to publish anything not in that list. Internal packages (`darnit-example`, `darnit-testchecks`) live in `packages/` but are never published to PyPI. +Authoritative list: [`packaging/pypi/public-packages.txt`](pypi/public-packages.txt). The release workflow refuses to publish anything not in that list. Internal packages (`darnit-testchecks`) live in `packages/` but are never published to PyPI. ## External setup (one-time) diff --git a/pyproject.toml b/pyproject.toml index b59b6de9..bbe932a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,6 @@ members = ["packages/*"] [tool.uv.sources] darnit-core = { workspace = true } darnit-baseline = { workspace = true } -darnit-example = { workspace = true } darnit-gittuf = { workspace = true } darnit-reproducibility = { workspace = true } darnit-hello = { workspace = true } @@ -119,7 +118,6 @@ dev = [ # Internal/test-only workspace packages -- not published to PyPI. Pinned here # so they install for development and tests via `uv sync`, but do not become # runtime dependencies of any published wheel. - "darnit-example", "darnit-hello", "darnit-testchecks", # framework used by tests/darnit/cli/ (feature 024) "darnit-csl", # optional CSL 1.0 framework; exercised by tests/darnit_csl @@ -189,7 +187,7 @@ ignore = [ "docs/examples/*" = ["B", "SIM", "E402"] # Less strict for examples [tool.ruff.lint.isort] -known-first-party = ["darnit", "darnit_baseline", "darnit_example"] +known-first-party = ["darnit", "darnit_baseline"] [tool.bandit] exclude_dirs = ["tests", "docs/examples"] diff --git a/scripts/create-example-test-repo.py b/scripts/create-example-test-repo.py deleted file mode 100755 index 478e7321..00000000 --- a/scripts/create-example-test-repo.py +++ /dev/null @@ -1,411 +0,0 @@ -#!/usr/bin/env python3 -"""Create a test repository for the darnit-example Project Hygiene Standard. - -Generates a minimal Python project that intentionally fails all 8 PH controls, -with a .mcp.json pointing at the example-hygiene MCP server so Claude Code -can audit and remediate it immediately. - -Usage: - # Create a failing repo in /tmp (default): - python scripts/create-example-test-repo.py --no-github - - # Apply quick fixes to make all controls pass: - python scripts/create-example-test-repo.py --remediate /tmp/hygiene-test-repo - -What's intentionally missing (maps to controls): - README.md → PH-DOC-01, PH-DOC-03 - LICENSE → PH-DOC-02 - SECURITY.md → PH-SEC-01 - .gitignore → PH-CFG-01 - .editorconfig → PH-CFG-02 - CONTRIBUTING.md → PH-QA-01 - .github/workflows/ → PH-CI-01 -""" - -from __future__ import annotations - -import argparse -import json -import logging -import subprocess -import sys - -logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") -logger = logging.getLogger(__name__) -import tempfile -from pathlib import Path - -# Resolve darnit workspace root (parent of scripts/) -DARNIT_ROOT = Path(__file__).resolve().parent.parent - - -def create_repo( - repo_name: str, - parent_dir: str | None = None, - create_github: bool = True, - github_org: str | None = None, - make_template: bool = False, -) -> None: - """Create a minimal test repo that fails all PH controls.""" - if parent_dir is None: - # Create a unique temp directory so repeated runs don't collide - parent_dir = tempfile.mkdtemp(prefix="darnit-example-") - - parent_path = Path(parent_dir).resolve() - repo_path = parent_path / repo_name - - if repo_path.exists(): - logger.info(f"\033[0;31mError: Directory '{repo_path}' already exists.\033[0m") - sys.exit(1) - - logger.info("\033[0;32m=== Project Hygiene Test Repo Generator ===\033[0m\n") - logger.info(f"Creating: {repo_path}\n") - - # -- Directory structure -------------------------------------------------- - (repo_path / "src").mkdir(parents=True) - - # -- pyproject.toml (minimal, no license field) --------------------------- - pyproject = """\ -[project] -name = "hygiene-test" -version = "0.0.1" -requires-python = ">=3.10" -""" - (repo_path / "pyproject.toml").write_text(pyproject, encoding="utf-8") - - # -- src/main.py ---------------------------------------------------------- - main_py = """\ -def main(): - logger.info("Hello from hygiene-test!") - logger.info("This repo intentionally has no hygiene files.") - logger.info("Run the example_hygiene_check MCP tool to see what is missing.") - - -if __name__ == "__main__": - main() -""" - (repo_path / "src" / "main.py").write_text(main_py, encoding="utf-8") - - # -- .mcp.json (points back to darnit workspace) ------------------------- - mcp_config = { - "mcpServers": { - "example-hygiene": { - "command": "uv", - "args": [ - "--directory", - str(DARNIT_ROOT), - "run", - "darnit", - "serve", - "--framework", - "example-hygiene", - ], - } - } - } - (repo_path / ".mcp.json").write_text(json.dumps(mcp_config, indent=2) + "\n", encoding="utf-8") - - # -- CLAUDE.md (instructions for Claude Code) ----------------------------- - claude_md = """\ -# Hygiene Test Repo - -This repo intentionally fails all Project Hygiene Standard controls. - -Use the `example_hygiene_check` MCP tool to audit this repository, -then fix each failing control. - -## Quick start - -1. Run the audit: ask Claude to check this repo's hygiene -2. Review the 8 failing controls -3. Fix them one by one (Claude can help create the missing files) - -## What's missing - -| Control | What to create | -|---------|---------------| -| PH-DOC-01 | README.md | -| PH-DOC-02 | LICENSE | -| PH-DOC-03 | README.md with a description section | -| PH-SEC-01 | SECURITY.md | -| PH-CFG-01 | .gitignore | -| PH-CFG-02 | .editorconfig | -| PH-QA-01 | CONTRIBUTING.md | -| PH-CI-01 | .github/workflows/*.yml | -""" - (repo_path / "CLAUDE.md").write_text(claude_md, encoding="utf-8") - - # -- Initialize git ------------------------------------------------------- - try: - subprocess.run( - ["git", "init"], cwd=repo_path, capture_output=True, check=True - ) - subprocess.run( - ["git", "add", "."], cwd=repo_path, capture_output=True, check=True - ) - subprocess.run( - [ - "git", - "commit", - "-m", - "Initial commit - intentionally non-compliant\n\n" - "This repository is designed for testing Project Hygiene Standard\n" - "compliance. It intentionally fails all 8 PH controls.", - ], - cwd=repo_path, - capture_output=True, - check=True, - ) - except subprocess.CalledProcessError as e: - stderr = e.stderr.decode() if e.stderr else str(e) - logger.info(f"\033[0;31mGit error: {stderr}\033[0m") - sys.exit(1) - - logger.info("\033[0;32m✓ Local repository created\033[0m") - - # -- Optional GitHub repo ------------------------------------------------- - if create_github: - _create_github_repo(repo_path, repo_name, github_org, make_template) - - # -- Summary -------------------------------------------------------------- - logger.info(f""" -\033[0;32m=== Repository Created ===\033[0m - - Location: {repo_path} - -\033[1;33mWhat's intentionally MISSING (for testing):\033[0m - ✗ README.md (PH-DOC-01, PH-DOC-03) - ✗ LICENSE (PH-DOC-02) - ✗ SECURITY.md (PH-SEC-01) - ✗ .gitignore (PH-CFG-01) - ✗ .editorconfig (PH-CFG-02) - ✗ CONTRIBUTING.md (PH-QA-01) - ✗ CI workflows (PH-CI-01) - -\033[0;32mNext steps:\033[0m - 1. cd {repo_path} - 2. Open in Claude Code - 3. Ask Claude to run the hygiene audit and fix failures - -\033[0;36mOr apply quick fixes:\033[0m - python scripts/create-example-test-repo.py --remediate {repo_path} -""") - - -def remediate_repo(repo_path_str: str) -> None: - """Apply minimal fixes to make all 8 PH controls pass.""" - repo_path = Path(repo_path_str).resolve() - if not repo_path.exists(): - logger.info(f"\033[0;31mError: '{repo_path}' does not exist.\033[0m") - sys.exit(1) - - logger.info("\033[0;32m=== Applying Quick Remediations ===\033[0m\n") - logger.info(f"Target: {repo_path}\n") - - created: list[str] = [] - - # PH-DOC-01 + PH-DOC-03: README with description (body must be >20 chars) - readme = repo_path / "README.md" - if not readme.exists(): - readme.write_text( - "# hygiene-test-repo\n\n" - "A test project for the Project Hygiene Standard demo.\n", - encoding="utf-8", - ) - created.append("README.md") - - # PH-DOC-02: LICENSE - license_file = repo_path / "LICENSE" - if not license_file.exists(): - license_file.write_text("MIT License - test project for demo purposes.\n", encoding="utf-8") - created.append("LICENSE") - - # PH-SEC-01: SECURITY.md - security = repo_path / "SECURITY.md" - if not security.exists(): - security.write_text( - "# Security Policy\n\nTo report a vulnerability, open an issue.\n", - encoding="utf-8", - ) - created.append("SECURITY.md") - - # PH-CFG-01: .gitignore - gitignore = repo_path / ".gitignore" - if not gitignore.exists(): - gitignore.write_text("*.pyc\n__pycache__/\n", encoding="utf-8") - created.append(".gitignore") - - # PH-CFG-02: .editorconfig - editorconfig = repo_path / ".editorconfig" - if not editorconfig.exists(): - editorconfig.write_text( - "root = true\n\n[*]\nindent_style = space\nindent_size = 4\n", - encoding="utf-8", - ) - created.append(".editorconfig") - - # PH-QA-01: CONTRIBUTING.md - contributing = repo_path / "CONTRIBUTING.md" - if not contributing.exists(): - contributing.write_text("# Contributing\n\nOpen a pull request.\n", encoding="utf-8") - created.append("CONTRIBUTING.md") - - # PH-CI-01: CI config - workflows_dir = repo_path / ".github" / "workflows" - ci_file = workflows_dir / "ci.yml" - if not ci_file.exists(): - workflows_dir.mkdir(parents=True, exist_ok=True) - ci_file.write_text( - "name: CI\n" - "on: [push, pull_request]\n" - "jobs:\n" - " check:\n" - " runs-on: ubuntu-latest\n" - " steps:\n" - " - uses: actions/checkout@v4\n", - encoding="utf-8", - ) - created.append(".github/workflows/ci.yml") - - if created: - logger.info("\033[0;32mCreated files:\033[0m") - for f in created: - logger.info(f" ✓ {f}") - logger.info(f"\n\033[0;32mDone! {len(created)} files created.\033[0m") - logger.info("Re-run the audit to verify all controls pass.") - else: - logger.info("\033[1;33mAll files already exist — nothing to do.\033[0m") - - -def _create_github_repo( - repo_path: Path, - repo_name: str, - github_org: str | None, - make_template: bool, -) -> None: - """Create a GitHub repository using the gh CLI.""" - try: - result = subprocess.run( - ["gh", "auth", "status"], capture_output=True - ) - if result.returncode != 0: - logger.info( - "\033[1;33m⚠ GitHub CLI not authenticated. " - "Skipping GitHub repo creation.\033[0m" - ) - return - except FileNotFoundError: - logger.info( - "\033[1;33m⚠ GitHub CLI (gh) not found. " - "Skipping GitHub repo creation.\033[0m" - ) - return - - if not github_org: - result = subprocess.run( - ["gh", "api", "user", "--jq", ".login"], - capture_output=True, - text=True, - ) - github_org = result.stdout.strip() - logger.info(f"\033[1;33mUsing GitHub user: {github_org}\033[0m") - - logger.info(f"\033[0;32mCreating GitHub repository: {github_org}/{repo_name}\033[0m") - - try: - subprocess.run( - [ - "gh", - "repo", - "create", - f"{github_org}/{repo_name}", - "--public", - "--source", - str(repo_path), - "--remote", - "origin", - "--description", - "Project Hygiene test repo - intentionally non-compliant", - "--push", - ], - capture_output=True, - check=True, - ) - logger.info("\033[0;32m✓ GitHub repository created\033[0m") - - if make_template: - subprocess.run( - [ - "gh", - "api", - "--method", - "PATCH", - "-H", - "Accept: application/vnd.github+json", - f"/repos/{github_org}/{repo_name}", - "-f", - "is_template=true", - ], - capture_output=True, - check=True, - ) - logger.info("\033[0;32m✓ Repository is now a template\033[0m") - - except subprocess.CalledProcessError as e: - stderr = e.stderr.decode() if e.stderr else str(e) - logger.info(f"\033[1;33m⚠ GitHub error: {stderr}\033[0m") - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Create a test repo for the Project Hygiene Standard", - ) - parser.add_argument( - "repo_name", - nargs="?", - default="hygiene-test-repo", - help="Name of the repository (default: hygiene-test-repo)", - ) - parser.add_argument( - "--parent-dir", - default=None, - help=f"Directory to create the repo in (default: {tempfile.gettempdir()})", - ) - parser.add_argument( - "--no-github", - action="store_true", - help="Skip GitHub repository creation", - ) - parser.add_argument( - "--github-org", - default=None, - help="GitHub org or username (default: authenticated user)", - ) - parser.add_argument( - "--template", - action="store_true", - help="Make the GitHub repo a template", - ) - parser.add_argument( - "--remediate", - metavar="PATH", - default=None, - help="Apply quick fixes to an existing test repo instead of creating a new one", - ) - - args = parser.parse_args() - - if args.remediate: - remediate_repo(args.remediate) - else: - create_repo( - repo_name=args.repo_name, - parent_dir=args.parent_dir, - create_github=not args.no_github, - github_org=args.github_org, - make_template=args.template, - ) - - -if __name__ == "__main__": - main() diff --git a/tests/darnit/config/test_expr_references.py b/tests/darnit/config/test_expr_references.py index d25b5307..1af4c4b6 100644 --- a/tests/darnit/config/test_expr_references.py +++ b/tests/darnit/config/test_expr_references.py @@ -97,16 +97,16 @@ def plugin_step_types() -> None: from darnit_csl.implementation import CommunitySpecImplementation from darnit_gittuf.implementation import GittufImplementation from darnit_reproducibility.implementation import ReproducibilityImplementation + from darnit_testchecks.implementation import CustomStepsImplementation from darnit_baseline.implementation import OSPSBaselineImplementation - from darnit_example.implementation import ExampleHygieneImplementation for implementation in ( OSPSBaselineImplementation, CommunitySpecImplementation, - ExampleHygieneImplementation, GittufImplementation, ReproducibilityImplementation, + CustomStepsImplementation, ): implementation().register_handlers() diff --git a/tests/darnit/config/test_strict_framework_loading.py b/tests/darnit/config/test_strict_framework_loading.py index 47749e05..1cd0f57b 100644 --- a/tests/darnit/config/test_strict_framework_loading.py +++ b/tests/darnit/config/test_strict_framework_loading.py @@ -61,16 +61,16 @@ def plugin_step_types() -> None: from darnit_csl.implementation import CommunitySpecImplementation from darnit_gittuf.implementation import GittufImplementation from darnit_reproducibility.implementation import ReproducibilityImplementation + from darnit_testchecks.implementation import CustomStepsImplementation from darnit_baseline.implementation import OSPSBaselineImplementation - from darnit_example.implementation import ExampleHygieneImplementation for implementation in ( OSPSBaselineImplementation, CommunitySpecImplementation, - ExampleHygieneImplementation, GittufImplementation, ReproducibilityImplementation, + CustomStepsImplementation, ): implementation().register_handlers() diff --git a/tests/darnit/core/test_module_path_policy.py b/tests/darnit/core/test_module_path_policy.py index 482b72d2..b4df7efc 100644 --- a/tests/darnit/core/test_module_path_policy.py +++ b/tests/darnit/core/test_module_path_policy.py @@ -41,7 +41,7 @@ def test_allowed_set_comes_from_implementation_entry_points(self) -> None: packages = discovery.implementation_packages() assert {"darnit_baseline", "darnit_csl", "darnit_gittuf", "darnit_reproducibility", "darnit_hello"} <= packages - assert "darnit_testchecks" not in packages + assert "yaml" not in packages def test_allowed_set_is_recomputed_after_discovery_reset(self, monkeypatch: pytest.MonkeyPatch) -> None: import importlib.metadata @@ -69,7 +69,7 @@ class TestRefused: "os.path:exists", "darnit_not_installed_xyz.tools:run", "darnitx.core:thing", - "darnit_testchecks.adapters.builtin:TestCheckAdapter", + "yaml:safe_load", ], ) def test_module_outside_policy_is_refused(self, path: str) -> None: diff --git a/tests/darnit/sieve/test_handler_registry_metadata.py b/tests/darnit/sieve/test_handler_registry_metadata.py index cbf04f41..654650ac 100644 --- a/tests/darnit/sieve/test_handler_registry_metadata.py +++ b/tests/darnit/sieve/test_handler_registry_metadata.py @@ -47,9 +47,9 @@ "repro_provenance_exists", "repro_bit_for_bit", "csl_llm_if_present", - "readme_description", - "readme_quality", - "ci_config", + "testchecks_readme_description", + "testchecks_readme_quality", + "testchecks_ci_config", ) # Keys shipped TOML sets that no code reads. Each is a silent no-op (#481) @@ -66,16 +66,16 @@ def plugin_step_types() -> None: from darnit_csl.implementation import CommunitySpecImplementation from darnit_gittuf.implementation import GittufImplementation from darnit_reproducibility.implementation import ReproducibilityImplementation + from darnit_testchecks.implementation import CustomStepsImplementation from darnit_baseline.implementation import OSPSBaselineImplementation - from darnit_example.implementation import ExampleHygieneImplementation for implementation in ( OSPSBaselineImplementation, CommunitySpecImplementation, - ExampleHygieneImplementation, GittufImplementation, ReproducibilityImplementation, + CustomStepsImplementation, ): implementation().register_handlers() diff --git a/tests/darnit/test_composition.py b/tests/darnit/test_composition.py index fac14f83..6a8a62bd 100644 --- a/tests/darnit/test_composition.py +++ b/tests/darnit/test_composition.py @@ -766,7 +766,7 @@ def test_resolution_performance(): @pytest.mark.unit @pytest.mark.parametrize( "impl_name", - # darnit-baseline and darnit-example load through the production + # darnit-baseline and darnit-testchecks load through the production # loader using the canonical `[metadata]` TOML root. darnit-gittuf # and darnit-hello currently use `[framework]` as the table name — # a pre-existing inconsistency unrelated to this feature (see @@ -774,7 +774,7 @@ def test_resolution_performance(): # here so the test does not regress on a latent issue; the # composition feature is purely additive and does not affect that # inconsistency. - ["openssf-baseline", "example-hygiene"], + ["openssf-baseline", "testchecks-steps"], ) def test_existing_implementations_unaffected(impl_name): """T061 / SC-008: every installed non-composite implementation loads diff --git a/tests/darnit/test_plugin_handler_registration.py b/tests/darnit/test_plugin_handler_registration.py index 6b352e78..303c06aa 100644 --- a/tests/darnit/test_plugin_handler_registration.py +++ b/tests/darnit/test_plugin_handler_registration.py @@ -113,10 +113,10 @@ class TestProtocolMethodNaming: [ ("darnit_baseline.implementation", "OSPSBaselineImplementation"), ("darnit_csl.implementation", "CommunitySpecImplementation"), - ("darnit_example.implementation", "ExampleHygieneImplementation"), ("darnit_gittuf.implementation", "GittufImplementation"), ("darnit_hello.implementation", "HelloImplementation"), ("darnit_reproducibility.implementation", "ReproducibilityImplementation"), + ("darnit_testchecks.implementation", "CustomStepsImplementation"), ], ) def test_in_tree_plugins_use_only_register_handlers(self, module: str, cls: str) -> None: @@ -200,17 +200,17 @@ def test_implementation_with_both_spellings_calls_both(self) -> None: impl.register_sieve_handlers.assert_called_once() @pytest.mark.unit - def test_example_hygiene_loads_with_a_fresh_registry(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Strict loading (044) must find darnit-example's step types without a manual registration.""" + def test_plugin_step_types_load_with_a_fresh_registry(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Strict loading (044) must find a plugin's step types without a manual registration.""" import darnit.sieve.handler_registry as handler_registry from darnit.config.control_loader import load_controls_from_effective from darnit.config.merger import load_effective_config_by_name monkeypatch.setattr(handler_registry, "_sieve_handler_registry", None) - config = load_effective_config_by_name("example-hygiene") + config = load_effective_config_by_name("testchecks-steps") assert len(load_controls_from_effective(config)) == len(config.controls) - assert get_sieve_handler_registry().get("readme_description") is not None + assert get_sieve_handler_registry().get("testchecks_readme_description") is not None @pytest.mark.unit def test_implementation_with_neither_is_a_noop(self) -> None: diff --git a/tests/darnit_example/__init__.py b/tests/darnit_example/__init__.py deleted file mode 100644 index beeab59a..00000000 --- a/tests/darnit_example/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the darnit-example package.""" diff --git a/tests/darnit_example/conftest.py b/tests/darnit_example/conftest.py deleted file mode 100644 index 864505d0..00000000 --- a/tests/darnit_example/conftest.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Shared fixtures for darnit-example tests.""" - -import pytest - -from darnit.sieve.models import CheckContext - - -@pytest.fixture -def make_context(tmp_path): - """Factory fixture that creates a CheckContext pointing at a temp directory. - - Usage: - ctx = make_context() # empty project - ctx = make_context({"README.md": "# Hi"}) # project with files - """ - - def _make(files: dict[str, str] | None = None) -> CheckContext: - if files: - for name, content in files.items(): - filepath = tmp_path / name - filepath.parent.mkdir(parents=True, exist_ok=True) - filepath.write_text(content, encoding="utf-8") - - return CheckContext( - owner="test-owner", - repo="test-repo", - local_path=str(tmp_path), - default_branch="main", - control_id="TEST", - ) - - return _make - - -@pytest.fixture -def empty_project(tmp_path): - """A temporary directory with no files (empty project).""" - return str(tmp_path) - - -@pytest.fixture -def full_project(tmp_path): - """A temporary directory with all hygiene files present.""" - files = { - "README.md": "# Test Project\n\nThis is a test project for validating hygiene controls.\n\n## Installation\n\nRun `pip install test`.\n\n## Usage\n\nJust use it.\n", - "LICENSE": "MIT License\n\nCopyright 2024\n", - "SECURITY.md": "# Security Policy\n\n## Reporting\n\nEmail security@example.com\n", - ".gitignore": "*.pyc\n__pycache__/\n", - ".editorconfig": "root = true\n\n[*]\nindent_style = space\n", - "CONTRIBUTING.md": "# Contributing\n\nPRs welcome.\n", - ".github/workflows/ci.yml": "name: CI\non: push\njobs:\n test:\n runs-on: ubuntu-latest\n", - } - for name, content in files.items(): - filepath = tmp_path / name - filepath.parent.mkdir(parents=True, exist_ok=True) - filepath.write_text(content, encoding="utf-8") - return str(tmp_path) diff --git a/tests/darnit_example/test_implementation.py b/tests/darnit_example/test_implementation.py deleted file mode 100644 index aed370c4..00000000 --- a/tests/darnit_example/test_implementation.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Tests for darnit_example implementation.""" - -import pytest - -from darnit.core.plugin import ComplianceImplementation -from darnit_example import register -from darnit_example.implementation import ExampleHygieneImplementation - - -def _framework_controls(impl): - """Controls as the audit loads them, from the implementation's framework TOML.""" - from darnit.config import load_controls_from_framework - from darnit.config.merger import load_framework_config - - return load_controls_from_framework(load_framework_config(impl.get_framework_config_path())) - - -class TestExampleHygieneImplementation: - """Tests for ExampleHygieneImplementation class.""" - - @pytest.fixture - def impl(self): - return ExampleHygieneImplementation() - - @pytest.mark.unit - def test_properties(self, impl): - assert impl.name == "example-hygiene" - assert impl.display_name == "Project Hygiene Standard (Example)" - assert impl.version == "0.1.0" - assert impl.spec_version == "PH v1.0" - - @pytest.mark.unit - def test_is_compliance_implementation(self, impl): - assert isinstance(impl, ComplianceImplementation) - - @pytest.mark.unit - def test_framework_toml_controls(self, impl): - controls = _framework_controls(impl) - assert len(controls) == 8 - assert sum(c.level == 1 for c in controls) == 6 - assert sum(c.level == 2 for c in controls) == 2 - - @pytest.mark.unit - def test_control_ids_are_ph_format(self, impl): - controls = _framework_controls(impl) - for control in controls: - assert control.control_id.startswith("PH-") - parts = control.control_id.split("-") - assert len(parts) >= 3 - - @pytest.mark.unit - def test_control_domains(self, impl): - controls = _framework_controls(impl) - valid_domains = {"DOC", "SEC", "CFG", "QA", "CI"} - for control in controls: - assert control.domain in valid_domains, f"Invalid domain: {control.domain}" - - @pytest.mark.unit - def test_get_framework_config_path(self, impl): - path = impl.get_framework_config_path() - assert path is not None - assert path.name == "example-hygiene.toml" - assert path.exists() - - -class TestHandlerRegistration: - """Tests for handler registration.""" - - @pytest.fixture(autouse=True) - def clear_registry(self): - from darnit.core.handlers import get_handler_registry - - registry = get_handler_registry() - registry.clear() - yield - registry.clear() - - @pytest.mark.unit - def test_register_handlers_adds_tools(self): - from darnit.core.handlers import get_handler_registry - - impl = ExampleHygieneImplementation() - impl.register_handlers() - - registry = get_handler_registry() - handlers = registry.list_handlers() - - assert len(handlers) > 0 - handler_names = {h.name for h in handlers} - assert "example_hygiene_check" in handler_names - - @pytest.mark.unit - def test_handlers_have_plugin_context(self): - from darnit.core.handlers import get_handler_registry - - impl = ExampleHygieneImplementation() - impl.register_handlers() - - registry = get_handler_registry() - handler_info = registry.get_handler_info("example_hygiene_check") - - assert handler_info is not None - assert handler_info.plugin == "example-hygiene" - -class TestRegisterFunction: - """Tests for the register() entry point function.""" - - @pytest.mark.unit - def test_register_returns_implementation(self): - impl = register() - assert isinstance(impl, ExampleHygieneImplementation) - diff --git a/tests/darnit_example/test_remediation.py b/tests/darnit_example/test_remediation.py deleted file mode 100644 index 77b99742..00000000 --- a/tests/darnit_example/test_remediation.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Tests for remediation actions in darnit-example.""" - -import pytest - -from darnit_example.remediation.actions import create_gitignore, create_readme - - -class TestCreateReadme: - """Tests for create_readme remediation action.""" - - @pytest.mark.unit - def test_dry_run(self, empty_project): - result = create_readme(empty_project, dry_run=True) - assert result["status"] == "dry_run" - assert "README.md" in result["message"] - - @pytest.mark.unit - def test_creates_file(self, empty_project): - result = create_readme(empty_project, dry_run=False) - assert result["status"] == "created" - - import os - assert os.path.exists(os.path.join(empty_project, "README.md")) - - @pytest.mark.unit - def test_skips_existing(self, full_project): - result = create_readme(full_project, dry_run=False) - assert result["status"] == "skipped" - - @pytest.mark.unit - def test_content_has_project_name(self, empty_project): - create_readme(empty_project, dry_run=False) - - import os - with open(os.path.join(empty_project, "README.md")) as f: - content = f.read() - # Should include the directory name as the title - assert content.startswith("# ") - assert len(content) > 10 - - -class TestCreateGitignore: - """Tests for create_gitignore remediation action.""" - - @pytest.mark.unit - def test_dry_run(self, empty_project): - result = create_gitignore(empty_project, dry_run=True) - assert result["status"] == "dry_run" - - @pytest.mark.unit - def test_creates_file(self, empty_project): - result = create_gitignore(empty_project, dry_run=False) - assert result["status"] == "created" - - import os - assert os.path.exists(os.path.join(empty_project, ".gitignore")) - - @pytest.mark.unit - def test_skips_existing(self, full_project): - result = create_gitignore(full_project, dry_run=False) - assert result["status"] == "skipped" - - @pytest.mark.unit - def test_content_has_common_patterns(self, empty_project): - create_gitignore(empty_project, dry_run=False) - - import os - with open(os.path.join(empty_project, ".gitignore")) as f: - content = f.read() - assert "__pycache__" in content - assert ".DS_Store" in content diff --git a/tests/darnit_testchecks/__init__.py b/tests/darnit_testchecks/__init__.py new file mode 100644 index 00000000..757142f0 --- /dev/null +++ b/tests/darnit_testchecks/__init__.py @@ -0,0 +1 @@ +"""Tests for the darnit-testchecks package.""" diff --git a/tests/darnit_example/test_controls.py b/tests/darnit_testchecks/test_handlers.py similarity index 93% rename from tests/darnit_example/test_controls.py rename to tests/darnit_testchecks/test_handlers.py index c2224c73..2632af82 100644 --- a/tests/darnit_example/test_controls.py +++ b/tests/darnit_testchecks/test_handlers.py @@ -1,14 +1,14 @@ -"""Tests for custom sieve handlers in darnit-example.""" +"""Tests for the plugin step types in darnit-testchecks.""" import pytest - -from darnit.sieve.handler_registry import HandlerContext, HandlerResultStatus -from darnit_example.handlers import ( +from darnit_testchecks.handlers import ( ci_config_handler, readme_description_handler, readme_quality_handler, ) +from darnit.sieve.handler_registry import HandlerContext, HandlerResultStatus + def _make_handler_context(tmp_path, files=None): """Create a HandlerContext pointing at a temp directory with optional files.""" @@ -21,7 +21,7 @@ def _make_handler_context(tmp_path, files=None): class TestReadmeHasDescription: - """Tests for PH-DOC-03: ReadmeHasDescription handler.""" + """Tests for the TCS-DOC-01 step types.""" @pytest.mark.unit def test_pass_with_description(self, tmp_path): @@ -57,7 +57,7 @@ def test_pattern_pass_with_good_structure(self, tmp_path): class TestCIConfigExists: - """Tests for PH-CI-01: CIConfigExists handler.""" + """Tests for the TCS-CI-01 step type.""" @pytest.mark.unit def test_pass_with_github_actions(self, tmp_path): diff --git a/tests/darnit_testchecks/test_implementation.py b/tests/darnit_testchecks/test_implementation.py new file mode 100644 index 00000000..c0e0ee4c --- /dev/null +++ b/tests/darnit_testchecks/test_implementation.py @@ -0,0 +1,73 @@ +"""Tests for the testchecks-steps implementation.""" + +import pytest +from darnit_testchecks import get_framework_path, get_steps_framework_path, register +from darnit_testchecks.implementation import CustomStepsImplementation + +from darnit.core.plugin import ComplianceImplementation + +STEP_TYPES = ("testchecks_readme_description", "testchecks_readme_quality", "testchecks_ci_config") + + +def _framework_controls(impl): + """Controls as the audit loads them, from the implementation's framework TOML.""" + from darnit.config import load_controls_from_framework + from darnit.config.merger import load_framework_config + + return load_controls_from_framework(load_framework_config(impl.get_framework_config_path())) + + +class TestCustomStepsImplementation: + @pytest.fixture + def impl(self): + return CustomStepsImplementation() + + @pytest.mark.unit + def test_properties(self, impl): + assert impl.name == "testchecks-steps" + assert impl.display_name == "Test Checks: Plugin Step Types" + assert impl.version == "0.1.0" + assert impl.spec_version == "test-steps-v1" + + @pytest.mark.unit + def test_is_compliance_implementation(self, impl): + assert isinstance(impl, ComplianceImplementation) + + @pytest.mark.unit + def test_framework_toml_controls(self, impl): + controls = _framework_controls(impl) + assert sorted(c.control_id for c in controls) == ["TCS-CI-01", "TCS-DOC-01"] + + @pytest.mark.unit + def test_framework_tomls_ship_inside_the_package(self, impl): + """Feature 021: both framework TOMLs resolve inside src/darnit_testchecks/.""" + steps = impl.get_framework_config_path() + assert steps == get_steps_framework_path() + assert steps.name == "testchecks-steps.toml" + assert get_framework_path().name == "testchecks.toml" + for path in (steps, get_framework_path()): + assert path.is_file() + assert path.parent.name == "darnit_testchecks" + + +class TestHandlerRegistration: + @pytest.mark.unit + def test_register_handlers_adds_step_types_with_plugin_context(self, monkeypatch): + import darnit.sieve.handler_registry as handler_registry + from darnit.sieve.handler_registry import get_sieve_handler_registry + + monkeypatch.setattr(handler_registry, "_sieve_handler_registry", None) + CustomStepsImplementation().register_handlers() + + registry = get_sieve_handler_registry() + for name in STEP_TYPES: + info = registry.get(name) + assert info is not None, name + assert info.plugin == "testchecks-steps" + assert info.settings is not None + + +class TestRegisterFunction: + @pytest.mark.unit + def test_register_returns_implementation(self): + assert isinstance(register(), CustomStepsImplementation) diff --git a/uv.lock b/uv.lock index 747d1149..2b868922 100644 --- a/uv.lock +++ b/uv.lock @@ -11,7 +11,6 @@ members = [ "darnit-baseline", "darnit-core", "darnit-csl", - "darnit-example", "darnit-gittuf", "darnit-hello", "darnit-mcp", @@ -533,17 +532,6 @@ dependencies = [ [package.metadata] requires-dist = [{ name = "darnit-core", editable = "packages/darnit" }] -[[package]] -name = "darnit-example" -version = "0.1.0" -source = { editable = "packages/darnit-example" } -dependencies = [ - { name = "darnit-core" }, -] - -[package.metadata] -requires-dist = [{ name = "darnit-core", editable = "packages/darnit" }] - [[package]] name = "darnit-gittuf" version = "0.1.1" @@ -596,7 +584,6 @@ parity-tier2 = [ [package.dev-dependencies] dev = [ { name = "darnit-csl" }, - { name = "darnit-example" }, { name = "darnit-hello" }, { name = "darnit-testchecks" }, { name = "jsonschema" }, @@ -627,7 +614,6 @@ provides-extras = ["attestation", "dev", "parity-tier2"] [package.metadata.requires-dev] dev = [ { name = "darnit-csl", editable = "packages/darnit-csl" }, - { name = "darnit-example", editable = "packages/darnit-example" }, { name = "darnit-hello", editable = "packages/darnit-hello" }, { name = "darnit-testchecks", editable = "packages/darnit-testchecks" }, { name = "jsonschema", specifier = ">=4.26.0" },