Skip to content

Add an optional en-GB-oxendict spelling gate (default on) - #55

Open
leynos wants to merge 4 commits into
mainfrom
add-en-gb-oxendict-gate
Open

Add an optional en-GB-oxendict spelling gate (default on)#55
leynos wants to merge 4 commits into
mainfrom
add-en-gb-oxendict-gate

Conversation

@leynos

@leynos leynos commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Adds an en-GB-oxendict (Oxford "-ize") spelling gate to generated repositories as a new boolean Copier option, en_gb_oxendict, default enabled.

Implementation

The rebased branch builds on the shared-dictionary spelling implementation now present on main:

  • generated repositories receive typos.toml, typos.local.toml, and the scripts/generate_typos_config.py / scripts/typos_rollout.py generator pair;
  • typos is pinned at 1.48.0 through TYPOS_VERSION and run with uv tool run;
  • make spellcheck invokes the generator with uv run, so its declared Python version is honoured, then checks Markdown with typos;
  • make markdownlint depends on make spellcheck and CI runs the gate after Setup uv.

Conditional root files retain the .jinja template suffix, allowing Copier to evaluate their conditional path names correctly.

Cache safety

  • local, cached, and HTTP dictionaries are limited to 1 MiB; HTTP responses reject oversized Content-Length values and stream through a capped reader;
  • a cross-process advisory lock serializes cache and freshness-metadata writers;
  • validated cache replacement remains atomic, and interrupted metadata writes retry safely on the next refresh;
  • stale-cache fallback emits a bounded diagnostic with the operation, failure category or HTTP status, and cache age without exposing source URLs.

Main integration

The latest main adds the independent enable_polonius Copier option. The shared render helper omits either option when its value is None, so Copier's real defaults are exercised. Property tests hold flavour and Polonius state constant while toggling spelling, proving that spelling traces vary only with en_gb_oxendict and Polonius output is unchanged.

Toggle semantics

When en_gb_oxendict is true (the default), generated repositories gain the spelling configuration, generator scripts, Makefile/CI wiring, and spelling documentation.

When false, renders carry no trace of the gate: no spelling files or scripts/ directory, cache-ignore entries, Makefile wiring, CI step, or generated spelling guidance.

Validation

  • Focused default-on, disabled, resource-bound, concurrency, retry, stale-cache, and Polonius-independence contracts pass.
  • Generated Makefiles validate with mbake and the structured snapshots are reviewed.
  • make check-fmt, make test, make typecheck, make lint, and make spelling pass.
  • Parent suite result: 99 passed, 1 skipped.

References

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Summary

  • Add the enabled-by-default en_gb_oxendict Copier option.
  • Generate pinned typos configuration, rollout scripts, Makefile targets, CI checks, and spelling documentation when enabled.
  • Remove all spelling-related files and integration when disabled.
  • Make markdownlint depend on spellcheck when the gate is enabled.
  • Bound dictionary inputs, serialize cache writes, use atomic replacement, and report stale-cache diagnostics.
  • Preserve compatibility with enable_polonius.
  • Document the design in ADR-003.
  • Add contract tests, snapshots, Makefile validation, formatting, type checking, linting, and integration coverage.

Validation

  • Parent suite: 99 passed, 1 skipped.

Walkthrough

Changes

Oxford spelling gate

