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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ __pycache__/
# Untracked cache of the estate-wide en-GB-oxendict dictionary.
.typos-oxendict-base.json
.typos-oxendict-base.toml
.typos-oxendict-base.toml.lock
.grepai/
5 changes: 5 additions & 0 deletions copier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,8 @@ dev_target:
default: 'x86_64-unknown-linux-gnu'
help: 'Optional Linux target triple for mold linker config'
placeholder: 'e.g. x86_64-unknown-linux-gnu'

en_gb_oxendict:
type: bool
default: true
help: 'Enforce en-GB Oxford ("-ize") spelling in documentation via a typos gate?'
19 changes: 13 additions & 6 deletions docs/adr-003-shared-oxford-spelling-base.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ Generated projects refresh the tracked shared dictionary published by
generator merges that base with tracked `typos.local.toml` and writes a
deterministic, tracked `typos.toml`.

The refresh validates data before atomic replacement, keeps a valid cache when
its authority is not newer, and supports explicit offline reuse. The generated
Makefile pins `typos`, exposes `make spelling`, includes spelling in
`make all`, and runs it through Continuous Integration (CI). The template
repository uses the same mechanism for its own Markdown and rendered Markdown
sources.
The refresh bounds dictionary inputs, validates data before atomic replacement,
serializes cache and metadata writers with a cross-process lock, keeps a valid
cache when its authority is not newer, reports bounded stale-cache diagnostics,
and supports explicit offline reuse.

The template repository always uses this mechanism through `make spelling`.
Generated repositories include it when the `en_gb_oxendict` Copier option is
enabled (the default): their Makefile pins `typos`, exposes `make spellcheck`,
includes that gate in `make all` and `make markdownlint`, and runs it through
Continuous Integration (CI). Disabled renders omit the files, wiring, and
documentation entirely.

## Consequences

Expand All @@ -36,3 +41,5 @@ sources.
local exceptions.
- A fresh checkout needs network access to collect the shared base before its
first spelling run.
- Concurrent generator processes serialize refreshes; a crashed writer may
leave an empty lock file, which is harmless and remains ignored.
91 changes: 81 additions & 10 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,94 @@ The parent gates run Ruff and mypy over the `tests/` tree; install `uv` so
- `make typecheck` runs `mypy` with the pytest dependency stubs and
`types-PyYAML`.

## Shared Oxford Spelling Gate

Both the template repository and rendered projects enforce en-GB-oxendict
Markdown spelling with `typos` 1.48.0. The tracked `typos.toml` is generated;
never edit its entries manually. Put a verified product name, upstream term, or
repository-specific correction in `typos.local.toml`, then run:
## Shared Oxford Spelling Policy

The template repository always enforces en-GB-oxendict Markdown spelling with
`make spelling`. Rendered projects enforce the same policy only when the
`en_gb_oxendict` Copier option is enabled; it defaults to enabled and can be
disabled without leaving spelling files, documentation, or gate wiring. An
enabled generated project exposes the equivalent gate as `make spellcheck`.

The tracked `typos.toml` is generated; never edit its entries manually. Put a
verified product name, upstream term, or repository-specific correction in
`typos.local.toml`, then run:

```sh
uv run scripts/generate_typos_config.py
```

The generator collects the shared dictionary from `leynos/agent-helper-scripts`
into the ignored local cache only when the remote source is newer. It bounds
dictionary inputs to 1 MiB, validates them before atomic replacement, and uses
a cross-process lock to serialize cache and metadata writers. A failed remote
refresh reuses a valid stale cache with a bounded diagnostic containing the
failure category or HTTP status and cache age. Explicit offline reuse is
available once the cache is populated. Parent `make spelling` and generated
`make spellcheck` regenerate their tracked configuration before checking
maintained Markdown.

Generated audit coverage is tested without network access by replacing Cargo
with a fake executable. The regression verifies that `make rust-audit` derives
the workspace root from `cargo metadata`, ignores manifests outside workspace
metadata, and invokes `cargo audit` once from the workspace root.

Generated CI skips the `cargo-audit` install, Python setup, and `make audit`
steps when `github.actor` is `dependabot[bot]`. That keeps whole-lockfile
advisories from blocking unrelated Dependabot PRs while leaving the audit gate
in place for human PRs. The compensating control is
`template/.github/workflows/audit.yml`, which runs weekly and can also be
triggered manually.

Optional GitHub Actions validation runs rendered workflows through `act`. It is
disabled by default and only runs when `WITH_ACT=1` is present:

```sh
make test WITH_ACT=1
```

The parent Makefile maps `WITH_ACT=1` to `RUN_ACT_VALIDATION=1` for pytest.
Those checks require `act` and either Docker or Podman. They prepare rendered
projects as temporary Git repositories, run the generated act-validation
workflow, and assert black-box evidence for Rust test execution.

Parent CI keeps Act validation in `.github/workflows/act-validation.yml`. The
main `.github/workflows/ci.yml` workflow runs ordinary `make test` without
`WITH_ACT=1` so the slower container-backed checks run in parallel instead of
blocking the main test and coverage path.

The template also renders `.github/workflows/mutation-testing.yml` into
generated projects; its rendered contract is asserted by
`_assert_mutation_workflow_contracts` in
`tests/helpers/tooling_contracts/mutation.py`. Dependabot owns the pinned
reusable-workflow commit SHA, so the contract test asserts the pin format (a
full commit SHA) rather than a hard-coded value.

## Shared Oxford Spelling Policy

The template repository always enforces en-GB-oxendict Markdown spelling with
`make spelling`. Rendered projects enforce the same policy only when the
`en_gb_oxendict` Copier option is enabled; it defaults to enabled and can be
disabled without leaving spelling files, documentation, or gate wiring. An
enabled generated project exposes the equivalent gate as `make spellcheck`.

The tracked `typos.toml` is generated; never edit its entries manually. Put a
verified product name, upstream term, or repository-specific correction in
`typos.local.toml`, then run:

```sh
uv run scripts/generate_typos_config.py
```

The generator collects the shared dictionary from `leynos/agent-helper-scripts`
into the ignored local cache only when the remote source is newer. It validates
and atomically replaces that cache, retains a newer local copy, and supports
offline reuse once populated. `make spelling` regenerates the tracked
configuration before checking maintained Markdown and rendered Markdown
templates.
into the ignored local cache only when the remote source is newer. It bounds
dictionary inputs to 1 MiB, validates them before atomic replacement, and uses
a cross-process lock to serialize cache and metadata writers. A failed remote
refresh reuses a valid stale cache with a bounded diagnostic containing the
failure category or HTTP status and cache age. Explicit offline reuse is
available once the cache is populated. Parent `make spelling` and generated
`make spellcheck` regenerate their tracked configuration before checking
maintained Markdown.

Generated audit coverage is tested without network access by replacing Cargo
with a fake executable. The regression verifies that `make rust-audit` derives
Expand Down
14 changes: 9 additions & 5 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ metadata used in the generated `Cargo.toml`:
the pull-request workflow leaves the changed-line `mode: check` gate deferred
in a documented comment. Fill it in (and set the `CS_ACCESS_TOKEN` secret)
once the repository is onboarded to CodeScene.
- `en_gb_oxendict` (default `true`) adds an en-GB-oxendict spelling gate to
generated repositories. Set it to `false` to omit the configuration,
generator, Makefile/CI wiring, and generated documentation.

## Generated Tooling

Expand Down Expand Up @@ -148,7 +151,8 @@ doctest therefore fails the command.

The generated `Makefile` exposes these public targets:

- `make all` runs formatting checks, linting, tests, and spelling checks.
- `make all` runs formatting checks, linting, and tests, plus spelling checks
when `en_gb_oxendict` is enabled.
- `make check-fmt` verifies Rust formatting.
- `make fmt` formats Rust and Markdown sources.
- `make lint` builds documentation, then runs Clippy and Whitaker, with every
Expand All @@ -166,11 +170,11 @@ The generated `Makefile` exposes these public targets:
dependency bumps; human pull requests still run it. The separate
`.github/workflows/audit.yml` workflow runs weekly and can also be triggered
manually to keep the lockfile covered.
- `make markdownlint` checks Markdown files and enforces en-GB-oxendict
spelling through the pinned `typos` release.
- `make spelling` refreshes the shared Oxford dictionary when its published
- `make markdownlint` checks Markdown files and, when `en_gb_oxendict` is
enabled, depends on `make spellcheck`.
- `make spellcheck` refreshes the shared Oxford dictionary when its published
source is newer than the ignored local cache, generates `typos.toml`, and
checks Markdown prose.
checks Markdown prose through the pinned `typos` release.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- `make nixie` validates Mermaid diagrams.

Install `clang`, `lld`, `mold`, `python3`, and `cargo-audit` before running the
Expand Down
86 changes: 83 additions & 3 deletions scripts/generate_typos_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,30 @@


def dictionary_from_cache(repository: Path = REPOSITORY_ROOT) -> rollout.Dictionary:
"""Load the cached shared base merged with local repository policy."""
"""Load cached shared policy merged with the repository overlay.

Parameters
----------
repository : Path
Repository containing the refreshed cache and optional
``typos.local.toml`` overlay.

Returns
-------
rollout.Dictionary
Validated shared policy, with local additions merged when present.

Raises
------
OSError
A dictionary file cannot be read.
TypeError
A dictionary value has the wrong TOML shape.
ValueError
Dictionary policy is invalid or the overlay conflicts with the base.
tomllib.TOMLDecodeError
A dictionary file is not valid TOML.
"""
dictionary = rollout.load_dictionary(repository / ".typos-oxendict-base.toml")
local_overlay = repository / "typos.local.toml"
if local_overlay.exists():
Expand All @@ -35,7 +58,29 @@ def dictionary_from_cache(repository: Path = REPOSITORY_ROOT) -> rollout.Diction


def render_config(repository: Path = REPOSITORY_ROOT) -> str:
"""Render deterministic configuration from the populated local cache."""
"""Render deterministic configuration from the populated local cache.

Parameters
----------
repository : Path
Repository containing the refreshed cache and optional local overlay.

Returns
-------
str
Validated ``typos.toml`` content generated from the merged policy.

Raises
------
OSError
A dictionary file cannot be read.
TypeError
A dictionary value has the wrong TOML shape.
ValueError
Dictionary policy or generated word mappings conflict.
tomllib.TOMLDecodeError
Input or rendered output is not valid TOML.
"""
return rollout.render_typos_config(dictionary_from_cache(repository))


Expand All @@ -46,7 +91,42 @@ def main(
source: str | Path = DEFAULT_BASE_URL,
offline: bool = False,
) -> rollout.RefreshResult:
"""Refresh the shared base cache and write the merged configuration."""
"""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.
"""
Comment on lines +94 to +129

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

result = rollout.refresh_base(
source,
repository / ".typos-oxendict-base.toml",
Expand Down
Loading
Loading