From 34ce3479b8811b6afc85ccccdb41954be7fe4c1a Mon Sep 17 00:00:00 2001 From: plx Date: Mon, 27 Jul 2026 14:19:18 -0500 Subject: [PATCH 1/6] Add contributor workflow and templates --- .github/ISSUE_TEMPLATE/01-bug.yml | 77 ++++ .../ISSUE_TEMPLATE/02-contract-proposal.yml | 58 +++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/pull_request_template.md | 64 +++ .github/workflows/ci.yml | 11 +- AGENTIC_NAVIGATION_GUIDE.md | 10 + CHANGELOG.md | 5 + CONTRIBUTING.md | 280 ++++++++++++ README.md | 1 + ...026-07-27-issue-70-contributor-workflow.md | 147 +++++++ scripts/check_contributor_templates.py | 404 ++++++++++++++++++ tests/issue_61_workflow_security_policy.rs | 7 +- tests/issue_66_readme_examples.rs | 3 +- tests/issue_69_security_policy.rs | 8 +- tests/issue_70_contributor_workflow.rs | 250 +++++++++++ tests/test_check_contributor_templates.py | 180 ++++++++ 16 files changed, 1504 insertions(+), 6 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/01-bug.yml create mode 100644 .github/ISSUE_TEMPLATE/02-contract-proposal.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md create mode 100644 audits/2026-07-27-issue-70-contributor-workflow.md create mode 100644 scripts/check_contributor_templates.py create mode 100644 tests/issue_70_contributor_workflow.rs create mode 100644 tests/test_check_contributor_templates.py diff --git a/.github/ISSUE_TEMPLATE/01-bug.yml b/.github/ISSUE_TEMPLATE/01-bug.yml new file mode 100644 index 0000000..a76cb87 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bug.yml @@ -0,0 +1,77 @@ +name: "Bug report" +description: "Report reproducible incorrect CLI or guide-validation behavior." +title: "[Bug]: " +labels: + - "bug" +body: + - type: "markdown" + attributes: + value: "Do not put vulnerability details, secrets, personal data, or private paths here. Use https://github.com/plx/agentic-navigation-guide/security/advisories/new for a private security report." + - type: "input" + id: "version" + attributes: + label: "Version or source commit" + description: "Give the installed version or exact source commit and installation method." + placeholder: "0.2.0, commit SHA, or trusted source checkout" + validations: + required: true + - type: "dropdown" + id: "platform" + attributes: + label: "Platform" + description: "Select the operating-system scope and name relevant filesystem details below." + options: + - "Linux" + - "macOS" + - "Windows" + - "Multiple or other" + validations: + required: true + - type: "textarea" + id: "observed" + attributes: + label: "Observed behavior" + description: "Describe the actual status, stdout, stderr, and filesystem result with sensitive values removed." + placeholder: "Command shape, exit status, bounded diagnostic, and resulting state" + validations: + required: true + - type: "textarea" + id: "expected" + attributes: + label: "Expected behavior" + description: "Describe the documented behavior or contract row that should apply." + placeholder: "Link the maintained contract section when possible" + validations: + required: true + - type: "textarea" + id: "reproduction" + attributes: + label: "Minimal deterministic reproduction" + description: "Provide fixed synthetic files and commands that do not touch global Cargo, git, home-directory, or private state." + placeholder: "Smallest clean temporary-workspace reproducer" + validations: + required: true + - type: "textarea" + id: "regression" + attributes: + label: "Proposed red-before-fix regression" + description: "Name the smallest deterministic test that should fail for this reason before a fix." + placeholder: "Test file, test name, expected nonzero status, and failure reason" + validations: + required: true + - type: "textarea" + id: "compatibility" + attributes: + label: "Compatibility and documentation impact" + description: "State affected versions, platforms, CLI or guide contracts, documentation, dependencies, and license attribution." + placeholder: "None, or an explicit impact list" + validations: + required: true + - type: "textarea" + id: "security" + attributes: + label: "Security assessment" + description: "Say whether this is an ordinary public bug. Move suspected vulnerabilities to the private route above before adding details." + placeholder: "Ordinary bug with no security impact, or moved to private reporting" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/02-contract-proposal.yml b/.github/ISSUE_TEMPLATE/02-contract-proposal.yml new file mode 100644 index 0000000..bdb278c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-contract-proposal.yml @@ -0,0 +1,58 @@ +name: "Contract proposal" +description: "Propose a reviewed change to the CLI, guide format, compatibility, or trust boundary." +title: "[Contract]: " +labels: + - "needs-decision" + - "domain: compatibility" +body: + - type: "markdown" + attributes: + value: "Proposals do not change the normative contract until an issue-scoped pull request is reviewed and merged. Do not include private vulnerability details." + - type: "textarea" + id: "current-contract" + attributes: + label: "Current contract and evidence" + description: "Link the maintained contract text, fixture row, test, or documented absence that governs today." + placeholder: "Current rule, evidence ID, and owning issue" + validations: + required: true + - type: "textarea" + id: "proposed-contract" + attributes: + label: "Proposed contract" + description: "State one exact new supported or rejected behavior and its success or failure semantics." + placeholder: "Normative wording plus an executable example" + validations: + required: true + - type: "textarea" + id: "compatibility" + attributes: + label: "Compatibility and alternatives" + description: "Identify migration, versioning, stream, status, package, and rejected-alternative consequences." + placeholder: "Compatible, breaking, or unavailable with rationale" + validations: + required: true + - type: "textarea" + id: "platforms" + attributes: + label: "Platform and trust-boundary impact" + description: "Cover Linux, macOS, Windows, filesystem capability differences, and stable-tree limitations." + placeholder: "Exact platform matrix and unavailable capabilities" + validations: + required: true + - type: "textarea" + id: "security" + attributes: + label: "Security and resource impact" + description: "Describe trust anchors, diagnostics, containment limits, resource bounds, and private-reporting needs without exploit details." + placeholder: "No change, or an explicit bounded impact" + validations: + required: true + - type: "textarea" + id: "dependencies" + attributes: + label: "Issue graph and implementation dependencies" + description: "List native `blocked by` and sub-issue relationships, related decisions, and the one intended implementation issue." + placeholder: "Blocked by #N; sub-issues; related but intentionally separate work" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a41628f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: "Private vulnerability report" + url: "https://github.com/plx/agentic-navigation-guide/security/advisories/new" + about: "Report suspected vulnerabilities privately; do not use a public issue." diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..c23c4a5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,64 @@ +# Pull request + +## Problem + + + +## Before behavior + + + +## After behavior + + + +## Red-before-fix evidence + + + +## Validation + + + +## Documentation and compatibility + + + +## Security and sensitive data + + + +## Dependencies and issue graph + + + +## Checklist + +- [ ] This pull request addresses one issue and contains no unrelated cleanup. +- [ ] The regression is deterministic, hermetic, bounded, and placed at the + narrowest useful layer. +- [ ] Focused and full validation results are recorded above. +- [ ] User-facing and normative documentation are aligned where applicable. +- [ ] Dependency, license, package, release, and security impacts are explicit. +- [ ] Every review conversation is resolved before merge. + + +Closes #NUMBER diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88864f5..e49289e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -209,6 +209,9 @@ jobs: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" - name: Install checksum-pinned actionlint 1.7.12 env: ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 @@ -265,15 +268,21 @@ jobs: run: >- PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests/test_audit_github_protections.py -v + - name: Validate contributor guidance and templates + run: | + python3 -m py_compile scripts/check_contributor_templates.py + PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests/test_check_contributor_templates.py -v + python3 scripts/check_contributor_templates.py - name: Lint maintained Markdown run: | rumdl check --disable MD010,MD013,MD038 README.md + rumdl check --disable MD013 CONTRIBUTING.md .github/pull_request_template.md rumdl check SECURITY.md docs/security-response-runbook.md rumdl check --disable MD010,MD013,MD018,MD031,MD038 docs/v0.2-contract.md docs/release-policy.md docs/maintainer-continuity.md docs/repository-protections.md docs/history/README.md - name: Check maintained documentation links env: GITHUB_TOKEN: ${{ github.token }} - run: lychee --no-progress README.md SECURITY.md .github/examples/readme-verify.yml docs/v0.2-contract.md docs/release-policy.md docs/security-response-runbook.md docs/maintainer-continuity.md docs/repository-protections.md docs/history/README.md + run: lychee --no-progress README.md CONTRIBUTING.md SECURITY.md .github/pull_request_template.md .github/ISSUE_TEMPLATE/*.yml .github/examples/readme-verify.yml docs/v0.2-contract.md docs/release-policy.md docs/security-response-runbook.md docs/maintainer-continuity.md docs/repository-protections.md docs/history/README.md issue-selector: name: Production-readiness issue selector diff --git a/AGENTIC_NAVIGATION_GUIDE.md b/AGENTIC_NAVIGATION_GUIDE.md index cf0278a..9c5388b 100644 --- a/AGENTIC_NAVIGATION_GUIDE.md +++ b/AGENTIC_NAVIGATION_GUIDE.md @@ -67,6 +67,7 @@ - CHANGELOG.md # Complete prepared 0.2.0 changes and published-0.1.4 migration - LICENSING.md # Fact-only historical license clarification and yank decision - README.md +- CONTRIBUTING.md # One-issue workflow, deterministic red-before evidence, test rules, validation, and triage - SECURITY.md # Supported versions, private vulnerability reporting, trust boundary, and disclosure policy - docs/ # Maintained user and contract documentation - v0.2-contract.md # Normative v0.2 guide language and filesystem mapping @@ -89,6 +90,11 @@ - config.toml # project-local aliases and rust-version-aware dependency resolution - .github/ - dependabot.yml # Weekly review-only Cargo and GitHub Actions update proposals + - ISSUE_TEMPLATE/ + - 01-bug.yml # Structured public bug report without sensitive vulnerability details + - 02-contract-proposal.yml # Structured CLI, guide, compatibility, and trust-boundary proposal + - config.yml # No blank public issues; private vulnerability-report contact route + - pull_request_template.md # One-issue evidence, validation, impacts, graph, and closing-directive prompts - examples/ - readme-verify.yml # Actionlint-checked immutable README workflow example - repository-protections/ @@ -114,6 +120,7 @@ - check_coverage.py # Fail-closed overall and critical-module branch-aware coverage policy - check_mutation_report.py # Reviewed blocker-mutation completeness and survivor gate - check_release_identity.py # Fail-closed version/tag/changelog/baseline checker + - check_contributor_templates.py # Strict contributor-guide, issue-form, chooser, and PR-template contract - audit_github_protections.py # Live ruleset, tag, environment, and secret-name policy audit - get_next_production_readiness_issue.py # Select the next remediation issue from live GitHub state - run_performance_baseline.py # Fixed-fixture release timing and resource regression harness @@ -136,6 +143,7 @@ - issue_68_normative_source.rs # Sole normative claimant, historical rule ledger, links, and contradiction proof - issue_71_maintainer_continuity.rs # Single-maintainer exception, candid control status, and policy-alignment regressions - issue_69_security_policy.rs # Private report route, support, threat-model, response, and CI policy proof + - issue_70_contributor_workflow.rs # Setup, issue graph, red-before, templates, validation, and triage proof - issue_55_platform_conformance.rs # Three-platform full-suite, capability, skip, and release-dependency policy - issue_47_output_contract.rs # Fixed CLI stream, quiet-mode, and recursive-diagnostic matrix - issue_101_parent_explicit_guide.rs # Parent-containing explicit-guide authority and cross-platform ancestor regressions @@ -144,6 +152,7 @@ - test_check_coverage.py # Coverage report floor and missing-instrumentation regressions - test_check_mutation_report.py # Mutation report completeness and survivor regressions - test_check_release_identity.py # Release-identity checker mutation regressions + - test_check_contributor_templates.py # Constrained YAML, required-heading, route, and fail-closed regressions - test_audit_github_protections.py # Offline GitHub-control comparison and pagination regressions - test_get_next_production_readiness_issue.py # Offline selector regression suite - test_performance_baseline.py # Performance matrix, scaling, metadata, and reference regressions @@ -158,6 +167,7 @@ - 2026-07-27-issue-68-specification-disposition.md # Historical specification and PR #21 disposition evidence - 2026-07-27-issue-71-maintainer-continuity.md # Approved sole-maintainer exception and missing-control disposition - 2026-07-27-issue-69-security-policy.md # Private route, claim mapping, tabletop, and residual-risk evidence + - 2026-07-27-issue-70-contributor-workflow.md # Red-before, clean-checkout, rendered-template, and cold-read evidence - 2026-07-27-issue-65-repository-protections.json # Non-sensitive admin-visible hosted-control attestation - 2026-07-27-issue-65-repository-protections.md # Required-check, tag, environment, and rejection-probe evidence - 2026-07-27-issue-47-output-contract.md # Broken-pipe, quiet-mode, and GitHub diagnostic evidence diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f1c360..3f1e6f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,11 @@ complete and the final candidate is revalidated. owner approval with administrator bypass disabled. The zero independent review count and allowed self-review are explicit consequences of the dated single-maintainer exception. +- A root contributor guide, structured bug and contract-proposal issue forms, + and a pull request template now require one-issue scope, deterministic + red-before-fix evidence, focused/full validation, native dependency links, + documentation/compatibility/security review, and dependency/license impact. + CI validates their constrained YAML and headings without adding fuzzing. - `docs/v0.2-contract.md` is mechanically enforced as the sole normative v0.2 specification. The original root `Specification.md` is preserved with its Git history under `docs/history/`, where an unmistakable dated banner and diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..57b867f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,280 @@ +# Contributing + +Thank you for improving `agentic-navigation-guide`. This repository values +small, evidence-backed changes over broad cleanup. Contributions submitted +intentionally are licensed under the repository's MIT OR Apache-2.0 terms. + +## Scope and supported environment + +The supported product is the installed Rust CLI. The +[v0.2 contract](docs/v0.2-contract.md) is normative; the +[README](README.md) is the concise entry point. The static site is outside this +CLI audit and should not be changed as part of ordinary product work. + +This is a personal repository with one maintainer and no GitHub organization, +team, backup administrator, or independent reviewer. Follow the +[dated sole-maintainer policy](docs/maintainer-continuity.md) rather than +assuming an organization-only approval or recovery path. + +The supported toolchain and host matrix is: + +| Tool or host | Required use | +| --- | --- | +| Rust `1.85.0` | MSRV check, tests, Clippy, package, and installed-package smoke | +| Rust `1.96.1` | Stable-minus-one check and tests | +| Rust `1.97.1` | Current-stable check, tests, and Clippy | +| Linux, macOS, and Windows | Complete debug/release behavior and platform capability gates | +| Python `3.12` | Repository policy checkers and their standard-library tests | +| `just 1.51.0` | Versioned repository task entry points | + +CI additionally pins nightly `2025-11-04`, `cargo-llvm-cov 0.8.7`, +`cargo-mutants 27.1.0`, `cargo-about 0.9.0`, `actionlint 1.7.12`, +`zizmor 1.25.2`, `rumdl 0.2.43`, and `lychee 0.24.2`. Use those exact +versions when reproducing the corresponding hosted job. Do not silently +upgrade an auxiliary tool inside an unrelated contribution. + +## Prepare a trusted checkout + +Clone only into a directory you control. Repository contents and guide text +are untrusted inputs; the executable, command-line arguments, environment, +operating system, and process credentials remain trusted under the +[security policy](SECURITY.md). + +```sh +git clone https://github.com/plx/agentic-navigation-guide.git +cd agentic-navigation-guide +rustup toolchain install 1.85.0 --profile minimal --component clippy,rustfmt +rustup toolchain install 1.96.1 --profile minimal +rustup toolchain install 1.97.1 --profile minimal --component clippy,rustfmt +cargo +1.85.0 fetch --locked +python3 --version +just --version +``` + +`cargo fetch --locked` obtains the graph reviewed in `Cargo.lock`; do not +remove `--locked` merely to make resolution succeed. Do not redirect Cargo or +git configuration into a contributor's home directory, and do not require a +registry token for ordinary build or test work. + +## Choose one issue + +Use `just get-next-production-readiness-issue` when working through the +release-readiness queue, or select one explicitly approved issue. Read its +body, comments, native `blocked by` and sub-issue relationships, and linked +decisions before editing. An open closing pull request covers work in progress; +it does not make an external prerequisite complete. + +Create one topic branch and keep one issue per pull request. Do not bundle a +new finding, opportunistic refactor, dependency refresh, formatting sweep, or +site change. Open a separate issue with the appropriate labels and native +dependency relationship. If a contract decision is missing, add +`needs-decision` and escalate it publicly instead of guessing. + +The pull request body ends with exactly one issue-scoped closing directive. +Replace the template placeholder `Closes #NUMBER` with the selected issue +number. Do not add a second `Fixes`, `Closes`, or `Resolves` directive. + +## Red-before-fix workflow + +Every defect fix begins with the smallest deterministic regression at the +narrowest useful layer: + +1. Branch from current `main` and record the base commit SHA. +2. Add only the proposed regression. Before implementation changes, run its + focused command and confirm the test fails for the reported reason before + the implementation changes. An unrelated compile, setup, timeout, or + assertion failure is not evidence. +3. Record the base commit SHA, exact command, nonzero status, and expected + failure reason in the issue audit or pull request. +4. Implement the smallest correction, rerun the focused test, and then run the + complete relevant validation matrix. +5. Commit and publish the final green test and implementation together. Do not + commit a deliberately failing state to `main`; a pull request may describe + the observed test-first working-tree failure without preserving a broken + commit. + +Documentation, policy, and hosted-control issues use the same method: add a +deterministic contract check that fails because the required artifact or +control is absent, then implement it. Never weaken an existing test to +manufacture red-before evidence. + +## Test and fixture rules + +- Put pure behavior tests next to the private module and CLI/package or + cross-document contracts under `tests/`. Use an issue-numbered name when an + acceptance contract owns the evidence. +- Build fixed, minimal fixtures. There is no network, clock, process-global + state, developer checkout, or assertion-free smoke test in a regression. +- Use `tempfile::TempDir` or the existing owned subprocess harnesses. A test + must not mutate the process-global current directory or environment, or a + developer's Cargo, rustup, git, credential, registry, home, or system state. +- Remove the five guide-configuration variables from child processes unless + the test explicitly owns one. Give every subprocess an owned working + directory and reap it before the temporary root is dropped. +- Gate symlink, reparse, non-UTF-8, permission, and other capability-specific + cases with the real platform capability. Unsupported hosts must record an + explicit unavailable result; a silent skip cannot satisfy a supported-host + claim. +- Keep fixtures bounded and deterministic. Preserve exact names, bytes, order, + seed labels, and expected diagnostics. Do not use random input to replace a + complete equivalence-class matrix. + +No fuzz target or corpus exists. Do not add or run fuzzing as part of ordinary +contributor work. Issue #56 remains deferred and requires a new explicit +maintainer decision before any generated-property target, seed, or corpus is +introduced. Parser reliability is currently exercised by the deterministic +issue #57 matrices: + +```sh +cargo test --locked parser_robustness_tests:: -- --nocapture +``` + +The only mutation process is the fixed 15-sentinel mutation job in CI. It is a +reviewed deterministic original-blocker set, not authorization for a broader +mutation campaign. Changes to its modules must preserve the exact filter, +bounded jobs/timeouts, complete report, and zero survivors or timeouts. + +## Validation matrix + +Run the focused regression first. The following example selects the issue #70 +contract; replace only the final test filter for another issue: + +```sh +cargo +1.85.0 test --locked --test issue_70_contributor_workflow -- --nocapture +``` + +For every Rust or maintained-contract change, run: + +```sh +cargo fmt -- --check +cargo +1.85.0 check --locked --all-targets --all-features +cargo +1.85.0 test --locked --all-targets --all-features +cargo +1.85.0 clippy --locked --all-targets --all-features -- -D warnings +cargo +1.96.1 check --locked --all-targets --all-features +cargo +1.96.1 test --locked --all-targets --all-features +cargo +1.97.1 check --locked --all-targets --all-features +cargo +1.97.1 test --locked --all-targets --all-features +cargo +1.97.1 clippy --locked --all-targets --all-features -- -D warnings +GUIDE_FORMAT_REQUIRE_CONFORMANCE=all cargo test --workspace --all-targets --all-features --locked -- --nocapture +cargo run --release --locked -- verify --github-actions-check --deny-ignored +``` + +Run the exact package commands from a clean committed tree when package, +manifest, maintained packaged documentation, or release-sensitive inputs +change: + +```sh +cargo package --locked +cargo publish --dry-run --locked +``` + +Run the standard-library policy suites when their surfaces are affected: + +```sh +just test-production-readiness-selector +just test-release-identity +just test-github-protections +just test-quality-gates +PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests/test_check_contributor_templates.py -v +python3 scripts/check_contributor_templates.py +``` + +For maintained Markdown or GitHub workflow changes, install the exact pinned +tools listed above and run the same commands as CI: + +```sh +actionlint .github/workflows/*.yml .github/examples/*.yml +zizmor --pedantic --no-ignores .github/workflows/ .github/examples/readme-verify.yml +rumdl check --disable MD010,MD013,MD038 README.md +rumdl check --disable MD013 CONTRIBUTING.md .github/pull_request_template.md +rumdl check SECURITY.md docs/security-response-runbook.md +rumdl check --disable MD010,MD013,MD018,MD031,MD038 docs/v0.2-contract.md docs/release-policy.md docs/maintainer-continuity.md docs/repository-protections.md docs/history/README.md +lychee --no-progress README.md CONTRIBUTING.md SECURITY.md .github/pull_request_template.md .github/ISSUE_TEMPLATE/*.yml .github/examples/readme-verify.yml docs/v0.2-contract.md docs/release-policy.md docs/security-response-runbook.md docs/maintainer-continuity.md docs/repository-protections.md docs/history/README.md +``` + +One workstation cannot establish the supported host matrix. The pull request +must pass the complete locked debug and release suites on Linux, macOS, and +Windows, the exact MSRV/current stable lanes, coverage, the fixed mutation +sentinels, package identity, license generation, performance baseline, +workflow lint, and navigation-guide verification. Rust beta is informational. + +## Dependencies, licenses, and release-sensitive files + +Dependency changes must be intentional, compatible with Rust 1.85, and +reviewed in `Cargo.lock`. Explain every direct dependency or feature change, +inspect the activated graph, run the MSRV/current-stable matrix, scan the +locked graph with pinned `cargo-audit 0.22.1`, and regenerate attribution with: + +```sh +cargo about generate about.hbs --output-file THIRD_PARTY_LICENSES.md +git diff --exit-code -- THIRD_PARTY_LICENSES.md +``` + +Commit the lockfile and attribution change together. Never treat a Dependabot +proposal, successful unlocked build, absent library target, or inactive +optional feature as sufficient review. + +Treat `Cargo.toml`, `Cargo.lock`, `release/`, `docs/release-policy.md`, +`SECURITY.md`, `CHANGELOG.md`, `.github/workflows/`, package allowlists, +license/notice files, checksums, tags, and provenance inputs as +release-sensitive. Review their exact diff and run their owning gates. +Issue #63 remains the authority for the release workflow and Trusted Publisher; +until its external criterion is complete, do not publish, create a release +tag, or introduce a long-lived registry token. + +## Security and sensitive data + +Send suspected vulnerabilities only through the +[private report route](SECURITY.md#report-a-vulnerability-privately). Do not +put exploit details, secrets, personal data, private repository contents, or +embargoed findings in an issue, ordinary pull request, commit, fixture, +workflow log, benchmark, or audit. + +Use synthetic, control-safe paths and data. Tests and diagnostics must not +echo raw guide lines, resolved external link targets, tokens, credential +names, or path-sensitive fixtures. A security fix follows the public +[response runbook](docs/security-response-runbook.md) and ordinary +fail-closed release gates. + +## Pull requests and review + +Open a draft pull request early enough to expose hosted checks, but keep its +scope to the selected issue. Complete every template section with concrete +before/after behavior, red-before-fix evidence, focused/full commands and +platforms, documentation/compatibility/security impact, dependency/license +impact, and issue-graph links. + +Update `README.md` with user-facing behavior, `docs/v0.2-contract.md` with +normative behavior, `CHANGELOG.md` with prepared-release impact, and the +navigation guide with new or removed files. Record intentional divergence in +the README with a date and rationale. Historical design text is not normative. + +The personal-repository ruleset requires `Required CI`, `Verify Navigation +Guide`, current-main testing, immutable history, and resolved review +conversations. GitHub does not allow the author to self-approve, and the +approved sole-maintainer model has zero required approving reviews. This does +not make review comments optional: answer actionable feedback with code or +evidence and resolve every conversation before merge. + +## Maintainer triage + +The maintainer: + +1. routes vulnerability details to the private advisory and removes sensitive + public material when possible; +2. applies the narrowest component/domain/risk/priority labels supported by + evidence, using `needs-decision` when a contract choice is missing; +3. confirms one issue, its native blockers/sub-issues, and exactly one closing + directive before review; +4. rejects unrelated cleanup and creates a separate issue or dependency edge; +5. checks red-before-fix evidence, deterministic placement, all supported + platforms, documentation alignment, compatibility, security, dependencies, + licensing, package, and release-sensitive changes; +6. resolves or explicitly dispositions every review conversation; +7. merges only after required checks pass against current `main`; and +8. verifies the closed issue, default-branch artifacts, and any hosted setting + after merge. + +If authority, intent, platform behavior, compatibility, or security scope is +uncertain, stop and request a public decision. Do not infer an organization, +second maintainer, emergency bypass, or release exception that does not exist. diff --git a/README.md b/README.md index d94bbbe..6f13a6d 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,7 @@ README: - [security and vulnerability-reporting limitations](docs/v0.2-contract.md#security-and-vulnerability-reporting); - [supported CLI-only product and compatibility](docs/v0.2-contract.md#supported-product-and-rust-api); - [release and Rust support policy](docs/release-policy.md); +- [contribution guide](CONTRIBUTING.md); - [security policy and private reporting route](SECURITY.md); and - [complete prepared `0.2.0` changes](CHANGELOG.md#020---unreleased). diff --git a/audits/2026-07-27-issue-70-contributor-workflow.md b/audits/2026-07-27-issue-70-contributor-workflow.md new file mode 100644 index 0000000..89b2f50 --- /dev/null +++ b/audits/2026-07-27-issue-70-contributor-workflow.md @@ -0,0 +1,147 @@ +# Issue #70 contributor-workflow evidence + +Date: 2026-07-27 + +Repository: `plx/agentic-navigation-guide` + +## Scope decisions + +This issue establishes a contributor workflow for the repository that exists, +not for a hypothetical organization: + +- the repository remains personal, with `plx` as its only maintainer and no + GitHub organization, team, backup administrator, or independent reviewer; +- the static site remains outside this CLI audit; +- Issue #63 still owns the externally blocked release workflow and crates.io + Trusted Publisher, so contributor guidance does not claim publication + authority; +- no `CODEOWNERS` file or cosmetic organization control is introduced; and +- no fuzz target, corpus, generated-property runner, random fixture, or new + mutation campaign is added or run. + +No fuzz target exists. The guide keeps issue #56 deferred and directs ordinary +parser reliability work to the deterministic issue #57 matrices. It documents +the already-reviewed fixed 15-sentinel mutation job without broadening it. + +## Deterministic red-before evidence + +The issue branch started at exact merged-main commit +`b2e3ec848ba6e2553fdecfa5f7e6f98bcefb482e`. Before any contributor artifact +or CI implementation existed, the new fixed-file contract was run: + +```sh +cargo test --locked --test issue_70_contributor_workflow -- --nocapture +``` + +It exited 101 with all three tests failing for the intended reason: + +- `CONTRIBUTING.md` did not exist; +- `.github/ISSUE_TEMPLATE/01-bug.yml` did not exist; and +- `audits/2026-07-27-issue-70-contributor-workflow.md` did not exist. + +The test compiled successfully and did not fail because of setup, networking, +random input, or unrelated runtime behavior. + +## Contributor artifacts and fail-closed contract + +The delivered surface is: + +- root `CONTRIBUTING.md`, which GitHub can surface to issue and pull request + authors; +- ordered structured issue forms for public bugs and contract proposals; +- a chooser that disables contributor blank issues and exposes the real + private vulnerability-report route; +- one pull request template with exact evidence and impact headings; +- `scripts/check_contributor_templates.py`, a Python-standard-library checker + for the repository's deliberately constrained YAML subset, chooser, pull + request headings, closing placeholder, and contributor-guide headings; +- negative checker regressions for unquoted values, aliases/anchors, odd + indentation, duplicate IDs, missing validation, wrong private route, + numeric template closers, missing headings, and missing artifacts; and +- an all-platform Rust contract that binds setup, issue-graph, red-before, + hermetic-test, validation, documentation, and maintainer instructions to CI. + +The checker accepts only two-space YAML indentation, JSON-style quoted string +scalars, explicit booleans, unique lowercase kebab-case input IDs, known input +types, and explicit required flags. This smaller grammar is sufficient for the +checked-in forms and rules out YAML features the repository does not need. + +The file locations and schema follow GitHub's maintained documentation for +[issue forms](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms), +[template chooser configuration](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository), +[pull request templates](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository), +and +[contributor guidelines](https://docs.github.com/en/enterprise-cloud@latest/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors). + +## Clean-checkout command exercise + +The exact candidate source passed: + +| Surface | Result | +| --- | --- | +| Rust `1.85.0` | Locked all-target/all-feature check, test, and warnings-denied Clippy passed | +| Rust `1.96.1` | Locked all-target/all-feature check and test passed | +| Rust `1.97.1` | Locked all-target/all-feature check, test, and warnings-denied Clippy passed | +| Full conformance suite | `GUIDE_FORMAT_REQUIRE_CONFORMANCE=all` passed 393 tests; the two manual benchmarks and explicit package test were the three documented ignores | +| Explicit packaged-artifact acceptance | The ignored issue #62 exact-package install, smoke, and no-library-consumer test passed separately | +| Issue #70 contracts | Three Rust tests, nine Python negative/unit tests, and the live constrained-template check passed | +| Deterministic parser matrix | The five issue #57 fixed equivalence-matrix tests passed; no generated input was used | +| Related and repository policy | Eleven issue #61/#66/#69 Rust contracts and 101 selector, identity, protection, coverage, fixed-mutation-report, and performance-policy tests passed | +| Package | The exact 35-file allowlist packaged and verified; `cargo publish --dry-run --locked` reached only Cargo's expected pre-upload abort | +| GitHub files | Ruby `Psych.safe_load` accepted all three YAML files; exact `actionlint` and pedantic `zizmor` commands passed with no findings | +| Maintained documentation | All exact `rumdl` commands passed; `lychee` checked 95 links (59 unique) with zero errors and four followed redirects | +| Formatting and navigation | `cargo fmt -- --check`, `just --fmt --check`, `git diff --check`, and release-mode guide verification passed | + +These commands targeted the exact candidate source in the issue working tree. +The first package and dry-run exercise used `--allow-dirty` only because the +candidate audit had not yet been committed. The exact non-`--allow-dirty` +package commands and focused contracts are repeated on the clean committed +tree before publication, and that commit/result is appended here. + +Hosted CI remains authoritative for Linux, macOS, and Windows. A single local +machine is not represented as three-platform evidence. + +## Template validation and preview + +The constrained local validator, its nine negative/unit regressions, and a +second parse with Ruby's maintained YAML implementation pass before +publication. GitHub only makes repository issue and pull request templates +available from the default branch, so the rendered chooser, each issue form, +the pull request template, and the community-profile contribution surface must +be checked immediately after merge. No draft test issue containing placeholder +content will be left open. + +## Representative-ticket cold read + +The contributor guide was read as the only process reference alongside a +representative issue-scoped remediation ticket. The cold-read record is +completed after the final guide text and automated review are available. +Known prerequisites that must remain discoverable without oral context are: + +- the sole normative contract and concise README roles; +- exact MSRV/stable/tool pins and locked commands; +- native blocker/sub-issue selection and one-issue scope; +- deterministic red-before evidence without a broken `main`; +- hermetic temporary-root, environment, and platform-capability rules; +- the absence of a fuzz target or corpus; +- private security routing and sensitive-fixture limits; +- dependency/license/package/release-sensitive review; and +- the personal repository's zero-independent-review constraint. + +This is an instruction-completeness exercise, not an independent technical +approval. The hosted automated review supplies an additional cold reader; any +missing prerequisite it identifies must be corrected or recorded before merge. + +## Acceptance mapping + +| Acceptance criterion | Evidence | +| --- | --- | +| Setup and complete relevant validation are discoverable | Pinned environment table, trusted-checkout setup, focused/full commands, package/policy/doc gates, and hosted platform explanation | +| Defects require red-before-fix evidence | Five-step workflow plus PR prompts require base SHA, exact command, nonzero status, reported reason, focused green, and full green | +| Templates capture every impact | Bug/contract forms and PR template cover compatibility, security, documentation, dependency/license, platform, validation, and issue graph | +| Issue graph needs no hidden process | Selector, one-issue rule, native blocker/sub-issue reading, separate-finding rule, and one closer are explicit | +| Commands and templates stay current in CI | Rust contract, strict standard-library checker, negative tests, rumdl, lychee, workflow lint, and all-platform suite | + +Post-merge closure requires successful main CI, GitHub-rendered template +recognition, a public issue note with those URLs/results, and no open draft test +artifact. diff --git a/scripts/check_contributor_templates.py b/scripts/check_contributor_templates.py new file mode 100644 index 0000000..26b2efc --- /dev/null +++ b/scripts/check_contributor_templates.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 +"""Validate the repository's constrained contributor-template contract.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import NoReturn + +ISSUE_FORMS = ( + Path(".github/ISSUE_TEMPLATE/01-bug.yml"), + Path(".github/ISSUE_TEMPLATE/02-contract-proposal.yml"), +) +CHOOSER = Path(".github/ISSUE_TEMPLATE/config.yml") +PULL_REQUEST_TEMPLATE = Path(".github/pull_request_template.md") +CONTRIBUTING = Path("CONTRIBUTING.md") +SECURITY_ROUTE = ( + "https://github.com/plx/agentic-navigation-guide/security/advisories/new" +) +FORM_TYPES = frozenset({"markdown", "input", "textarea", "dropdown"}) +PR_HEADINGS = ( + "## Problem", + "## Before behavior", + "## After behavior", + "## Red-before-fix evidence", + "## Validation", + "## Documentation and compatibility", + "## Security and sensitive data", + "## Dependencies and issue graph", + "## Checklist", +) +CONTRIBUTING_HEADINGS = ( + "# Contributing", + "## Scope and supported environment", + "## Prepare a trusted checkout", + "## Choose one issue", + "## Red-before-fix workflow", + "## Test and fixture rules", + "## Validation matrix", + "## Dependencies, licenses, and release-sensitive files", + "## Security and sensitive data", + "## Pull requests and review", + "## Maintainer triage", +) +CLOSING_DIRECTIVE = re.compile( + r"(?im)^\s*(?:closes|fixes|resolves)\s+#[0-9]+\s*$" +) +FORM_ID = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*\Z") + + +class TemplateContractError(ValueError): + """A contributor artifact does not satisfy the checked contract.""" + + +@dataclass +class FormItem: + kind: str + identifier: str | None = None + attributes: dict[str, str | bool] = field(default_factory=dict) + options: list[str] = field(default_factory=list) + validations: dict[str, bool] = field(default_factory=dict) + + +@dataclass +class IssueForm: + name: str + description: str + title: str + labels: tuple[str, ...] + items: tuple[FormItem, ...] + + +def fail(path: Path, line: int | None, message: str) -> NoReturn: + location = str(path) if line is None else f"{path}:{line}" + raise TemplateContractError(f"{location}: {message}") + + +def checked_lines(path: Path, source: str, *, yaml: bool = True) -> list[str]: + if not source.endswith("\n"): + fail(path, None, "file must end with one newline") + if "\r" in source: + fail(path, None, "carriage returns are not allowed") + + lines = source.splitlines() + for number, line in enumerate(lines, start=1): + if "\t" in line: + fail(path, number, "tabs are not allowed") + if line.rstrip() != line: + fail(path, number, "trailing whitespace is not allowed") + if yaml: + indentation = len(line) - len(line.lstrip(" ")) + if indentation % 2: + fail(path, number, "indentation must use two-space units") + stripped = line.lstrip() + if stripped.startswith(("&", "*", "!", "<<:", "- &", "- *", "- !")): + fail( + path, + number, + "YAML aliases, anchors, tags, and merges are forbidden", + ) + return lines + + +def quoted_scalar(path: Path, number: int, raw: str) -> str: + try: + value = json.loads(raw) + except json.JSONDecodeError as error: + fail(path, number, f"scalar must be one JSON-style quoted string: {error.msg}") + if not isinstance(value, str) or not value.strip(): + fail(path, number, "quoted scalar must be a non-empty string") + return value + + +def boolean_scalar(path: Path, number: int, raw: str) -> bool: + if raw == "true": + return True + if raw == "false": + return False + fail(path, number, "boolean scalar must be true or false") + + +def split_mapping(path: Path, number: int, content: str) -> tuple[str, str]: + if ": " in content: + key, raw = content.split(": ", 1) + return key, raw + if content.endswith(":"): + return content[:-1], "" + fail(path, number, "expected a YAML mapping entry") + + +def validate_issue_form(path: Path, source: str) -> IssueForm: + lines = checked_lines(path, source) + top: dict[str, str] = {} + labels: list[str] = [] + items: list[FormItem] = [] + seen_top: set[str] = set() + section = "top" + subsection: str | None = None + + for number, line in enumerate(lines, start=1): + if not line: + continue + indentation = len(line) - len(line.lstrip(" ")) + content = line[indentation:] + + if indentation == 0: + key, raw = split_mapping(path, number, content) + if key in seen_top: + fail(path, number, f"duplicate top-level key {key!r}") + seen_top.add(key) + if key in {"name", "description", "title"}: + top[key] = quoted_scalar(path, number, raw) + section = "top" + elif key == "labels" and not raw: + section = "labels" + elif key == "body" and not raw: + section = "body" + else: + fail(path, number, f"unsupported top-level key {key!r}") + subsection = None + continue + + if section == "labels" and indentation == 2 and content.startswith("- "): + labels.append(quoted_scalar(path, number, content[2:])) + continue + + if section != "body": + fail(path, number, f"unexpected content in {section!r} section") + + if indentation == 2 and content.startswith("- type: "): + kind = quoted_scalar(path, number, content.removeprefix("- type: ")) + if kind not in FORM_TYPES: + fail(path, number, f"unsupported issue-form input type {kind!r}") + items.append(FormItem(kind=kind)) + subsection = None + continue + + if not items: + fail(path, number, "body content must start with an input type") + item = items[-1] + + if indentation == 4: + key, raw = split_mapping(path, number, content) + if key == "id": + if item.identifier is not None: + fail(path, number, "duplicate input id") + item.identifier = quoted_scalar(path, number, raw) + subsection = None + elif key in {"attributes", "validations"} and not raw: + subsection = key + else: + fail(path, number, f"unsupported body-item key {key!r}") + continue + + if indentation == 6 and subsection == "attributes": + key, raw = split_mapping(path, number, content) + if key in {"label", "description", "placeholder", "render", "value"}: + if key in item.attributes: + fail(path, number, f"duplicate attribute {key!r}") + item.attributes[key] = quoted_scalar(path, number, raw) + continue + if key == "multiple": + item.attributes[key] = boolean_scalar(path, number, raw) + continue + if key == "options" and not raw: + subsection = "options" + continue + fail(path, number, f"unsupported input attribute {key!r}") + + if indentation == 8 and subsection == "options" and content.startswith("- "): + item.options.append(quoted_scalar(path, number, content[2:])) + continue + + if indentation == 6 and subsection == "validations": + key, raw = split_mapping(path, number, content) + if key != "required" or key in item.validations: + fail(path, number, f"unsupported or duplicate validation {key!r}") + item.validations[key] = boolean_scalar(path, number, raw) + continue + + fail(path, number, "line is outside the constrained issue-form grammar") + + if tuple(top) != ("name", "description", "title"): + fail(path, None, "top-level string keys must be name, description, then title") + if len(top["name"]) <= 3: + fail(path, None, "form name must contain more than three characters") + if not labels: + fail(path, None, "form must apply at least one existing repository label") + if not items: + fail(path, None, "form body must not be empty") + + identifiers: set[str] = set() + for item in items: + if item.kind == "markdown": + if item.identifier is not None: + fail(path, None, "markdown items must not have an id") + if set(item.attributes) != {"value"}: + fail(path, None, "markdown items must contain exactly one value") + if item.validations: + fail(path, None, "markdown items must not contain validations") + continue + + if item.identifier is None or not FORM_ID.fullmatch(item.identifier): + fail(path, None, "every input id must be unique lowercase kebab case") + if item.identifier in identifiers: + fail(path, None, f"duplicate input id {item.identifier!r}") + identifiers.add(item.identifier) + if "label" not in item.attributes or "description" not in item.attributes: + fail(path, None, f"input {item.identifier!r} needs label and description") + if set(item.validations) != {"required"}: + fail(path, None, f"input {item.identifier!r} needs an explicit required flag") + if item.kind == "dropdown" and not item.options: + fail(path, None, f"dropdown {item.identifier!r} needs options") + if item.kind != "dropdown" and item.options: + fail(path, None, f"non-dropdown {item.identifier!r} cannot have options") + + return IssueForm( + name=top["name"], + description=top["description"], + title=top["title"], + labels=tuple(labels), + items=tuple(items), + ) + + +def validate_chooser(path: Path, source: str) -> None: + lines = checked_lines(path, source) + expected = [ + "blank_issues_enabled: false", + "contact_links:", + ' - name: "Private vulnerability report"', + f' url: "{SECURITY_ROUTE}"', + ' about: "Report suspected vulnerabilities privately; do not use a public issue."', + ] + if lines != expected: + fail(path, None, "chooser must disable blank issues and expose only the private route") + + +def ordered_headings( + path: Path, source: str, expected: tuple[str, ...], level_prefix: str +) -> None: + checked_lines(path, source, yaml=False) + headings = tuple( + line for line in source.splitlines() if line.startswith(level_prefix) + ) + if headings != expected: + fail(path, None, f"headings must be exactly {expected!r}, found {headings!r}") + + +def validate_pull_request_template(path: Path, source: str) -> None: + ordered_headings(path, source, PR_HEADINGS, "## ") + if source.count("Closes #NUMBER") != 1: + fail(path, None, "template must contain one issue-number placeholder") + if CLOSING_DIRECTIVE.search(source): + fail(path, None, "template must not contain a live numeric closing directive") + for prompt in ( + "base commit SHA", + "exact pre-fix command", + "focused post-fix", + "full post-fix", + "documentation impact", + "compatibility impact", + "security impact", + "dependency or license impact", + ): + if prompt not in source: + fail(path, None, f"pull request template omits prompt {prompt!r}") + + +def validate_contributing(path: Path, source: str) -> None: + checked_lines(path, source, yaml=False) + headings = tuple( + line for line in source.splitlines() if line.startswith(("# ", "## ")) + ) + if headings != CONTRIBUTING_HEADINGS: + fail(path, None, "contributor-guide headings changed without contract review") + if CLOSING_DIRECTIVE.search(source): + fail(path, None, "contributor guide must not contain a live closing directive") + + +def check_repository(root: Path) -> None: + forms: list[IssueForm] = [] + for relative in ISSUE_FORMS: + path = root / relative + try: + source = path.read_text(encoding="utf-8") + except OSError as error: + fail(relative, None, f"cannot read required issue form: {error}") + forms.append(validate_issue_form(relative, source)) + + names = [form.name for form in forms] + if len(names) != len(set(names)): + fail(Path(".github/ISSUE_TEMPLATE"), None, "issue-form names must be unique") + + required_ids = { + ISSUE_FORMS[0]: { + "version", + "platform", + "observed", + "expected", + "reproduction", + "regression", + "compatibility", + "security", + }, + ISSUE_FORMS[1]: { + "current-contract", + "proposed-contract", + "compatibility", + "platforms", + "security", + "dependencies", + }, + } + for relative, form in zip(ISSUE_FORMS, forms, strict=True): + actual = { + item.identifier for item in form.items if item.identifier is not None + } + if actual != required_ids[relative]: + fail(relative, None, f"input ids must be exactly {sorted(required_ids[relative])}") + + artifacts = ( + (CHOOSER, validate_chooser), + (PULL_REQUEST_TEMPLATE, validate_pull_request_template), + (CONTRIBUTING, validate_contributing), + ) + for relative, validator in artifacts: + try: + source = (root / relative).read_text(encoding="utf-8") + except OSError as error: + fail(relative, None, f"cannot read required contributor artifact: {error}") + validator(relative, source) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Validate contributor guidance and GitHub templates." + ) + parser.add_argument( + "--root", + type=Path, + default=Path(__file__).resolve().parents[1], + help="repository root (defaults to this script's checkout)", + ) + return parser + + +def main() -> int: + args = build_parser().parse_args() + try: + check_repository(args.root.resolve()) + except TemplateContractError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print("contributor template contract OK: 2 issue forms, chooser, PR template, guide") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/issue_61_workflow_security_policy.rs b/tests/issue_61_workflow_security_policy.rs index 106f797..ef3303b 100644 --- a/tests/issue_61_workflow_security_policy.rs +++ b/tests/issue_61_workflow_security_policy.rs @@ -292,8 +292,11 @@ fn workflow_lint_is_fail_closed_and_checksum_pins_every_tool() { "zizmor --pedantic --no-ignores .github/workflows/ .github/examples/readme-verify.yml" )); assert!(lint.contains("rumdl check --disable MD010,MD013,MD038 README.md")); - assert!(lint.contains("rumdl check SECURITY.md docs/security-response-runbook.md")); assert!(lint - .contains("lychee --no-progress README.md SECURITY.md .github/examples/readme-verify.yml")); + .contains("rumdl check --disable MD013 CONTRIBUTING.md .github/pull_request_template.md")); + assert!(lint.contains("rumdl check SECURITY.md docs/security-response-runbook.md")); + assert!(lint.contains( + "lychee --no-progress README.md CONTRIBUTING.md SECURITY.md .github/pull_request_template.md" + )); assert!(!lint.contains("continue-on-error")); } diff --git a/tests/issue_66_readme_examples.rs b/tests/issue_66_readme_examples.rs index 4298588..651ac4d 100644 --- a/tests/issue_66_readme_examples.rs +++ b/tests/issue_66_readme_examples.rs @@ -417,8 +417,9 @@ fn issue_66_ci_example_is_exact_parseable_and_immutable() { "RUMDL_VERSION: 0.2.43", "RUMDL_SHA256: 01e0dd2d89c07d244c5c93243f7faf2986d2abec68a7cec458e38c25988fbabc", "rumdl check --disable MD010,MD013,MD038 README.md", + "rumdl check --disable MD013 CONTRIBUTING.md .github/pull_request_template.md", "rumdl check SECURITY.md docs/security-response-runbook.md", - "lychee --no-progress README.md SECURITY.md .github/examples/readme-verify.yml", + "lychee --no-progress README.md CONTRIBUTING.md SECURITY.md .github/pull_request_template.md", ] { assert!( ci.contains(required), diff --git a/tests/issue_69_security_policy.rs b/tests/issue_69_security_policy.rs index 4a48bfa..b3b5007 100644 --- a/tests/issue_69_security_policy.rs +++ b/tests/issue_69_security_policy.rs @@ -198,9 +198,13 @@ fn issue_69_docs_ci_and_audit_keep_every_security_claim_in_scope() { ] { assert!(guide.contains(path), "navigation guide omits {path:?}"); } + assert!( + ci.contains("rumdl check --disable MD013 CONTRIBUTING.md .github/pull_request_template.md") + ); assert!(ci.contains("rumdl check SECURITY.md docs/security-response-runbook.md")); - assert!(ci - .contains("lychee --no-progress README.md SECURITY.md .github/examples/readme-verify.yml")); + assert!(ci.contains( + "lychee --no-progress README.md CONTRIBUTING.md SECURITY.md .github/pull_request_template.md" + )); for evidence in [ "Issue #35", diff --git a/tests/issue_70_contributor_workflow.rs b/tests/issue_70_contributor_workflow.rs new file mode 100644 index 0000000..eafdbe0 --- /dev/null +++ b/tests/issue_70_contributor_workflow.rs @@ -0,0 +1,250 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +fn repository_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf() +} + +fn repository_file(path: &str) -> String { + fs::read_to_string(repository_root().join(path)) + .unwrap_or_else(|error| panic!("read repository file {path}: {error}")) +} + +fn normalized_whitespace(source: &str) -> String { + source.split_whitespace().collect::>().join(" ") +} + +#[test] +fn issue_70_contributor_guide_is_complete_and_uses_real_pinned_commands() { + let guide = repository_file("CONTRIBUTING.md"); + let normalized = normalized_whitespace(&guide); + let ci = repository_file(".github/workflows/ci.yml"); + let workflow_sources = format!( + "{ci}\n{}", + repository_file(".github/workflows/verify-guide.yml") + ); + + for required in [ + "# Contributing", + "## Scope and supported environment", + "## Prepare a trusted checkout", + "## Choose one issue", + "## Red-before-fix workflow", + "## Test and fixture rules", + "## Validation matrix", + "## Dependencies, licenses, and release-sensitive files", + "## Security and sensitive data", + "## Pull requests and review", + "## Maintainer triage", + "personal repository with one maintainer and no GitHub organization", + "The static site is outside this CLI audit", + "Rust `1.85.0`", + "Rust `1.96.1`", + "Rust `1.97.1`", + "Python `3.12`", + "`just 1.51.0`", + "cargo +1.85.0 fetch --locked", + "cargo +1.85.0 check --locked --all-targets --all-features", + "cargo +1.85.0 test --locked --all-targets --all-features", + "cargo +1.85.0 clippy --locked --all-targets --all-features -- -D warnings", + "cargo +1.97.1 check --locked --all-targets --all-features", + "cargo +1.97.1 test --locked --all-targets --all-features", + "cargo +1.97.1 clippy --locked --all-targets --all-features -- -D warnings", + "cargo fmt -- --check", + "GUIDE_FORMAT_REQUIRE_CONFORMANCE=all cargo test --workspace --all-targets --all-features --locked -- --nocapture", + "cargo run --release --locked -- verify --github-actions-check --deny-ignored", + "cargo package --locked", + "cargo publish --dry-run --locked", + "just test-production-readiness-selector", + "just test-release-identity", + "just test-github-protections", + "just test-quality-gates", + "cargo test --locked parser_robustness_tests:: -- --nocapture", + "No fuzz target or corpus exists", + "Issue #56 remains deferred", + "fixed 15-sentinel mutation job", + "test fails for the reported reason before the implementation changes", + "Do not commit a deliberately failing state to `main`", + "base commit SHA, exact command, nonzero status, and expected failure reason", + "native `blocked by` and sub-issue relationships", + "exactly one issue-scoped closing directive", + "Closes #NUMBER", + "TempDir", + "process-global current directory or environment", + "Linux, macOS, and Windows", + "resolved review conversations", + ] { + assert!( + normalized.contains(required), + "CONTRIBUTING.md omits required workflow text {required:?}" + ); + } + + for (documented, workflow_pin) in [ + ("Rust `1.85.0`", "rust: \"1.85.0\""), + ("Rust `1.96.1`", "rust: \"1.96.1\""), + ("Rust `1.97.1`", "rust: \"1.97.1\""), + ("Python `3.12`", "python-version: \"3.12\""), + ("`just 1.51.0`", "tool: just@1.51.0"), + ("`cargo-llvm-cov 0.8.7`", "tool: cargo-llvm-cov@0.8.7"), + ("`cargo-mutants 27.1.0`", "tool: cargo-mutants@27.1.0"), + ("`cargo-about 0.9.0`", "tool: cargo-about@0.9.0"), + ("`actionlint 1.7.12`", "ACTIONLINT_VERSION: 1.7.12"), + ("`zizmor 1.25.2`", "ZIZMOR_VERSION: 1.25.2"), + ("`rumdl 0.2.43`", "RUMDL_VERSION: 0.2.43"), + ("`lychee 0.24.2`", "LYCHEE_VERSION: 0.24.2"), + ] { + assert!( + guide.contains(documented), + "CONTRIBUTING.md omits documented pin {documented:?}" + ); + assert!( + ci.contains(workflow_pin), + "CI omits contributor-guide pin source {workflow_pin:?}" + ); + } + + for command in [ + "cargo check --locked --all-targets --all-features", + "cargo test --locked --all-targets --all-features", + "cargo clippy --locked --all-targets --all-features -- -D warnings", + "cargo fmt -- --check", + "cargo package --locked", + "cargo publish --dry-run --locked", + "cargo run --release --locked -- verify --github-actions-check --deny-ignored", + ] { + assert!( + workflow_sources.contains(command), + "documented validation command is absent from CI: {command:?}" + ); + } +} + +#[test] +fn issue_70_templates_capture_required_issue_and_pull_request_evidence() { + let bug = repository_file(".github/ISSUE_TEMPLATE/01-bug.yml"); + let proposal = repository_file(".github/ISSUE_TEMPLATE/02-contract-proposal.yml"); + let chooser = repository_file(".github/ISSUE_TEMPLATE/config.yml"); + let pull_request = repository_file(".github/pull_request_template.md"); + let normalized_pr = normalized_whitespace(&pull_request); + + for required in [ + "id: \"version\"", + "id: \"platform\"", + "id: \"observed\"", + "id: \"expected\"", + "id: \"reproduction\"", + "id: \"regression\"", + "id: \"compatibility\"", + "id: \"security\"", + "security/advisories/new", + ] { + assert!(bug.contains(required), "bug form omits {required:?}"); + } + for required in [ + "id: \"current-contract\"", + "id: \"proposed-contract\"", + "id: \"compatibility\"", + "id: \"platforms\"", + "id: \"security\"", + "id: \"dependencies\"", + "native `blocked by`", + ] { + assert!( + proposal.contains(required), + "contract-proposal form omits {required:?}" + ); + } + for required in [ + "blank_issues_enabled: false", + "https://github.com/plx/agentic-navigation-guide/security/advisories/new", + "Private vulnerability report", + ] { + assert!( + chooser.contains(required), + "template chooser omits {required:?}" + ); + } + for heading in [ + "## Problem", + "## Before behavior", + "## After behavior", + "## Red-before-fix evidence", + "## Validation", + "## Documentation and compatibility", + "## Security and sensitive data", + "## Dependencies and issue graph", + "## Checklist", + ] { + assert!( + pull_request.contains(heading), + "pull request template omits heading {heading:?}" + ); + } + for required in [ + "base commit SHA", + "exact pre-fix command", + "focused post-fix", + "full post-fix", + "Linux, macOS, and Windows", + "documentation impact", + "compatibility impact", + "security impact", + "dependency or license impact", + "Closes #NUMBER", + ] { + assert!( + normalized_pr.contains(required), + "pull request template omits evidence prompt {required:?}" + ); + } +} + +#[test] +fn issue_70_ci_docs_and_audit_keep_the_contributor_contract_current() { + let ci = repository_file(".github/workflows/ci.yml"); + let readme = repository_file("README.md"); + let changelog = repository_file("CHANGELOG.md"); + let navigation = repository_file("AGENTIC_NAVIGATION_GUIDE.md"); + let audit = repository_file("audits/2026-07-27-issue-70-contributor-workflow.md"); + + assert!(ci.contains("python3 scripts/check_contributor_templates.py")); + assert!(ci.contains("tests/test_check_contributor_templates.py -v")); + assert!( + ci.contains("rumdl check --disable MD013 CONTRIBUTING.md .github/pull_request_template.md") + ); + assert!(ci.contains( + "lychee --no-progress README.md CONTRIBUTING.md SECURITY.md .github/pull_request_template.md" + )); + assert!(readme.contains("[contribution guide](CONTRIBUTING.md)")); + assert!(changelog.contains("structured bug and contract-proposal issue forms")); + + for path in [ + "- CONTRIBUTING.md", + "- ISSUE_TEMPLATE/", + "- pull_request_template.md", + "- check_contributor_templates.py", + "- issue_70_contributor_workflow.rs", + "- test_check_contributor_templates.py", + "- 2026-07-27-issue-70-contributor-workflow.md", + ] { + assert!( + navigation.contains(path), + "navigation guide omits contributor artifact {path:?}" + ); + } + for evidence in [ + "Deterministic red-before evidence", + "Clean-checkout command exercise", + "Template validation and preview", + "Representative-ticket cold read", + "No fuzz target", + "personal repository", + "Issue #63", + ] { + assert!( + audit.contains(evidence), + "issue #70 audit omits evidence {evidence:?}" + ); + } +} diff --git a/tests/test_check_contributor_templates.py b/tests/test_check_contributor_templates.py new file mode 100644 index 0000000..72723b6 --- /dev/null +++ b/tests/test_check_contributor_templates.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +import sys +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "check_contributor_templates.py" +SPEC = importlib.util.spec_from_file_location( + "check_contributor_templates", SCRIPT +) +assert SPEC is not None and SPEC.loader is not None +checker = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = checker +SPEC.loader.exec_module(checker) + + +def valid_form() -> str: + return """\ +name: "Test form" +description: "Exercise the constrained form grammar." +title: "[Test]: " +labels: + - "testing" +body: + - type: "markdown" + attributes: + value: "Read the instructions." + - type: "textarea" + id: "observed" + attributes: + label: "Observed behavior" + description: "Describe what happened." + validations: + required: true + - type: "dropdown" + id: "platform" + attributes: + label: "Platform" + description: "Select the operating system." + options: + - "Linux" + - "macOS" + - "Windows" + validations: + required: true +""" + + +def valid_pull_request_template() -> str: + prompts = "\n".join( + ( + "base commit SHA", + "exact pre-fix command", + "focused post-fix", + "full post-fix", + "documentation impact", + "compatibility impact", + "security impact", + "dependency or license impact", + ) + ) + sections = "\n\n".join( + f"{heading}\n\n{prompts if index == 0 else 'Complete this section.'}" + for index, heading in enumerate(checker.PR_HEADINGS) + ) + return f"{sections}\n\nCloses #NUMBER\n" + + +class ContributorTemplateCheckerTests(unittest.TestCase): + def test_valid_constrained_form_passes(self) -> None: + form = checker.validate_issue_form(Path("form.yml"), valid_form()) + self.assertEqual(form.name, "Test form") + self.assertEqual( + [item.identifier for item in form.items], + [None, "observed", "platform"], + ) + + def test_unquoted_scalar_fails(self) -> None: + source = valid_form().replace( + 'name: "Test form"', "name: Test form", 1 + ) + with self.assertRaisesRegex( + checker.TemplateContractError, "quoted string" + ): + checker.validate_issue_form(Path("form.yml"), source) + + def test_duplicate_id_fails(self) -> None: + source = valid_form().replace( + 'id: "platform"', 'id: "observed"', 1 + ) + with self.assertRaisesRegex( + checker.TemplateContractError, "duplicate input id" + ): + checker.validate_issue_form(Path("form.yml"), source) + + def test_missing_explicit_required_flag_fails(self) -> None: + source = valid_form().replace( + " validations:\n required: true\n" + " - type: \"dropdown\"", + " - type: \"dropdown\"", + 1, + ) + with self.assertRaisesRegex( + checker.TemplateContractError, "explicit required flag" + ): + checker.validate_issue_form(Path("form.yml"), source) + + def test_yaml_alias_and_odd_indentation_fail(self) -> None: + for source, expected in ( + (valid_form().replace('"testing"', "&labels", 1), "aliases"), + (valid_form().replace(" - type:", " - type:", 1), "two-space"), + ): + with self.subTest(expected=expected): + with self.assertRaisesRegex( + checker.TemplateContractError, expected + ): + checker.validate_issue_form(Path("form.yml"), source) + + def test_pull_request_headings_and_placeholder_pass(self) -> None: + checker.validate_pull_request_template( + Path(".github/pull_request_template.md"), + valid_pull_request_template(), + ) + + def test_pull_request_numeric_closer_and_missing_heading_fail(self) -> None: + source = valid_pull_request_template().replace( + "Closes #NUMBER", "Closes #70" + ) + with self.assertRaisesRegex( + checker.TemplateContractError, "placeholder" + ): + checker.validate_pull_request_template( + Path(".github/pull_request_template.md"), source + ) + source = valid_pull_request_template().replace( + "## Before behavior", "## Before" + ) + with self.assertRaisesRegex( + checker.TemplateContractError, "headings must be exactly" + ): + checker.validate_pull_request_template( + Path(".github/pull_request_template.md"), source + ) + + def test_chooser_requires_the_exact_private_route(self) -> None: + valid = "\n".join( + ( + "blank_issues_enabled: false", + "contact_links:", + ' - name: "Private vulnerability report"', + f' url: "{checker.SECURITY_ROUTE}"', + ' about: "Report suspected vulnerabilities privately; do not use a public issue."', + "", + ) + ) + checker.validate_chooser( + Path(".github/ISSUE_TEMPLATE/config.yml"), valid + ) + with self.assertRaisesRegex( + checker.TemplateContractError, "private route" + ): + checker.validate_chooser( + Path(".github/ISSUE_TEMPLATE/config.yml"), + valid.replace("security/advisories/new", "issues/new"), + ) + + def test_missing_repository_artifacts_fail_closed(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + with self.assertRaisesRegex( + checker.TemplateContractError, "cannot read required" + ): + checker.check_repository(Path(temporary)) + + +if __name__ == "__main__": + unittest.main() From 046c95668f36b80dfd3999ccd46bda7b388fd089 Mon Sep 17 00:00:00 2001 From: plx Date: Mon, 27 Jul 2026 14:20:00 -0500 Subject: [PATCH 2/6] Record issue 70 validation evidence --- audits/2026-07-27-issue-70-contributor-workflow.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/audits/2026-07-27-issue-70-contributor-workflow.md b/audits/2026-07-27-issue-70-contributor-workflow.md index 89b2f50..ca1504f 100644 --- a/audits/2026-07-27-issue-70-contributor-workflow.md +++ b/audits/2026-07-27-issue-70-contributor-workflow.md @@ -94,9 +94,15 @@ The exact candidate source passed: These commands targeted the exact candidate source in the issue working tree. The first package and dry-run exercise used `--allow-dirty` only because the -candidate audit had not yet been committed. The exact non-`--allow-dirty` -package commands and focused contracts are repeated on the clean committed -tree before publication, and that commit/result is appended here. +candidate audit had not yet been committed. + +Commit `34ce3479b8811b6afc85ccccdb41954be7fe4c1a` was then clean before and +after the exact non-`--allow-dirty` `cargo package --locked` and +`cargo publish --dry-run --locked` commands. Both passed with the reviewed +35-file package and Cargo's expected dry-run upload abort. The three focused +Rust contracts, nine Python regressions, live template checker, exact +documentation/workflow commands, formatting checks, and release-mode +navigation-guide verification also passed from that clean commit. Hosted CI remains authoritative for Linux, macOS, and Windows. A single local machine is not represented as three-platform evidence. From c83f89e7b579bb82ca3f5d2b1022126614c76506 Mon Sep 17 00:00:00 2001 From: plx Date: Mon, 27 Jul 2026 14:27:32 -0500 Subject: [PATCH 3/6] Address contributor workflow review --- .github/workflows/ci.yml | 6 +++-- AGENTIC_NAVIGATION_GUIDE.md | 2 +- CONTRIBUTING.md | 3 +-- ...026-07-27-issue-70-contributor-workflow.md | 21 ++++++++++++--- justfile | 5 ++++ tests/issue_70_contributor_workflow.rs | 27 ++++++++++++++++--- 6 files changed, 53 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e49289e..24a23c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -212,6 +212,9 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" + - uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.68.5 + with: + tool: just@1.51.0 - name: Install checksum-pinned actionlint 1.7.12 env: ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 @@ -271,8 +274,7 @@ jobs: - name: Validate contributor guidance and templates run: | python3 -m py_compile scripts/check_contributor_templates.py - PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests/test_check_contributor_templates.py -v - python3 scripts/check_contributor_templates.py + just test-contributor-templates - name: Lint maintained Markdown run: | rumdl check --disable MD010,MD013,MD038 README.md diff --git a/AGENTIC_NAVIGATION_GUIDE.md b/AGENTIC_NAVIGATION_GUIDE.md index 9c5388b..8379fa5 100644 --- a/AGENTIC_NAVIGATION_GUIDE.md +++ b/AGENTIC_NAVIGATION_GUIDE.md @@ -115,7 +115,7 @@ - NOTICE # Apache-2.0 section 4(d) notice for downstream distributors - LICENSE-MIT - LICENSE-APACHE -- justfile # Repository task aliases, including production-readiness work selection +- justfile # Repository task aliases, including work selection and contributor-template checks - scripts/ - check_coverage.py # Fail-closed overall and critical-module branch-aware coverage policy - check_mutation_report.py # Reviewed blocker-mutation completeness and survivor gate diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57b867f..d7cc0e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -175,8 +175,7 @@ just test-production-readiness-selector just test-release-identity just test-github-protections just test-quality-gates -PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests/test_check_contributor_templates.py -v -python3 scripts/check_contributor_templates.py +just test-contributor-templates ``` For maintained Markdown or GitHub workflow changes, install the exact pinned diff --git a/audits/2026-07-27-issue-70-contributor-workflow.md b/audits/2026-07-27-issue-70-contributor-workflow.md index ca1504f..ef81dae 100644 --- a/audits/2026-07-27-issue-70-contributor-workflow.md +++ b/audits/2026-07-27-issue-70-contributor-workflow.md @@ -120,9 +120,24 @@ content will be left open. ## Representative-ticket cold read The contributor guide was read as the only process reference alongside a -representative issue-scoped remediation ticket. The cold-read record is -completed after the final guide text and automated review are available. -Known prerequisites that must remain discoverable without oral context are: +representative issue-scoped remediation ticket. The hosted automated cold +review on +[PR #137](https://github.com/plx/agentic-navigation-guide/pull/137#issuecomment-5095796945) +reported no security or correctness concern and found two useful +instruction/proof gaps: + +- the template checker needed a discoverable versioned `just` entry point; and +- the Rust contract needed to bind Python `3.12` to the `workflow-lint` job + rather than accepting the same pin elsewhere in CI. + +Both findings were corrected. The checker now runs through +`just test-contributor-templates` locally and in CI, and the Rust contract +extracts and checks the exact owning workflow job. The review's Markdown-rule +note required no weakening: the new documents pass with MD010 and MD038 still +enabled. + +The cold reader confirmed that these prerequisites remain discoverable without +oral context: - the sole normative contract and concise README roles; - exact MSRV/stable/tool pins and locked commands; diff --git a/justfile b/justfile index 6c485eb..04d4909 100644 --- a/justfile +++ b/justfile @@ -26,6 +26,11 @@ audit-github-protections *args: test-github-protections: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests/test_audit_github_protections.py -v +# Run the contributor-template checker and its fail-closed regressions. +test-contributor-templates: + PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests/test_check_contributor_templates.py -v + python3 scripts/check_contributor_templates.py + # Run fail-closed coverage, mutation-report, and performance policy regressions. test-quality-gates: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests/test_check_coverage.py tests/test_check_mutation_report.py tests/test_performance_baseline.py -v diff --git a/tests/issue_70_contributor_workflow.rs b/tests/issue_70_contributor_workflow.rs index eafdbe0..9d60601 100644 --- a/tests/issue_70_contributor_workflow.rs +++ b/tests/issue_70_contributor_workflow.rs @@ -19,6 +19,14 @@ fn issue_70_contributor_guide_is_complete_and_uses_real_pinned_commands() { let guide = repository_file("CONTRIBUTING.md"); let normalized = normalized_whitespace(&guide); let ci = repository_file(".github/workflows/ci.yml"); + let justfile = repository_file("justfile"); + let workflow_lint = ci + .split_once(" workflow-lint:\n") + .expect("CI must contain the workflow-lint job") + .1 + .split_once("\n issue-selector:\n") + .expect("workflow-lint must precede the issue-selector job") + .0; let workflow_sources = format!( "{ci}\n{}", repository_file(".github/workflows/verify-guide.yml") @@ -59,6 +67,7 @@ fn issue_70_contributor_guide_is_complete_and_uses_real_pinned_commands() { "just test-release-identity", "just test-github-protections", "just test-quality-gates", + "just test-contributor-templates", "cargo test --locked parser_robustness_tests:: -- --nocapture", "No fuzz target or corpus exists", "Issue #56 remains deferred", @@ -84,7 +93,6 @@ fn issue_70_contributor_guide_is_complete_and_uses_real_pinned_commands() { ("Rust `1.85.0`", "rust: \"1.85.0\""), ("Rust `1.96.1`", "rust: \"1.96.1\""), ("Rust `1.97.1`", "rust: \"1.97.1\""), - ("Python `3.12`", "python-version: \"3.12\""), ("`just 1.51.0`", "tool: just@1.51.0"), ("`cargo-llvm-cov 0.8.7`", "tool: cargo-llvm-cov@0.8.7"), ("`cargo-mutants 27.1.0`", "tool: cargo-mutants@27.1.0"), @@ -103,6 +111,17 @@ fn issue_70_contributor_guide_is_complete_and_uses_real_pinned_commands() { "CI omits contributor-guide pin source {workflow_pin:?}" ); } + assert!(guide.contains("Python `3.12`")); + assert!(workflow_lint.contains("python-version: \"3.12\"")); + assert!(workflow_lint.contains("tool: just@1.51.0")); + assert!(workflow_lint.contains("just test-contributor-templates")); + assert!(justfile.contains("test-contributor-templates:")); + assert!( + justfile.contains( + "PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests/test_check_contributor_templates.py -v" + ) + ); + assert!(justfile.contains("python3 scripts/check_contributor_templates.py")); for command in [ "cargo check --locked --all-targets --all-features", @@ -203,13 +222,15 @@ fn issue_70_templates_capture_required_issue_and_pull_request_evidence() { #[test] fn issue_70_ci_docs_and_audit_keep_the_contributor_contract_current() { let ci = repository_file(".github/workflows/ci.yml"); + let justfile = repository_file("justfile"); let readme = repository_file("README.md"); let changelog = repository_file("CHANGELOG.md"); let navigation = repository_file("AGENTIC_NAVIGATION_GUIDE.md"); let audit = repository_file("audits/2026-07-27-issue-70-contributor-workflow.md"); - assert!(ci.contains("python3 scripts/check_contributor_templates.py")); - assert!(ci.contains("tests/test_check_contributor_templates.py -v")); + assert!(ci.contains("just test-contributor-templates")); + assert!(justfile.contains("python3 scripts/check_contributor_templates.py")); + assert!(justfile.contains("tests/test_check_contributor_templates.py -v")); assert!( ci.contains("rumdl check --disable MD013 CONTRIBUTING.md .github/pull_request_template.md") ); From cdebd8b4bb793c053dfc157b9c16fb95c7dee2dc Mon Sep 17 00:00:00 2001 From: plx Date: Mon, 27 Jul 2026 14:29:02 -0500 Subject: [PATCH 4/6] Correct install action version annotation --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24a23c5..2e84e38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -212,7 +212,7 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - - uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.68.5 + - uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.77.6 with: tool: just@1.51.0 - name: Install checksum-pinned actionlint 1.7.12 From 1daadeea22268a244ab62c489edb3160a55ad7bf Mon Sep 17 00:00:00 2001 From: plx Date: Mon, 27 Jul 2026 14:34:21 -0500 Subject: [PATCH 5/6] Make workflow contract line-ending invariant --- ...026-07-27-issue-70-contributor-workflow.md | 6 ++++ tests/issue_70_contributor_workflow.rs | 30 ++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/audits/2026-07-27-issue-70-contributor-workflow.md b/audits/2026-07-27-issue-70-contributor-workflow.md index ef81dae..34e2ce3 100644 --- a/audits/2026-07-27-issue-70-contributor-workflow.md +++ b/audits/2026-07-27-issue-70-contributor-workflow.md @@ -136,6 +136,12 @@ extracts and checks the exact owning workflow job. The review's Markdown-rule note required no weakening: the new documents pass with MD010 and MD038 still enabled. +The first hosted Windows run then exposed that the new workflow-job extractor +assumed an LF checkout. The test now normalizes Git's LF and CRLF forms before +extracting the owning job and explicitly asserts that both representations +produce identical evidence. This was a test-proof portability defect; the +issue forms and contributor guidance were unchanged. + The cold reader confirmed that these prerequisites remain discoverable without oral context: diff --git a/tests/issue_70_contributor_workflow.rs b/tests/issue_70_contributor_workflow.rs index 9d60601..be0743a 100644 --- a/tests/issue_70_contributor_workflow.rs +++ b/tests/issue_70_contributor_workflow.rs @@ -14,19 +14,35 @@ fn normalized_whitespace(source: &str) -> String { source.split_whitespace().collect::>().join(" ") } +fn workflow_job(source: &str, job: &str, next_job: &str) -> String { + let normalized = source.replace("\r\n", "\n"); + normalized + .split_once(&format!(" {job}:\n")) + .unwrap_or_else(|| panic!("CI must contain the {job} job")) + .1 + .split_once(&format!("\n {next_job}:\n")) + .unwrap_or_else(|| panic!("{job} must precede the {next_job} job")) + .0 + .to_owned() +} + #[test] fn issue_70_contributor_guide_is_complete_and_uses_real_pinned_commands() { let guide = repository_file("CONTRIBUTING.md"); let normalized = normalized_whitespace(&guide); let ci = repository_file(".github/workflows/ci.yml"); let justfile = repository_file("justfile"); - let workflow_lint = ci - .split_once(" workflow-lint:\n") - .expect("CI must contain the workflow-lint job") - .1 - .split_once("\n issue-selector:\n") - .expect("workflow-lint must precede the issue-selector job") - .0; + let workflow_lint = workflow_job(&ci, "workflow-lint", "issue-selector"); + let canonical_ci = ci.replace("\r\n", "\n"); + assert_eq!( + workflow_lint, + workflow_job( + &canonical_ci.replace('\n', "\r\n"), + "workflow-lint", + "issue-selector" + ), + "workflow-job checks must be invariant across Git LF and CRLF checkouts" + ); let workflow_sources = format!( "{ci}\n{}", repository_file(".github/workflows/verify-guide.yml") From ccfcc1b951a11334cfd04284e1010f54f71a3d10 Mon Sep 17 00:00:00 2001 From: plx Date: Mon, 27 Jul 2026 14:45:00 -0500 Subject: [PATCH 6/6] Exercise checked-in contributor artifacts --- ...026-07-27-issue-70-contributor-workflow.md | 23 +++++++++++++------ tests/test_check_contributor_templates.py | 3 +++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/audits/2026-07-27-issue-70-contributor-workflow.md b/audits/2026-07-27-issue-70-contributor-workflow.md index 34e2ce3..2d42695 100644 --- a/audits/2026-07-27-issue-70-contributor-workflow.md +++ b/audits/2026-07-27-issue-70-contributor-workflow.md @@ -84,7 +84,7 @@ The exact candidate source passed: | Rust `1.97.1` | Locked all-target/all-feature check, test, and warnings-denied Clippy passed | | Full conformance suite | `GUIDE_FORMAT_REQUIRE_CONFORMANCE=all` passed 393 tests; the two manual benchmarks and explicit package test were the three documented ignores | | Explicit packaged-artifact acceptance | The ignored issue #62 exact-package install, smoke, and no-library-consumer test passed separately | -| Issue #70 contracts | Three Rust tests, nine Python negative/unit tests, and the live constrained-template check passed | +| Issue #70 contracts | Three Rust tests, ten Python tests (including the checked-in repository), and the live constrained-template check passed | | Deterministic parser matrix | The five issue #57 fixed equivalence-matrix tests passed; no generated input was used | | Related and repository policy | Eleven issue #61/#66/#69 Rust contracts and 101 selector, identity, protection, coverage, fixed-mutation-report, and performance-policy tests passed | | Package | The exact 35-file allowlist packaged and verified; `cargo publish --dry-run --locked` reached only Cargo's expected pre-upload abort | @@ -109,13 +109,15 @@ machine is not represented as three-platform evidence. ## Template validation and preview -The constrained local validator, its nine negative/unit regressions, and a +The constrained local validator, its ten unit/integration regressions, and a second parse with Ruby's maintained YAML implementation pass before -publication. GitHub only makes repository issue and pull request templates -available from the default branch, so the rendered chooser, each issue form, -the pull request template, and the community-profile contribution surface must -be checked immediately after merge. No draft test issue containing placeholder -content will be left open. +publication. The Python suite includes an explicit positive check of the +checked-in forms, chooser, pull request template, and contributor guide. +GitHub only makes repository issue and pull request templates available from +the default branch, so the rendered chooser, each issue form, the pull request +template, and the community-profile contribution surface must be checked +immediately after merge. No draft test issue containing placeholder content +will be left open. ## Representative-ticket cold read @@ -142,6 +144,13 @@ extracting the owning job and explicitly asserts that both representations produce identical evidence. This was a test-proof portability defect; the issue forms and contributor guidance were unchanged. +The final automated review on +[the corrected head](https://github.com/plx/agentic-navigation-guide/pull/137#issuecomment-5095926802) +reported no bug, security concern, or performance concern. Its one useful test +gap was that the unit module did not itself name the live checked-in positive +path. `test_checked_in_repository_artifacts_pass` now makes that integration +contract explicit; the recipe retains the separate CLI-style live check. + The cold reader confirmed that these prerequisites remain discoverable without oral context: diff --git a/tests/test_check_contributor_templates.py b/tests/test_check_contributor_templates.py index 72723b6..2e0b7fd 100644 --- a/tests/test_check_contributor_templates.py +++ b/tests/test_check_contributor_templates.py @@ -71,6 +71,9 @@ def valid_pull_request_template() -> str: class ContributorTemplateCheckerTests(unittest.TestCase): + def test_checked_in_repository_artifacts_pass(self) -> None: + checker.check_repository(ROOT) + def test_valid_constrained_form_passes(self) -> None: form = checker.validate_issue_form(Path("form.yml"), valid_form()) self.assertEqual(form.name, "Test form")