Layer / File(s) Summary
Dictionary rollout and generated configuration
scripts/typos_rollout.py, template/.../typos_rollout.py, template/.../generate_typos_config.py, template/.../typos.toml, template/.../typos.local.toml
Add bounded reads, HTTPS redirect checks, cache locking, stale-cache handling, dictionary merging, and deterministic typos.toml generation.
Conditional template gate wiring
copier.yaml, template/Makefile.jinja, template/.github/workflows/ci.yml.jinja, template/.gitignore.jinja, template/AGENTS.md.jinja, template/docs/*
Add the default-enabled en_gb_oxendict option. Emit spelling files, commands, CI checks, cache ignores, and guidance only when enabled.
Parent documentation and rollout policy
docs/adr-003-shared-oxford-spelling-base.md, docs/developers-guide.md, docs/users-guide.md, scripts/generate_typos_config.py
Document separate parent and generated-project gates, bounded inputs, validation, locking, stale-cache diagnostics, offline reuse, and regeneration before Markdown checks.
Render contracts and rollout tests
tests/helpers/*, tests/test_template/*, tests/test_typos_rollout.py, .gitignore
Update Makefile and CI contracts and snapshots. Test enabled and disabled renders, generated configuration, source changes, size limits, redirects, stale-cache fallback, cleanup, and concurrent locking.

Sequence Diagram(s)

sequenceDiagram
  participant Copier
  participant Makefile
  participant Generator
  participant Typos
  participant CI
  Copier->>Makefile: Render spellcheck target when enabled
  CI->>Makefile: Run make spellcheck
  Makefile->>Generator: Refresh dictionary and write typos.toml
  Makefile->>Typos: Scan Markdown with generated configuration
Loading

Possibly related PRs

Poem

Enable the gate; let Oxford words align.
Refresh the cache and render each line.
Make spellcheck guard the page.
Let CI run the spelling stage.
When disabled, leave no trace behind.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 4 warnings, 2 inconclusive)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error The rollout tests exercise the parent scripts, but rendered script tests only check markers and basic output; a no-op rendered generator could pass because typos.toml is pre-populated. Add an end-to-end rendered-project test with a controlled local source and assert cache refresh, overlay merge, regeneration, offline reuse, and failure handling.
Unit Architecture ❌ Error _valid_cache() performs filesystem/parsing work but returns only bool and suppresses OSError; refresh_base() hard-codes fcntl, urllib, time, logging, and Path I/O. Split pure parsing/rendering from cache and HTTP command services; inject filesystem, transport, clock, and logger dependencies; expose cache-validation errors instead of returning False.
User-Facing Documentation ⚠️ Warning docs/users-guide.md documents the option and targets, but no n+1 migration document signposts the new default gate or the spelling to spellcheck rename. Add the next minor migration guide for this pre-1.0 template. Explain the default-enabled option, generated files, and target rename, then link it from the user guide.
Domain Architecture ⚠️ Warning scripts/typos_rollout.py combines pure spelling policy with TOML serialization, filesystem cache writes, fcntl locking, and HTTPS transport; RefreshResult and errors expose infrastructure types. Split policy parsing, merging, and mapping into an infrastructure-free module. Inject filesystem and HTTP adapters, and translate their failures into domain-level results before orchestration.
Observability ⚠️ Warning The new HTTPS refresh, cache, and cross-process lock paths emit only one stale-cache warning; no metrics or tracing expose refresh outcomes, latency, resource limits, or lock behaviour. Add bounded refresh metrics and structured tracing or equivalent CLI telemetry for status, failure category, cache age, HTTP and lock timing, and size-limit events; keep URLs out.
Performance And Resource Use ⚠️ Warning _read_metadata loads the sidecar with unbounded read_text/json.loads, unlike the 1 MiB dictionary cap; spellcheck also recursively walks all directories before exclusions. Bound metadata reads and writes, and prune excluded directories in the Markdown scan before invoking typos.
Testing (Unit And Behavioural) ❓ Inconclusive Investigation pending. Inspect the added unit and behavioural tests against the changed generator, templates, and generated-project workflows.
Security And Privacy ❓ Inconclusive Investigation is still in progress; no verdict evidence submitted yet. Inspect the changed refresh, cache, URL, file-write, and command-execution paths before deciding.
✅ Passed checks (12 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding an optional, default-enabled en-GB-oxendict spelling gate.
Description check ✅ Passed The description directly explains the spelling gate, implementation, toggle behaviour, safety controls, integration, and validation.
Docstring Coverage ✅ Passed Docstring coverage is 89.86% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Developer Documentation ✅ Passed Accept the documentation: docs/developers-guide.md covers the gate and cache workflow, ADR-003 records the design, and no roadmap or locale updates are required.
Module-Level Documentation ✅ Passed Accept the check: all 14 changed Python modules have module docstrings, and the rollout docstring states its relationship to generate_typos_config.py; the changed Rust stub has //! documentation.
Testing (Property / Proof) ✅ Passed Hypothesis tests cover arbitrary Oxford stems and spelling/Polonius option combinations; boundary, cache-order, retry, redirect, and concurrency invariants also have focused tests.
Testing (Compile-Time / Ui) ✅ Passed No Rust/TypeScript implementation changed, so trybuild is not applicable. Syrupy snapshots cover generated Makefile/CI behaviour, with focused assertions for enabled and disabled text output.
Concurrency And State ✅ Passed Accept the check: refresh_base owns cache state behind a per-cache flock; atomic writes and finally unlocks protect failures, while tests cover contention, retry, stale-cache, and bounded ref...
Architectural Complexity And Maintainability ✅ Passed Accept the design: typos_rollout.py isolates explicit parsing, HTTP, cache, locking, and atomic-write invariants; the ADR and tests define reuse, while standard-library code avoids new dependencies.
Rust Compiler Lint Integrity ✅ Passed The diff contains no Rust paths or Rust code changes. It adds or edits Python, Jinja, TOML, Markdown, and test files only, so it introduces no Rust lint suppressions or clone changes.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch add-en-gb-oxendict-gate
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-en-gb-oxendict-gate

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21fa0e0aef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread template/{% if en_gb_oxendict %}typos.toml{% endif %} Outdated
Comment thread template/Makefile.jinja Outdated
@lodyai
lodyai Bot force-pushed the add-en-gb-oxendict-gate branch 2 times, most recently from 31e24b8 to 52292bd Compare August 2, 2026 13:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@template/`{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py:
- Around line 34-50: Document all specified public interfaces with NumPy-style
docstrings: in template/{% if en_gb_oxendict %}scripts{% endif
%}/typos_rollout.py lines 34-50, add Attributes and invariants for Dictionary
and RefreshResult; lines 106-108 document load_dictionary parameters and parse
failures; lines 111-155 document merge and generated-mapping conflict behavior;
lines 171-192 document rendering output and TOML validation failures; lines
209-211 document write behavior and filesystem failures; and lines 383-398
document source selection, offline behavior, statuses, and exceptions. In
template/{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py
lines 25-58, document cache merging, generated output, and refresh options. Use
structured Parameters, Returns, Raises, and relevant Attributes sections for
public functions, classes, and methods; retain only single-line summaries for
private helpers.
- Around line 354-364: Update the refresh flow around _read_metadata,
_conditional_headers, and _remote_is_not_newer to detect when saved["source"]
differs from source, discard the prior metadata and cache validators for that
request, and prevent returning the old cache as current. Add a regression test
that refreshes two different URLs using matching ETags or timestamps and
verifies the second URL is fetched and its cache is retained.
- Around line 317-325: Update the HTTP request flow around _https_request and
urllib.request.urlopen so every redirect target is validated before it is
opened, rejecting any target whose URL scheme is not HTTPS. Preserve the
existing HTTPS-only validation for the initial source and ensure redirects
cannot downgrade to HTTP.

In `@tests/test_template/test_spelling_gate.py`:
- Around line 72-89: Update test_spelling_gate_config_matches_generator to run
scripts/generate_typos_config.py in the rendered project with
subprocess.run(check=True, cwd=project) before reading typos.toml; add the
required subprocess import and keep the existing assertions against the
regenerated file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ee87e1f6-4519-4245-a9bc-e55dd5440784

📥 Commits

Reviewing files that changed from the base of the PR and between bd8c3cf and 52292bd.

📒 Files selected for processing (18)
  • copier.yaml
  • docs/users-guide.md
  • template/.github/workflows/ci.yml.jinja
  • template/AGENTS.md.jinja
  • template/Makefile.jinja
  • template/docs/developers-guide.md.jinja
  • template/docs/documentation-style-guide.md
  • template/docs/repository-layout.md.jinja
  • template/docs/users-guide.md.jinja
  • template/{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py
  • template/{% if en_gb_oxendict %}typos.local.toml{% endif %}.jinja
  • template/{% if en_gb_oxendict %}typos.toml{% endif %}.jinja
  • tests/helpers/rendering.py
  • tests/helpers/tooling_contracts/makefile.py
  • tests/helpers/tooling_contracts/workflows.py
  • tests/test_template/__snapshots__/test_snapshots.ambr
  • tests/test_template/test_spelling_gate.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/whitaker (auto-detected)

Comment thread tests/test_template/test_spelling_gate.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@template/`{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py:
- Around line 34-50: Document all specified public interfaces with NumPy-style
docstrings: in template/{% if en_gb_oxendict %}scripts{% endif
%}/typos_rollout.py lines 34-50, add Attributes and invariants for Dictionary
and RefreshResult; lines 106-108 document load_dictionary parameters and parse
failures; lines 111-155 document merge and generated-mapping conflict behavior;
lines 171-192 document rendering output and TOML validation failures; lines
209-211 document write behavior and filesystem failures; and lines 383-398
document source selection, offline behavior, statuses, and exceptions. In
template/{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py
lines 25-58, document cache merging, generated output, and refresh options. Use
structured Parameters, Returns, Raises, and relevant Attributes sections for
public functions, classes, and methods; retain only single-line summaries for
private helpers.
- Around line 354-364: Update the refresh flow around _read_metadata,
_conditional_headers, and _remote_is_not_newer to detect when saved["source"]
differs from source, discard the prior metadata and cache validators for that
request, and prevent returning the old cache as current. Add a regression test
that refreshes two different URLs using matching ETags or timestamps and
verifies the second URL is fetched and its cache is retained.
- Around line 317-325: Update the HTTP request flow around _https_request and
urllib.request.urlopen so every redirect target is validated before it is
opened, rejecting any target whose URL scheme is not HTTPS. Preserve the
existing HTTPS-only validation for the initial source and ensure redirects
cannot downgrade to HTTP.

In `@tests/test_template/test_spelling_gate.py`:
- Around line 72-89: Update test_spelling_gate_config_matches_generator to run
scripts/generate_typos_config.py in the rendered project with
subprocess.run(check=True, cwd=project) before reading typos.toml; add the
required subprocess import and keep the existing assertions against the
regenerated file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ee87e1f6-4519-4245-a9bc-e55dd5440784

📥 Commits

Reviewing files that changed from the base of the PR and between bd8c3cf and 52292bd.

📒 Files selected for processing (18)
  • copier.yaml
  • docs/users-guide.md
  • template/.github/workflows/ci.yml.jinja
  • template/AGENTS.md.jinja
  • template/Makefile.jinja
  • template/docs/developers-guide.md.jinja
  • template/docs/documentation-style-guide.md
  • template/docs/repository-layout.md.jinja
  • template/docs/users-guide.md.jinja
  • template/{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py
  • template/{% if en_gb_oxendict %}typos.local.toml{% endif %}.jinja
  • template/{% if en_gb_oxendict %}typos.toml{% endif %}.jinja
  • tests/helpers/rendering.py
  • tests/helpers/tooling_contracts/makefile.py
  • tests/helpers/tooling_contracts/workflows.py
  • tests/test_template/__snapshots__/test_snapshots.ambr
  • tests/test_template/test_spelling_gate.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/whitaker (auto-detected)
🛑 Comments failed to post (3)
template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py (3)

34-50: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Document all public interfaces in NumPy style.

Add structured Parameters, Returns, Raises, and relevant Attributes sections. Keep single-line summaries only for private helpers.

  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L34-L50: Document Dictionary and RefreshResult attributes and invariants.
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L106-L108: Document load_dictionary inputs and parse failures.
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L111-L155: Document merge and generated-mapping conflict behaviour.
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L171-L192: Document rendering output and TOML validation failures.
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L209-L211: Document write behaviour and filesystem failures.
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L383-L398: Document source selection, offline behaviour, statuses, and exceptions.
  • template/{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py#L25-L58: Document cache merging, generated output, and refresh options.

As per path instructions, “Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.”

📍 Affects 2 files
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L34-L50 (this comment)
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L106-L108
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L111-L155
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L171-L192
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L209-L211
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L383-L398
  • template/{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py#L25-L58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@template/`{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py around
lines 34 - 50, Document all specified public interfaces with NumPy-style
docstrings: in template/{% if en_gb_oxendict %}scripts{% endif
%}/typos_rollout.py lines 34-50, add Attributes and invariants for Dictionary
and RefreshResult; lines 106-108 document load_dictionary parameters and parse
failures; lines 111-155 document merge and generated-mapping conflict behavior;
lines 171-192 document rendering output and TOML validation failures; lines
209-211 document write behavior and filesystem failures; and lines 383-398
document source selection, offline behavior, statuses, and exceptions. In
template/{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py
lines 25-58, document cache merging, generated output, and refresh options. Use
structured Parameters, Returns, Raises, and relevant Attributes sections for
public functions, classes, and methods; retain only single-line summaries for
private helpers.

Source: Path instructions


317-325: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Python 3.13 official documentation urllib.request HTTPRedirectHandler HTTPS to HTTP redirect default

💡 Result:

In Python 3.13, the urllib.request.HTTPRedirectHandler does not inherently block or restrict redirects from HTTPS to HTTP by default based on the protocol switch itself [1][2]. The HTTPRedirectHandler performs a security check on the scheme of the redirected URL, but it permits transitions between 'http', 'https', and 'ftp' [1][3]. Specifically, the internal redirect_request method verifies the scheme of the new URL: if urlparts.scheme not in ('http', 'https', 'ftp', ''): raise HTTPError(...) As long as the target URL uses one of these allowed schemes, the redirect is processed [1][2]. There is no default logic in the standard library's HTTPRedirectHandler that inspects the original request's protocol (HTTPS) and prevents it from being redirected to an insecure protocol (HTTP) [1][4]. If you require stricter behavior—such as disallowing "downgrade" redirects from HTTPS to HTTP—you must subclass HTTPRedirectHandler and override the redirect_request method to implement custom validation logic that checks the scheme of both the current request and the target URL [2][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
file=$(find template -path '*typos_rollout.py' -print -quit)
printf '%s\n' "FILE=$file"
sed -n '280,345p' "$file"
rg -n "_https_request|urlopen|HTTPRedirectHandler|source|_refresh_http" template -g '*.py' || true

Repository: leynos/agent-template-rust

Length of output: 6324


🏁 Script executed:

#!/bin/bash
set -e
file=$(find template -path '*typos_rollout.py' -print -quit)
sed -n '345,380p' "$file"
python3 - <<'PY'
import sys
import urllib.request

handler = urllib.request.HTTPRedirectHandler()
request = urllib.request.Request("https://example.test/dictionary")
redirected = handler.redirect_request(
    request,
    None,
    302,
    "Found",
    {"location": "http://attacker.test/dictionary"},
)
print(f"python={sys.version.split()[0]}")
print(f"redirect_request_result={redirected.full_url if redirected else None}")
PY

Repository: leynos/agent-template-rust

Length of output: 1707


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
import sys
import urllib.request

handler = urllib.request.HTTPRedirectHandler()
request = urllib.request.Request("https://example.test/dictionary")
redirected = handler.redirect_request(
    request,
    None,
    302,
    "Found",
    {"location": "http://attacker.test/dictionary"},
    "http://attacker.test/dictionary",
)
print(f"python={sys.version.split()[0]}")
print(f"redirect_request_result={redirected.full_url if redirected else None}")
PY

Repository: leynos/agent-template-rust

Length of output: 234


Reject non-HTTPS redirect targets before opening them.

urllib.request.urlopen() follows HTTPS-to-HTTP redirects. Validate every redirect target to prevent an on-path attacker from altering the spelling configuration or bypassing the gate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@template/`{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py around
lines 317 - 325, Update the HTTP request flow around _https_request and
urllib.request.urlopen so every redirect target is validated before it is
opened, rejecting any target whose URL scheme is not HTTPS. Preserve the
existing HTTPS-only validation for the initial source and ensure redirects
cannot downgrade to HTTP.

354-364: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reset validators when the remote source changes.

Check saved["source"] against source before building conditional headers or comparing response validators. A caller can change main(..., source=...), but this path reuses metadata from the previous URL. Matching ETags or timestamps can then return current and retain a valid cache from the wrong source.

Add a regression test that refreshes two different URLs with matching validators.

Proposed fix
     saved = _read_metadata(metadata)
+    if saved.get("source") != source:
+        saved = {}
     request = _https_request(source, _conditional_headers(saved))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    saved = _read_metadata(metadata)
    if saved.get("source") != source:
        saved = {}
    request = _https_request(source, _conditional_headers(saved))
    try:
        with urllib.request.urlopen(  # noqa: S310 - _https_request rejects non-HTTPS URLs.
            request,
            timeout=30.0,
        ) as response:
            if response.status == HTTP_NOT_MODIFIED and _valid_cache(cache):
                return RefreshResult("current", cache)
            if _valid_cache(cache) and _remote_is_not_newer(saved, response.headers):
                return RefreshResult("current", cache)
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 356-359: Request-controlled URL passed to urlopen; validate against an allowlist to prevent SSRF.
Context: urllib.request.urlopen( # noqa: S310 - _https_request rejects non-HTTPS URLs.
request,
timeout=30.0,
)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(urlopen-unsanitized-data)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@template/`{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py around
lines 354 - 364, Update the refresh flow around _read_metadata,
_conditional_headers, and _remote_is_not_newer to detect when saved["source"]
differs from source, discard the prior metadata and cache validators for that
request, and prevent returning the old cache as current. Add a regression test
that refreshes two different URLs using matching ETags or timestamps and
verifies the second URL is fetched and its cache is retained.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/users-guide.md`:
- Around line 80-84: Update the make spellcheck entry in the users guide to
state that it is available only when en_gb_oxendict is enabled, while preserving
the existing description of its behavior. Ensure the documentation does not
present this target as unconditional.

In `@scripts/generate_typos_config.py`:
- Around line 94-129: Update the Raises section of the refresh function’s
docstring to explicitly document rollout.DictionaryTooLargeError for local or
remote dictionaries exceeding the fixed input limit, while preserving the
existing ValueError description for other validation failures.

In `@scripts/typos_rollout.py`:
- Around line 627-671: Update both scripts/typos_rollout.py (lines 627-671 and
166-176) and template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py
(lines 627-671 and 166-176): extract the repeated stale-cache handling from
_refresh_http into _fallback_or_raise and call it from all three exception
handlers, and change _cache_lock’s return annotation from typ.Iterator to
cabc.Iterator[None]. Add a contract test or checksum assertion ensuring both
modules remain identical, then run make test.

In `@template/`{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py:
- Around line 18-22: Annotate the module-level constants in
generate_typos_config.py explicitly: declare DEFAULT_BASE_URL as str and
REPOSITORY_ROOT as Path, preserving their existing values and initialization.

In `@template/AGENTS.md.jinja`:
- Around line 24-27: Update the conditional closing tags associated with the
en_gb_oxendict sections, including the block near the spelling guidance and the
corresponding block later in the template, to use whitespace-trimming endif
syntax so Copier removes the standalone tag newline and does not insert blank
lines into generated lists.

In `@template/docs/developers-guide.md.jinja`:
- Around line 56-63: Change the conditional “Spelling policy” heading in the
template from level 2 to level 3 so it remains nested under “Tooling” and does
not capture “Security audit ignores”; leave the surrounding prose and
conditional behavior unchanged.

In `@tests/test_template/test_spelling_gate.py`:
- Around line 207-227: Add descriptive assertion messages to every bare assert
in the property-test block, including the typos configuration, generator script,
spellcheck target, counterpart path existence, byte equality, and Polonius
Makefile-line comparisons. Update the assertions around the generated Makefile
and selected/counterpart paths so Hypothesis failures identify the violated
contract directly.
- Around line 161-162: Add descriptive assertion messages to both bare asserts
in the relevant spelling-gate test, preserving their existing conditions and
clearly identifying which forbidden string was found in users_guide.
- Around line 90-94: Update the subprocess invocation in the test to address
Ruff S607 on the argument-list line: preferably resolve the uv executable with
shutil.which("uv") and invoke that path; otherwise add a narrowly scoped # noqa:
S607 on the diagnostic line with an explicit justification, while preserving the
existing S603 suppression and trusted-generator behavior.

In `@tests/test_typos_rollout.py`:
- Around line 368-409: Strengthen test_cache_lock_serializes_generator_processes
by having the child emit a readiness marker immediately before generator.main
attempts lock acquisition, then wait for and assert that marker before releasing
the parent-held lock. Retain the existing post-release success assertions, but
remove reliance on process.wait(timeout=0.2) as proof of lock contention.
- Around line 183-409: Update the new tests in the shown test functions to add
descriptive failure messages to every bare assert, including cache-file checks,
response/request validation, metadata cleanup, retry results, and
subprocess/cache assertions. Preserve each assertion’s existing condition and
follow the file’s established `assert expression, "message"` style.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c805b8e7-eda2-49e3-a3b9-431d3ff3429c

📥 Commits

Reviewing files that changed from the base of the PR and between bd8c3cf and 0342b3e.

📒 Files selected for processing (27)
  • .gitignore
  • copier.yaml
  • docs/adr-003-shared-oxford-spelling-base.md
  • docs/developers-guide.md
  • docs/users-guide.md
  • scripts/generate_typos_config.py
  • scripts/typos_rollout.py
  • template/.github/workflows/ci.yml.jinja
  • template/.gitignore
  • template/.gitignore.jinja
  • template/AGENTS.md.jinja
  • template/Makefile.jinja
  • template/docs/developers-guide.md.jinja
  • template/docs/documentation-style-guide.md
  • template/docs/repository-layout.md.jinja
  • template/docs/users-guide.md.jinja
  • template/scripts/generate_typos_config.py
  • template/{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py
  • template/{% if en_gb_oxendict %}typos.local.toml{% endif %}.jinja
  • template/{% if en_gb_oxendict %}typos.toml{% endif %}.jinja
  • tests/helpers/rendering.py
  • tests/helpers/tooling_contracts/makefile.py
  • tests/helpers/tooling_contracts/workflows.py
  • tests/test_template/__snapshots__/test_snapshots.ambr
  • tests/test_template/test_spelling_gate.py
  • tests/test_typos_rollout.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/whitaker (auto-detected)
💤 Files with no reviewable changes (2)
  • template/.gitignore
  • template/scripts/generate_typos_config.py

Comment thread docs/users-guide.md
Comment on lines +94 to +129
"""Refresh shared policy and write the merged generated configuration.

Parameters
----------
output : Path | None
Generated configuration destination. Defaults to ``typos.toml`` in
``repository``.
repository : Path
Repository that owns the cache, metadata, overlay, and output.
source : str | Path
Authoritative local dictionary path or HTTPS URL.
offline : bool
Reuse a valid cache without consulting ``source`` when true.

Returns
-------
rollout.RefreshResult
Refresh status and the validated cache used to generate the output.

Raises
------
FileNotFoundError
Offline mode has no valid cache or a local source is absent.
OSError
Refresh, locking, or output filesystem operations fail.
TypeError
A dictionary value has the wrong TOML shape.
ValueError
A source, dictionary, merge, or generated mapping is invalid.
tomllib.TOMLDecodeError
Input or generated output is not valid TOML.
urllib.error.HTTPError
A remote refresh fails and no valid cache is available.
urllib.error.URLError
A network refresh fails and no valid cache is available.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document rollout.DictionaryTooLargeError.

The contract tests expect main to raise rollout.DictionaryTooLargeError for oversized local and HTTP dictionaries, but the Raises section lists only ValueError for validation failures. Name the custom exception explicitly so callers can identify the fixed input-limit failure.

Based on learnings, the refresh must bound dictionary inputs and validate them before atomic replacement.

Proposed docstring update
     ValueError
         A source, dictionary, merge, or generated mapping is invalid.
+    rollout.DictionaryTooLargeError
+        A local or remote dictionary exceeds the fixed input limit.
     tomllib.TOMLDecodeError
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate_typos_config.py` around lines 94 - 129, Update the Raises
section of the refresh function’s docstring to explicitly document
rollout.DictionaryTooLargeError for local or remote dictionaries exceeding the
fixed input limit, while preserving the existing ValueError description for
other validation failures.

Source: Learnings

Comment thread scripts/typos_rollout.py
Comment on lines 627 to 671
def _refresh_http(
source: str,
cache: pathlib.Path,
metadata: pathlib.Path,
) -> RefreshResult:
"""Refresh a cache from a validated HTTPS source with stale fallback."""
saved = _read_metadata(metadata)
same_source = saved.get("source") == source
if not same_source:
saved = {}
request = _https_request(source, _conditional_headers(saved))
try:
with urllib.request.urlopen( # noqa: S310 - _https_request rejects non-HTTPS URLs.
request,
timeout=30.0,
) as response:
if response.status == HTTP_NOT_MODIFIED and _valid_cache(cache):
with _open_https(request) as response:
if (
response.status == HTTP_NOT_MODIFIED
and same_source
and _valid_cache(cache)
):
return RefreshResult("current", cache)
if _valid_cache(cache) and _remote_is_not_newer(saved, response.headers):
return RefreshResult("current", cache)
content = _read_bounded_stream(
response,
declared_size=_content_length(response.headers),
)
return _write_remote_cache(
source,
_CacheTargets(cache, metadata),
response.read(),
content,
response.headers,
)
except urllib.error.HTTPError as error:
if error.code == HTTP_NOT_MODIFIED and _valid_cache(cache):
if error.code == HTTP_NOT_MODIFIED and same_source and _valid_cache(cache):
return RefreshResult("current", cache)
if _valid_cache(cache):
return RefreshResult("stale-cache", cache)
return _stale_cache_result(cache, category="http", status=error.code)
raise
except urllib.error.URLError:
if _valid_cache(cache):
return _stale_cache_result(cache, category="network")
raise
except (OSError, urllib.error.URLError):
except OSError:
if _valid_cache(cache):
return RefreshResult("stale-cache", cache)
return _stale_cache_result(cache, category="os")
raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Apply both rollout-module fixes to the parent copy and the template copy. scripts/typos_rollout.py and the template module are identical duplicates, so each correction must land twice or the two copies drift apart. Two corrections apply: extract the repeated stale-cache fallback from _refresh_http to clear Ruff PLR0911, and replace the deprecated typ.Iterator alias with cabc.Iterator.

  • scripts/typos_rollout.py#L627-L671: extract a _fallback_or_raise helper and call it from the three except bodies.
  • scripts/typos_rollout.py#L166-L176: change the _cache_lock return type to cabc.Iterator[None].
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L627-L671: apply the identical _fallback_or_raise extraction.
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L166-L176: change the _cache_lock return type to cabc.Iterator[None].

Add a contract test or a checksum assertion that proves the two modules stay identical. Run make test after the change, as the guidelines require for files under template/.

🧰 Tools
🪛 Ruff (0.16.0)

[warning] 627-627: Too many return statements (7 > 6)

(PLR0911)

📍 Affects 2 files
  • scripts/typos_rollout.py#L627-L671 (this comment)
  • scripts/typos_rollout.py#L166-L176
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L627-L671
  • template/{% if en_gb_oxendict %}scripts{% endif %}/typos_rollout.py#L166-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/typos_rollout.py` around lines 627 - 671, Update both
scripts/typos_rollout.py (lines 627-671 and 166-176) and template/{% if
en_gb_oxendict %}scripts{% endif %}/typos_rollout.py (lines 627-671 and
166-176): extract the repeated stale-cache handling from _refresh_http into
_fallback_or_raise and call it from all three exception handlers, and change
_cache_lock’s return annotation from typ.Iterator to cabc.Iterator[None]. Add a
contract test or checksum assertion ensuring both modules remain identical, then
run make test.

Sources: Coding guidelines, Linters/SAST tools

Comment on lines +18 to +22
DEFAULT_BASE_URL = (
"https://raw.githubusercontent.com/leynos/agent-helper-scripts/"
"refs/heads/main/data/typos-oxendict-base.toml"
)
REPOSITORY_ROOT = Path(__file__).resolve().parent.parent

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add explicit annotations to module constants.

Add : str to DEFAULT_BASE_URL and : Path to REPOSITORY_ROOT. Keep module-level values explicitly typed.

As per path instructions, “All code must have clear type hints using modern style”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@template/`{% if en_gb_oxendict %}scripts{% endif %}/generate_typos_config.py
around lines 18 - 22, Annotate the module-level constants in
generate_typos_config.py explicitly: declare DEFAULT_BASE_URL as str and
REPOSITORY_ROOT as Path, preserving their existing values and initialization.

Source: Path instructions

Comment thread template/AGENTS.md.jinja
Comment on lines +24 to +27
{% if en_gb_oxendict %}- **Use consistent spelling and grammar.** Comments must use en-GB-oxendict
("-ize" / "-yse" / "-our") spelling and grammar, with the exception of
references to external APIs.
{% endif %}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect Copier Jinja whitespace settings and the AGENTS.md conditional blocks.
fd -t f 'copier.yaml' -x rg -n '_envops|trim_blocks|lstrip_blocks|keep_trailing_newline' {}
fd -t f 'AGENTS.md.jinja' -x sed -n '20,30p;333,345p' {}
rg -n 'AGENTS.md' tests -C3

Repository: leynos/agent-template-rust

Length of output: 3869


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- template configuration files ---'
fd -t f -a | rg '(^|/)(copier|pyproject|Makefile|AGENTS\.md\.jinja|test_.*\.py)$' | head -200
printf '%s\n' '--- Jinja environment settings ---'
rg -n -S 'envops|trim_blocks|lstrip_blocks|keep_trailing_newline|copier' . --glob '!**/.git/**' --glob '!**/target/**' | head -240
printf '%s\n' '--- template block context ---'
sed -n '1,60p' template/AGENTS.md.jinja
sed -n '320,350p' template/AGENTS.md.jinja
printf '%s\n' '--- relevant test implementations ---'
sed -n '1,230p' tests/test_template/test_spelling_gate.py
sed -n '1,230p' tests/test_template/test_tooling_contracts.py
printf '%s\n' '--- test and render commands ---'
rg -n 'copier|render|make test|markdownlint|AGENTS' Makefile pyproject.toml tests --glob '*.py' --glob 'Makefile' --glob 'pyproject.toml' | head -260

Repository: leynos/agent-template-rust

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
for path in [Path("copier.yaml"), Path("copier.yml"), Path(".copier-answers.yml"), Path("pyproject.toml")]:
    if path.exists():
        print(f"--- {path} ---")
        print(path.read_text())
PY
printf '%s\n' '--- exact whitespace around conditional tags ---'
python3 - <<'PY'
from pathlib import Path
p = Path("template/AGENTS.md.jinja")
s = p.read_text()
for needle in ["{% if en_gb_oxendict %}", "{% endif %}"]:
    print(f"{needle}: {[i+1 for i,l in enumerate(s.splitlines()) if needle in l]}")
for start, end in [(20, 30), (333, 345)]:
    print(f"--- lines {start}-{end} ---")
    for n, line in enumerate(s.splitlines(), 1):
        if start <= n <= end:
            print(f"{n:4}: {line!r}")
PY
printf '%s\n' '--- repository status and test targets ---'
git status --short
rg -n '^(test|markdownlint|fmt|render)|copier run|copier copy|pytest' Makefile justfile pyproject.toml tox.ini noxfile.py 2>/dev/null || true

Repository: leynos/agent-template-rust

Length of output: 5806


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import importlib.util
print("jinja2_available", importlib.util.find_spec("jinja2") is not None)
print("copier_available", importlib.util.find_spec("copier") is not None)
PY
printf '%s\n' '--- focused render-related test helpers ---'
sed -n '1,110p' tests/helpers/rendering.py
sed -n '1,90p' tests/test_template/test_basic_rendering.py
printf '%s\n' '--- full Copier settings and dependencies ---'
sed -n '1,35p' requirements.txt
sed -n '1,45p' Makefile
printf '%s\n' '--- template whitespace control markers ---'
rg -n '\{%-|-%\}|\{%\s*(if|endif)' template/AGENTS.md.jinja

Repository: leynos/agent-template-rust

Length of output: 8493


🌐 Web query:

Copier documentation default _envops trim_blocks lstrip_blocks keep_trailing_newline

💡 Result:

In Copier, _envops is a configuration key used to pass options directly to the underlying Jinja2 environment [1][2]. While Copier provides specific defaults, it allows you to override these or pass additional supported Jinja2 settings by including them under _envops in your copier.yml file [1][3]. Regarding the specific settings you mentioned: 1. keep_trailing_newline: This is the only Jinja2 environment option that Copier explicitly overrides by default. Copier sets keep_trailing_newline to true [1][2]. If you prefer the standard Jinja2 behavior (where trailing newlines are stripped), you must explicitly set keep_trailing_newline to false in your _envops configuration [1][2]. 2. trim_blocks and lstrip_blocks: These are standard Jinja2 environment options. They are not explicitly configured by Copier by default, meaning they inherit the default values provided by Jinja2 itself (typically false unless otherwise specified by the user). You can enable them by adding them to your _envops block in copier.yml, for example: _envops: trim_blocks: true lstrip_blocks: true Copier does not maintain its own custom implementation for these specific flags; it passes them directly to the Jinja2 Environment constructor [4]. You can refer to the official Jinja2 API documentation for the full list of available options that can be controlled via _envops [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- snapshot and Markdown lint configuration ---'
fd -a -t f | rg '(__snapshots__|snapshot|markdownlint|\.markdownlint|AGENTS)' | head -160
rg -n -S 'MD032|markdownlint|AGENTS' . --glob '!**/.git/**' --glob '!**/target/**' | head -220
printf '%s\n' '--- standalone newline-behaviour verifier ---'
python3 - <<'PY'
from pathlib import Path

source = Path("template/AGENTS.md.jinja").read_text()
lines = source.splitlines(keepends=True)

def render(flag: bool, trim_blocks: bool) -> str:
    out = []
    active = True
    for line in lines:
        if "{% if en_gb_oxendict %}" in line:
            active = flag
            line = line.replace("{% if en_gb_oxendict %}", "")
            if not active:
                line = ""
        if "{% endif %}" in line:
            line = line.replace("{% endif %}", "")
            active = True
        if active and line:
            out.append(line)
        elif active and not line and trim_blocks:
            # Jinja's trim_blocks removes the newline on a block-tag-only line.
            pass
        elif not active and line.endswith("\n") and not trim_blocks:
            # A standalone block tag still contributes its source newline.
            out.append("\n")
    text = "".join(out)
    return text

for trim in (False, True):
    print(f"trim_blocks={trim}")
    for flag in (False, True):
        rendered = render(flag, trim)
        print(f"  en_gb_oxendict={flag}")
        for n, line in enumerate(rendered.splitlines(), 1):
            if line == "" and 1 < n < len(rendered.splitlines()):
                print(f"    blank line {n}; previous={rendered.splitlines()[n-2]!r}; next={rendered.splitlines()[n]!r}")
PY

Repository: leynos/agent-template-rust

Length of output: 25400


Use {% endif -%} at lines 27 and 341. With the default en_gb_oxendict value, Copier preserves the standalone tag newlines and adds blank lines to the generated lists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@template/AGENTS.md.jinja` around lines 24 - 27, Update the conditional
closing tags associated with the en_gb_oxendict sections, including the block
near the spelling guidance and the corresponding block later in the template, to
use whitespace-trimming endif syntax so Copier removes the standalone tag
newline and does not insert blank lines into generated lists.

Comment on lines +90 to +94
subprocess.run( # noqa: S603 - runs the rendered, trusted generator.
["uv", "run", "scripts/generate_typos_config.py"],
check=True,
cwd=project.path,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Suppress the S607 finding on the "uv" invocation, not just S603.

Ruff reports S607 ("start-process-with-partial-path") on the argument-list line because "uv" is resolved through PATH. The # noqa: S603 comment on line 90 does not cover this, because the diagnostic is attached to line 91. Add a targeted, justified suppression for S607, or resolve the full path with shutil.which("uv").

As per path instructions, "Only narrow in-line disables (# noqa: XYZ) are permitted, must be accompanied by justification and used only as a last resort."

🔧 Proposed fix
     subprocess.run(  # noqa: S603 - runs the rendered, trusted generator.
-        ["uv", "run", "scripts/generate_typos_config.py"],
+        ["uv", "run", "scripts/generate_typos_config.py"],  # noqa: S607 - "uv" is resolved via PATH by design.
         check=True,
         cwd=project.path,
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
subprocess.run( # noqa: S603 - runs the rendered, trusted generator.
["uv", "run", "scripts/generate_typos_config.py"],
check=True,
cwd=project.path,
)
subprocess.run( # noqa: S603 - runs the rendered, trusted generator.
["uv", "run", "scripts/generate_typos_config.py"], # noqa: S607 - "uv" is resolved via PATH by design.
check=True,
cwd=project.path,
)
🧰 Tools
🪛 Ruff (0.16.0)

[error] 91-91: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_template/test_spelling_gate.py` around lines 90 - 94, Update the
subprocess invocation in the test to address Ruff S607 on the argument-list
line: preferably resolve the uv executable with shutil.which("uv") and invoke
that path; otherwise add a narrowly scoped # noqa: S607 on the diagnostic line
with an explicit justification, while preserving the existing S603 suppression
and trusted-generator behavior.

Sources: Path instructions, Linters/SAST tools

Comment on lines +161 to +162
assert "spellcheck" not in users_guide
assert "en-GB-oxendict" not in users_guide

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add assertion messages.

Lines 161-162 use bare assert statements. Every other assertion in this file carries a descriptive message. Add one here too, for consistent failure diagnostics.

As per path instructions, "Use assert …, "message" over bare asserts."

♻️ Proposed fix
-    assert "spellcheck" not in users_guide
-    assert "en-GB-oxendict" not in users_guide
+    assert "spellcheck" not in users_guide, (
+        "expected disabled render users guide to omit the spellcheck mention"
+    )
+    assert "en-GB-oxendict" not in users_guide, (
+        "expected disabled render users guide to omit the en-GB-oxendict mention"
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert "spellcheck" not in users_guide
assert "en-GB-oxendict" not in users_guide
assert "spellcheck" not in users_guide, (
"expected disabled render users guide to omit the spellcheck mention"
)
assert "en-GB-oxendict" not in users_guide, (
"expected disabled render users guide to omit the en-GB-oxendict mention"
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_template/test_spelling_gate.py` around lines 161 - 162, Add
descriptive assertion messages to both bare asserts in the relevant
spelling-gate test, preserving their existing conditions and clearly identifying
which forbidden string was found in users_guide.

Source: Path instructions

Comment on lines +207 to +227
assert (project / "typos.toml").exists() is en_gb_oxendict
assert (project / "scripts" / "generate_typos_config.py").exists() is (
en_gb_oxendict
)
makefile = read_generated_text(project / "Makefile")
assert ("spellcheck" in makefile) is en_gb_oxendict

selected_path = project / polonius_path
counterpart_path = counterpart / polonius_path
assert selected_path.exists() is counterpart_path.exists()
if selected_path.exists():
assert selected_path.read_bytes() == counterpart_path.read_bytes()
selected_polonius_lines = [
line for line in makefile.splitlines() if "POLONIUS" in line
]
counterpart_polonius_lines = [
line
for line in read_generated_text(counterpart / "Makefile").splitlines()
if "POLONIUS" in line
]
assert selected_polonius_lines == counterpart_polonius_lines

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add assertion messages to the property test.

Lines 207, 208-210, 212, 216, 218, and 227 use bare assert statements. Add descriptive messages so a Hypothesis-shrunk failure identifies which contract broke without inspecting the generated example by hand.

As per path instructions, "Use assert …, "message" over bare asserts."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_template/test_spelling_gate.py` around lines 207 - 227, Add
descriptive assertion messages to every bare assert in the property-test block,
including the typos configuration, generator script, spellcheck target,
counterpart path existence, byte equality, and Polonius Makefile-line
comparisons. Update the assertions around the generated Makefile and
selected/counterpart paths so Hypothesis failures identify the violated contract
directly.

Source: Path instructions

Comment on lines +183 to +409
def test_oversized_local_dictionary_is_rejected_without_cache_mutation(
generator: types.ModuleType,
tmp_path: pathlib.Path,
) -> None:
"""Local sources over the fixed input limit never reach the cache."""
repository, source = prepare_repository(tmp_path)
source.write_bytes(b"x" * (generator.rollout.MAX_DICTIONARY_BYTES + 1))

with pytest.raises(
generator.rollout.DictionaryTooLargeError,
match="dictionary declares",
):
generator.main(repository=repository, source=source)

assert not (repository / ".typos-oxendict-base.toml").exists()
assert not (repository / ".typos-oxendict-base.json").exists()


@pytest.mark.parametrize(
("headers", "content", "message"),
[
({"Content-Length": "1048577"}, b"", "dictionary declares"),
({}, b"x" * 1_048_577, "dictionary exceeds"),
],
ids=["declared-size", "streamed-size"],
)
def test_oversized_http_dictionary_is_rejected_without_cache_mutation(
generator: types.ModuleType,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
headers: dict[str, str],
content: bytes,
message: str,
) -> None:
"""HTTP sources are bounded with and without Content-Length."""
repository, _ = prepare_repository(tmp_path)
response = FakeHttpResponse(content, headers)
monkeypatch.setattr(
generator.rollout,
"_open_https",
lambda *args, **kwargs: response,
)

with pytest.raises(generator.rollout.DictionaryTooLargeError, match=message):
generator.main(repository=repository, source="https://example.invalid/base")

assert not (repository / ".typos-oxendict-base.toml").exists()
assert not (repository / ".typos-oxendict-base.json").exists()


def test_http_failure_reports_bounded_stale_cache_diagnostic(
generator: types.ModuleType,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A degraded HTTP refresh identifies status and cache age without its URL."""
repository, source = prepare_repository(tmp_path)
generator.main(repository=repository, source=source)
monkeypatch.setattr(
generator.rollout,
"_open_https",
lambda *args, **kwargs: (_ for _ in ()).throw(
urllib.error.HTTPError(
"https://secret.invalid/dictionary",
503,
"unavailable",
email.message.Message(),
None,
)
),
)

with caplog.at_level("WARNING", logger=generator.rollout.__name__):
result = generator.main(
repository=repository,
source="https://example.invalid/base",
)

assert result.status == "stale-cache"
record = caplog.records[-1]
assert record.levelname == "WARNING"
assert "operation=https-refresh" in record.message
assert "category=http" in record.message
assert "status=503" in record.message
assert "cache_age_seconds=" in record.message
assert "secret.invalid" not in record.message
assert "example.invalid" not in record.message


def test_changed_http_source_discards_matching_validators(
generator: types.ModuleType,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A new HTTP authority is fetched even when its validators match."""
repository, _ = prepare_repository(tmp_path)
headers = {
"ETag": '"shared-validator"',
"Last-Modified": "Wed, 21 Oct 2015 07:28:00 GMT",
}
responses = iter(
[
FakeHttpResponse(dictionary_text(stem="first").encode(), headers),
FakeHttpResponse(dictionary_text(stem="second").encode(), headers),
]
)
requests: list[urllib.request.Request] = []

def open_https(request: urllib.request.Request) -> FakeHttpResponse:
requests.append(request)
return next(responses)

monkeypatch.setattr(generator.rollout, "_open_https", open_https)

first_url = "https://first.example.invalid/dictionary.toml"
second_url = "https://second.example.invalid/dictionary.toml"
generator.main(repository=repository, source=first_url)
result = generator.main(repository=repository, source=second_url)

second_request = requests[1]
assert second_request.full_url == second_url
assert second_request.get_header("If-none-match") is None
assert second_request.get_header("If-modified-since") is None
assert result.status == "refreshed"
assert '"secondise" = "secondize"' in generator.render_config(repository)


def test_http_redirect_cannot_downgrade_to_plain_http(
generator: types.ModuleType,
) -> None:
"""Every redirect target must retain HTTPS transport."""
handler = generator.rollout._HttpsRedirectHandler()
request = generator.rollout._https_request(
"https://example.invalid/dictionary.toml",
{},
)

with pytest.raises(ValueError, match="must use HTTPS"):
handler.redirect_request(
request,
io.BytesIO(),
302,
"Found",
email.message.Message(),
"http://example.invalid/dictionary.toml",
)


def test_metadata_failure_cleans_up_and_retries_changed_source(
generator: types.ModuleType,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An interrupted metadata write leaves valid cache state for a clean retry."""
repository, source = prepare_repository(tmp_path)
cache = repository / ".typos-oxendict-base.toml"
metadata = repository / ".typos-oxendict-base.json"
original_write_metadata = generator.rollout._write_metadata

def fail_metadata(*args: object, **kwargs: object) -> None:
raise OSError("simulated metadata failure")

monkeypatch.setattr(generator.rollout, "_write_metadata", fail_metadata)
with pytest.raises(OSError, match="simulated metadata failure"):
generator.main(repository=repository, source=source)

assert generator.rollout.load_dictionary(cache)
assert not metadata.exists()
temporary_files = [
path
for path in repository.glob(f".{cache.name}.*")
if path.name != f"{cache.name}.lock"
]
assert temporary_files == []

monkeypatch.setattr(generator.rollout, "_write_metadata", original_write_metadata)
source.write_text(dictionary_text(stem="retried"), encoding="utf-8")
result = generator.main(repository=repository, source=source)

assert result.status == "refreshed"
assert metadata.exists()
assert '"retriedise" = "retriedize"' in generator.render_config(repository)


def test_cache_lock_serializes_generator_processes(
generator: types.ModuleType,
tmp_path: pathlib.Path,
) -> None:
"""A second process waits for the cache owner and then refreshes cleanly."""
repository, source = prepare_repository(tmp_path)
cache = repository / ".typos-oxendict-base.toml"
lock_path = cache.with_name(f"{cache.name}.lock")
lock_path.touch()
child_code = (
"import pathlib, sys; "
"sys.path.insert(0, sys.argv[1]); "
"import generate_typos_config as generator; "
"generator.main(repository=pathlib.Path(sys.argv[2]), "
"source=pathlib.Path(sys.argv[3]))"
)

with lock_path.open("a+b") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
process = subprocess.Popen( # noqa: S603 - argv contains trusted test paths.
[
sys.executable,
"-c",
child_code,
str(SCRIPTS_ROOT),
str(repository),
str(source),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
with pytest.raises(subprocess.TimeoutExpired):
process.wait(timeout=0.2)
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)

stdout, stderr = process.communicate(timeout=10)
assert process.returncode == 0, f"stdout={stdout}\nstderr={stderr}"
assert generator.rollout.load_dictionary(cache)
assert (repository / ".typos-oxendict-base.json").exists()


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add assertion messages throughout the new tests.

Several new assertions are bare, for example lines 197-198, 229-230, 350-351, 357, 363-365, and 406-407. The rest of this file, and most of the newer stale-cache and validator tests (lines 233-309), consistently carry descriptive messages. Bring the remaining bare asserts in line for consistent failure diagnostics.

As per path instructions, "Use assert …, "message" over bare asserts."

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 327-327: Do not make http calls without encryption
Context: "http://example.invalid/dictionary.toml"
Note: [CWE-319] Cleartext Transmission of Sensitive Information.

(requests-http)


[error] 386-398: Command coming from incoming request
Context: subprocess.Popen( # noqa: S603 - argv contains trusted test paths.
[
sys.executable,
"-c",
child_code,
str(SCRIPTS_ROOT),
str(repository),
str(source),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.16.0)

[warning] 223-223: Unused lambda argument: args

(ARG005)


[warning] 223-223: Unused lambda argument: kwargs

(ARG005)


[warning] 245-245: Unused lambda argument: args

(ARG005)


[warning] 245-245: Unused lambda argument: kwargs

(ARG005)


[warning] 343-343: Unused function argument: args

(ARG001)


[warning] 343-343: Unused function argument: kwargs

(ARG001)


[warning] 344-344: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_typos_rollout.py` around lines 183 - 409, Update the new tests in
the shown test functions to add descriptive failure messages to every bare
assert, including cache-file checks, response/request validation, metadata
cleanup, retry results, and subprocess/cache assertions. Preserve each
assertion’s existing condition and follow the file’s established `assert
expression, "message"` style.

Source: Path instructions

Comment on lines +368 to +409
def test_cache_lock_serializes_generator_processes(
generator: types.ModuleType,
tmp_path: pathlib.Path,
) -> None:
"""A second process waits for the cache owner and then refreshes cleanly."""
repository, source = prepare_repository(tmp_path)
cache = repository / ".typos-oxendict-base.toml"
lock_path = cache.with_name(f"{cache.name}.lock")
lock_path.touch()
child_code = (
"import pathlib, sys; "
"sys.path.insert(0, sys.argv[1]); "
"import generate_typos_config as generator; "
"generator.main(repository=pathlib.Path(sys.argv[2]), "
"source=pathlib.Path(sys.argv[3]))"
)

with lock_path.open("a+b") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
process = subprocess.Popen( # noqa: S603 - argv contains trusted test paths.
[
sys.executable,
"-c",
child_code,
str(SCRIPTS_ROOT),
str(repository),
str(source),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
with pytest.raises(subprocess.TimeoutExpired):
process.wait(timeout=0.2)
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)

stdout, stderr = process.communicate(timeout=10)
assert process.returncode == 0, f"stdout={stdout}\nstderr={stderr}"
assert generator.rollout.load_dictionary(cache)
assert (repository / ".typos-oxendict-base.json").exists()


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Strengthen the lock-contention test against false confidence.

process.wait(timeout=0.2) proves the child has not finished while the parent holds the lock, but it does not prove the child was actually blocked on the lock. If the wrapped refresh operation happens to take longer than 0.2 s on its own (Python startup, module import, TOML parsing), the test passes even with a broken or missing lock. Have the child process emit a readiness marker (for example, a line on stdout) right before it attempts to acquire the lock, and assert that marker was observed before releasing the parent's lock. This turns the timing assumption into a deterministic synchronization point.

🧰 Tools
🪛 ast-grep (0.45.0)

[error] 386-398: Command coming from incoming request
Context: subprocess.Popen( # noqa: S603 - argv contains trusted test paths.
[
sys.executable,
"-c",
child_code,
str(SCRIPTS_ROOT),
str(repository),
str(source),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_typos_rollout.py` around lines 368 - 409, Strengthen
test_cache_lock_serializes_generator_processes by having the child emit a
readiness marker immediately before generator.main attempts lock acquisition,
then wait for and assert that marker before releasing the parent-held lock.
Retain the existing post-release success assertions, but remove reliance on
process.wait(timeout=0.2) as proof of lock contention.

leynos and others added 4 commits August 5, 2026 12:16
Generated repositories now enforce en-GB Oxford ("-ize") spelling in
Markdown prose with typos, using the two-layer mechanism stilyagi
introduced in ee77d08a19bc9c1107f30fc2b5bb63ced979fbc4: a generated
typos.toml restores Oxford -ize forms over the en-gb locale, and
scripts/generate_typos_config.py is the single source of truth for its
stems, accepted words, and ignore patterns.

The gate is a new boolean copier option, en_gb_oxendict, defaulting to
true. When enabled the template renders the generator script and the
committed typos.toml, pins the typos version through a Makefile
TYPOS_VERSION variable, and adds a spellcheck target that markdownlint
depends on. Because the generated repository has no Python test
runner, the generator carries the netsuke parse tests' intent in a
--check mode: it tomllib-parses the rendered document (failing fast on
duplicate extend-words keys), asserts the exact entry count, and diffs
the committed file against the generator output. CI runs the gate as a
dedicated step after uv is installed, which required converting ci.yml
to a raw-wrapped Jinja template following the release workflow's
escaping convention. Disabled renders carry no trace of the gate.

Running the gate over rendered output surfaced five genuine spelling
issues in the template documentation (Flavored, signaling, afterward,
parameterisation, and an American citation title), which are corrected
or, for the external citation title, ignored by pattern.

Parent contract tests cover both toggle states, and the rendered
Makefile and CI snapshots are updated accordingly.
Trim Copier control-block whitespace without stripping Make recipe tabs.
Keep default and disabled renders syntactically valid while avoiding blank
lines that make generated snapshots fail Git whitespace checks.
Exercise the Copier default and prove that spelling and Polonius options
remain independent across generated project states.

Bound dictionary inputs, serialize cache refreshes across processes, and
report actionable stale-cache diagnostics. Document the optional generated
gate and its distinction from the parent repository spelling target.
Reject transport-downgrading redirects and scope HTTP validators to their
recorded source so an old authority cannot be reported as current.

Document the generator's public contracts and prove regeneration, source
changes, and redirect handling in the parent template tests.
@lodyai
lodyai Bot force-pushed the add-en-gb-oxendict-gate branch from 0342b3e to b70c296 Compare August 5, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants