From 99aa41e254930af7dfe7658e172ca3c1a710f3ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 23:57:58 +0000 Subject: [PATCH 1/6] Initial plan From 4e1012ec6412510265c1a22df2f32a14d3f785c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 00:01:27 +0000 Subject: [PATCH 2/6] feat: expose authority URL manifest and docs bulk fetch command Agent-Logs-Url: https://github.com/bitflight-devops/skilllint/sessions/fd38ea46-062c-499d-8243-f33cffb96666 Co-authored-by: Jamie-BitFlight <25075504+Jamie-BitFlight@users.noreply.github.com> --- CLAUDE.md | 3 + README.md | 21 +++++++ packages/skilllint/cli_docs.py | 59 ++++++++++++++++++- packages/skilllint/rule_registry.py | 32 ++++++++-- packages/skilllint/tests/test_cli_docs.py | 56 ++++++++++++++++++ .../skilllint/tests/test_rule_registry.py | 56 ++++++++++++++++++ 6 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 packages/skilllint/tests/test_rule_registry.py diff --git a/CLAUDE.md b/CLAUDE.md index 899d866..990c55f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,9 @@ When agents need external documentation (Claude hooks docs, Cursor rules docs, e # Cache a page (prints the file path to stdout) skilllint docs fetch URL +# Cache all unique rule authority reference URLs +skilllint docs fetch-authorities + # List sections in a cached file skilllint docs sections FILE diff --git a/README.md b/README.md index 07b0a30..4a91c12 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,7 @@ Usage: skilllint docs [OPTIONS] COMMAND [ARGS]... Commands: fetch Fetch a documentation page or return a cached copy. + fetch-authorities Fetch documentation for all rule authority URLs. latest Find the most recent cached file for a page name. sections Print a table of sections in a cached markdown file. section Extract the text of a named section from a cached markdown file. @@ -311,6 +312,23 @@ Options: Prints the cached file path to stdout. Status messages go to stderr. Exits 1 when no cache exists and the network is unavailable. +#### docs fetch-authorities + +``` +Usage: skilllint docs fetch-authorities [OPTIONS] + +Cache Options: + --ttl FLOAT Cache time-to-live in hours before a refresh is attempted. [default: 4.0] + --force Skip the freshness check and always attempt a network fetch. + +Options: + --help Show this message and exit. +``` + +Fetches every unique authority reference URL declared by the rule registry. Prints one +cached file path per successful fetch. Exits 1 if any URL cannot be fetched and no stale +cache is available. + #### docs latest ``` @@ -402,6 +420,9 @@ sidecar that records the SHA-256 digest, byte count, source URL, and fetch times # Cache a documentation page (default TTL: 4 hours) skilllint docs fetch https://docs.anthropic.com/en/docs/claude-code/settings.md +# Pre-fetch all rule authority reference URLs +skilllint docs fetch-authorities + # Force a network refresh regardless of TTL skilllint docs fetch https://docs.anthropic.com/en/docs/claude-code/settings.md --force diff --git a/packages/skilllint/cli_docs.py b/packages/skilllint/cli_docs.py index 5afa7ea..15ef799 100644 --- a/packages/skilllint/cli_docs.py +++ b/packages/skilllint/cli_docs.py @@ -7,13 +7,14 @@ from __future__ import annotations -from pathlib import Path # noqa: TC003 +from pathlib import Path from typing import Annotated import typer from rich.console import Console from rich.panel import Panel +from skilllint.rule_registry import iter_authority_urls from skilllint.vendor_cache import ( CacheStatus, IntegrityStatus, @@ -96,6 +97,62 @@ def fetch( console.print(result.path) +# --------------------------------------------------------------------------- +# fetch-authorities +# --------------------------------------------------------------------------- + + +@docs_app.command("fetch-authorities") +def fetch_authorities( + ttl: Annotated[ + float, + typer.Option( + "--ttl", help="Cache time-to-live in hours before a refresh is attempted.", rich_help_panel="Cache Options" + ), + ] = 4.0, + force: Annotated[ + bool, + typer.Option( + "--force", + help="Skip the freshness check and always attempt a network fetch.", + rich_help_panel="Cache Options", + ), + ] = False, +) -> None: + """Fetch cached documentation for all rule authority reference URLs. + + Prints one cached file path per successfully fetched authority URL. + + Raises: + typer.Exit: Exit code 1 when one or more authority URLs cannot be fetched + and no stale cache can be served. + """ + authority_urls = list(iter_authority_urls(unique=True)) + if not authority_urls: + err_console.print(":warning: [yellow]No authority URLs found in the rule registry[/yellow]") + return + + had_failure = False + for url in authority_urls: + try: + result = fetch_or_cached(url, ttl_hours=ttl, force=force) + except NoCacheError as exc: + had_failure = True + err_console.print(f":cross_mark: [red]FAILED[/red] {exc.url} ({exc.reason})") + continue + + if result.status is CacheStatus.STALE: + err_console.print(f":warning: [yellow]STALE[/yellow] {url} — serving stale cache") + else: + status_label = result.status.value.upper() + err_console.print(f":white_check_mark: [green]{status_label}[/green] {url}") + + console.print(result.path) + + if had_failure: + raise typer.Exit(code=1) + + # --------------------------------------------------------------------------- # latest # --------------------------------------------------------------------------- diff --git a/packages/skilllint/rule_registry.py b/packages/skilllint/rule_registry.py index e2e8496..c9b0436 100644 --- a/packages/skilllint/rule_registry.py +++ b/packages/skilllint/rule_registry.py @@ -23,13 +23,11 @@ def check_name_field(frontmatter: dict, path: Path) -> list[ValidationIssue]: from __future__ import annotations -from typing import TYPE_CHECKING, Annotated, Any, Literal +from collections.abc import Callable, Iterator +from typing import Annotated, Any, Literal from pydantic import BaseModel, ConfigDict, Field -if TYPE_CHECKING: - from collections.abc import Callable - class RuleAuthority(BaseModel): """Structured authority metadata for a validation rule. @@ -159,4 +157,28 @@ def list_rules( return sorted(rules, key=lambda r: r.id) -__all__ = ["RULE_REGISTRY", "RuleAuthority", "RuleEntry", "get_rule", "list_rules", "skilllint_rule"] +def iter_authority_urls(*, unique: bool = True) -> Iterator[str]: + """Iterate authority reference URLs declared by registered rules. + + Args: + unique: When True, yield each reference URL at most once while preserving + first-seen order (by sorted rule ID). When False, include duplicates. + + Yields: + Authority reference URL strings from RuleEntry.authority.reference. + """ + seen: set[str] = set() + for rule in list_rules(): + reference = rule.authority.reference if rule.authority is not None else None + if not reference: + continue + + if unique: + if reference in seen: + continue + seen.add(reference) + + yield reference + + +__all__ = ["RULE_REGISTRY", "RuleAuthority", "RuleEntry", "get_rule", "iter_authority_urls", "list_rules", "skilllint_rule"] diff --git a/packages/skilllint/tests/test_cli_docs.py b/packages/skilllint/tests/test_cli_docs.py index e774389..186e927 100644 --- a/packages/skilllint/tests/test_cli_docs.py +++ b/packages/skilllint/tests/test_cli_docs.py @@ -31,8 +31,10 @@ # --------------------------------------------------------------------------- _TEST_URL = "https://docs.example.com/en/docs/settings.md" +_TEST_URL_2 = "https://docs.example.com/en/docs/hooks.md" _TEST_PAGE = "settings" _TEST_PATH = Path("/tmp/settings-2024-01-01-0000.md") +_TEST_PATH_2 = Path("/tmp/hooks-2024-01-01-0000.md") def _cache_result(status: CacheStatus, path: Path = _TEST_PATH) -> CacheResult: @@ -244,6 +246,60 @@ def test_default_force_is_false(self, cli_runner: CliRunner, mocker: MockerFixtu assert kwargs.get("force") is False +# --------------------------------------------------------------------------- +# docs fetch-authorities +# --------------------------------------------------------------------------- + + +class TestDocsFetchAuthorities: + """Tests for the ``docs fetch-authorities`` subcommand.""" + + def test_fetches_all_authority_urls(self, cli_runner: CliRunner, mocker: MockerFixture) -> None: + """Fetches each authority URL and prints one path per success.""" + mock_iter = mocker.patch("skilllint.cli_docs.iter_authority_urls") + mock_iter.return_value = iter([_TEST_URL, _TEST_URL_2]) + + mock_fetch = mocker.patch("skilllint.cli_docs.fetch_or_cached") + mock_fetch.side_effect = [ + _cache_result(CacheStatus.FRESH, path=_TEST_PATH), + _cache_result(CacheStatus.NEW, path=_TEST_PATH_2), + ] + + result = cli_runner.invoke(plugin_validator.app, ["docs", "fetch-authorities"]) + + assert result.exit_code == 0 + mock_iter.assert_called_once_with(unique=True) + assert mock_fetch.call_count == 2 + assert str(_TEST_PATH) in result.output + assert str(_TEST_PATH_2) in result.output + + def test_forwards_ttl_and_force_options(self, cli_runner: CliRunner, mocker: MockerFixture) -> None: + """Forwards --ttl/--force values to fetch_or_cached for each URL.""" + mocker.patch("skilllint.cli_docs.iter_authority_urls", return_value=iter([_TEST_URL])) + mock_fetch = mocker.patch("skilllint.cli_docs.fetch_or_cached") + mock_fetch.return_value = _cache_result(CacheStatus.FRESH) + + result = cli_runner.invoke(plugin_validator.app, ["docs", "fetch-authorities", "--ttl", "12", "--force"]) + + assert result.exit_code == 0 + mock_fetch.assert_called_once_with(_TEST_URL, ttl_hours=pytest.approx(12.0), force=True) + + def test_exits_one_when_any_authority_fetch_fails(self, cli_runner: CliRunner, mocker: MockerFixture) -> None: + """Continues remaining URLs but exits 1 if any URL has no available cache.""" + mocker.patch("skilllint.cli_docs.iter_authority_urls", return_value=iter([_TEST_URL, _TEST_URL_2])) + mock_fetch = mocker.patch("skilllint.cli_docs.fetch_or_cached") + mock_fetch.side_effect = [ + NoCacheError(url=_TEST_URL, reason="network down"), + _cache_result(CacheStatus.STALE, path=_TEST_PATH_2), + ] + + result = cli_runner.invoke(plugin_validator.app, ["docs", "fetch-authorities"]) + + assert result.exit_code == 1 + assert mock_fetch.call_count == 2 + assert str(_TEST_PATH_2) in result.output + + # --------------------------------------------------------------------------- # docs latest # --------------------------------------------------------------------------- diff --git a/packages/skilllint/tests/test_rule_registry.py b/packages/skilllint/tests/test_rule_registry.py new file mode 100644 index 0000000..2093bdd --- /dev/null +++ b/packages/skilllint/tests/test_rule_registry.py @@ -0,0 +1,56 @@ +"""Tests for rule registry helper iteration APIs.""" + +from __future__ import annotations + +from skilllint.rule_registry import RULE_REGISTRY, RuleAuthority, RuleEntry, iter_authority_urls + + +def _entry(rule_id: str, reference: str | None) -> RuleEntry: + authority = None if reference is None else RuleAuthority(origin="example.test", reference=reference) + return RuleEntry( + id=rule_id, + fn=lambda: None, + severity="info", + category="test", + platforms=["agentskills"], + docstring=f"Rule {rule_id}", + authority=authority, + ) + + +def test_iter_authority_urls_unique_filters_empty_and_dedupes() -> None: + """Default unique iteration dedupes while preserving first-seen order.""" + RULE_REGISTRY.clear() + RULE_REGISTRY.update( + { + "AA001": _entry("AA001", "https://example.test/spec#a"), + "AA002": _entry("AA002", "https://example.test/spec#a"), + "AA003": _entry("AA003", None), + "AA004": _entry("AA004", ""), + "AA005": _entry("AA005", "https://example.test/spec#b"), + } + ) + + urls = list(iter_authority_urls()) + + assert urls == ["https://example.test/spec#a", "https://example.test/spec#b"] + + +def test_iter_authority_urls_non_unique_keeps_duplicates() -> None: + """unique=False includes duplicated references from multiple rules.""" + RULE_REGISTRY.clear() + RULE_REGISTRY.update( + { + "AA001": _entry("AA001", "https://example.test/spec#a"), + "AA002": _entry("AA002", "https://example.test/spec#a"), + "AA003": _entry("AA003", "https://example.test/spec#b"), + } + ) + + urls = list(iter_authority_urls(unique=False)) + + assert urls == [ + "https://example.test/spec#a", + "https://example.test/spec#a", + "https://example.test/spec#b", + ] From 6a685845640ba10ebd6c07f2010101535077c037 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 00:02:32 +0000 Subject: [PATCH 3/6] chore: address review feedback on rule registry typing import Agent-Logs-Url: https://github.com/bitflight-devops/skilllint/sessions/fd38ea46-062c-499d-8243-f33cffb96666 Co-authored-by: Jamie-BitFlight <25075504+Jamie-BitFlight@users.noreply.github.com> --- packages/skilllint/rule_registry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/skilllint/rule_registry.py b/packages/skilllint/rule_registry.py index c9b0436..0799b5c 100644 --- a/packages/skilllint/rule_registry.py +++ b/packages/skilllint/rule_registry.py @@ -23,11 +23,14 @@ def check_name_field(frontmatter: dict, path: Path) -> list[ValidationIssue]: from __future__ import annotations -from collections.abc import Callable, Iterator -from typing import Annotated, Any, Literal +from collections.abc import Iterator +from typing import TYPE_CHECKING, Annotated, Any, Literal from pydantic import BaseModel, ConfigDict, Field +if TYPE_CHECKING: + from collections.abc import Callable + class RuleAuthority(BaseModel): """Structured authority metadata for a validation rule. From 95918ba760482706d185dacccb2bf3d2217c0ff9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 11:41:29 +0000 Subject: [PATCH 4/6] chore: plan review/ci follow-up fixes Agent-Logs-Url: https://github.com/bitflight-devops/skilllint/sessions/2ad2d721-1e8d-4c3d-919d-65336297e775 Co-authored-by: Jamie-BitFlight <25075504+Jamie-BitFlight@users.noreply.github.com> --- scripts/fetch_doc_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetch_doc_source.py b/scripts/fetch_doc_source.py index 9b7384d..2e78619 100755 --- a/scripts/fetch_doc_source.py +++ b/scripts/fetch_doc_source.py @@ -14,7 +14,7 @@ from __future__ import annotations -from pathlib import Path # noqa: TC003 +from pathlib import Path from typing import Annotated import typer From 1498f1d5373621ebf19d0739b75815e99d86d175 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 11:42:40 +0000 Subject: [PATCH 5/6] fix: normalize authority URLs for docs fetch-authorities Agent-Logs-Url: https://github.com/bitflight-devops/skilllint/sessions/2ad2d721-1e8d-4c3d-919d-65336297e775 Co-authored-by: Jamie-BitFlight <25075504+Jamie-BitFlight@users.noreply.github.com> --- CLAUDE.md | 2 +- README.md | 11 ++++---- packages/skilllint/cli_docs.py | 2 +- packages/skilllint/rule_registry.py | 27 ++++++++++++++----- .../skilllint/tests/test_rule_registry.py | 24 +++++++++++++++-- 5 files changed, 50 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 990c55f..087e0fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ When agents need external documentation (Claude hooks docs, Cursor rules docs, e # Cache a page (prints the file path to stdout) skilllint docs fetch URL -# Cache all unique rule authority reference URLs +# Cache all unique normalized rule authority URLs skilllint docs fetch-authorities # List sections in a cached file diff --git a/README.md b/README.md index 4a91c12..e30f1ea 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ Usage: skilllint docs [OPTIONS] COMMAND [ARGS]... Commands: fetch Fetch a documentation page or return a cached copy. - fetch-authorities Fetch documentation for all rule authority URLs. + fetch-authorities Fetch documentation for all normalized rule authority URLs. latest Find the most recent cached file for a page name. sections Print a table of sections in a cached markdown file. section Extract the text of a named section from a cached markdown file. @@ -325,9 +325,10 @@ Options: --help Show this message and exit. ``` -Fetches every unique authority reference URL declared by the rule registry. Prints one -cached file path per successful fetch. Exits 1 if any URL cannot be fetched and no stale -cache is available. +Fetches every unique authority URL declared by the rule registry after normalizing +origin-relative references against each rule authority origin. Prints one cached file +path per successful fetch. Exits 1 if any URL cannot be fetched and no stale cache is +available. #### docs latest @@ -420,7 +421,7 @@ sidecar that records the SHA-256 digest, byte count, source URL, and fetch times # Cache a documentation page (default TTL: 4 hours) skilllint docs fetch https://docs.anthropic.com/en/docs/claude-code/settings.md -# Pre-fetch all rule authority reference URLs +# Pre-fetch all normalized rule authority URLs skilllint docs fetch-authorities # Force a network refresh regardless of TTL diff --git a/packages/skilllint/cli_docs.py b/packages/skilllint/cli_docs.py index 15ef799..aa0d5fa 100644 --- a/packages/skilllint/cli_docs.py +++ b/packages/skilllint/cli_docs.py @@ -119,7 +119,7 @@ def fetch_authorities( ), ] = False, ) -> None: - """Fetch cached documentation for all rule authority reference URLs. + """Fetch cached documentation for all normalized rule authority URLs. Prints one cached file path per successfully fetched authority URL. diff --git a/packages/skilllint/rule_registry.py b/packages/skilllint/rule_registry.py index 0799b5c..498c607 100644 --- a/packages/skilllint/rule_registry.py +++ b/packages/skilllint/rule_registry.py @@ -25,6 +25,7 @@ def check_name_field(frontmatter: dict, path: Path) -> list[ValidationIssue]: from collections.abc import Iterator from typing import TYPE_CHECKING, Annotated, Any, Literal +from urllib.parse import urljoin from pydantic import BaseModel, ConfigDict, Field @@ -161,27 +162,39 @@ def list_rules( def iter_authority_urls(*, unique: bool = True) -> Iterator[str]: - """Iterate authority reference URLs declared by registered rules. + """Iterate normalized authority documentation URLs from registered rules. Args: - unique: When True, yield each reference URL at most once while preserving + unique: When True, yield each normalized URL at most once while preserving first-seen order (by sorted rule ID). When False, include duplicates. Yields: - Authority reference URL strings from RuleEntry.authority.reference. + Absolute authority documentation URLs. """ seen: set[str] = set() for rule in list_rules(): - reference = rule.authority.reference if rule.authority is not None else None + if rule.authority is None: + continue + + reference = rule.authority.reference if not reference: continue + normalized = reference + if not normalized.startswith(("https://", "http://")): + origin = rule.authority.origin.strip() + if not origin: + continue + if "://" not in origin: + origin = f"https://{origin}" + normalized = urljoin(f"{origin.rstrip('/')}/", normalized) + if unique: - if reference in seen: + if normalized in seen: continue - seen.add(reference) + seen.add(normalized) - yield reference + yield normalized __all__ = ["RULE_REGISTRY", "RuleAuthority", "RuleEntry", "get_rule", "iter_authority_urls", "list_rules", "skilllint_rule"] diff --git a/packages/skilllint/tests/test_rule_registry.py b/packages/skilllint/tests/test_rule_registry.py index 2093bdd..99e169c 100644 --- a/packages/skilllint/tests/test_rule_registry.py +++ b/packages/skilllint/tests/test_rule_registry.py @@ -5,8 +5,8 @@ from skilllint.rule_registry import RULE_REGISTRY, RuleAuthority, RuleEntry, iter_authority_urls -def _entry(rule_id: str, reference: str | None) -> RuleEntry: - authority = None if reference is None else RuleAuthority(origin="example.test", reference=reference) +def _entry(rule_id: str, reference: str | None, origin: str = "example.test") -> RuleEntry: + authority = None if reference is None else RuleAuthority(origin=origin, reference=reference) return RuleEntry( id=rule_id, fn=lambda: None, @@ -54,3 +54,23 @@ def test_iter_authority_urls_non_unique_keeps_duplicates() -> None: "https://example.test/spec#a", "https://example.test/spec#b", ] + + +def test_iter_authority_urls_resolves_relative_references() -> None: + """Relative references are resolved against origin and deduped when normalized.""" + RULE_REGISTRY.clear() + RULE_REGISTRY.update( + { + "AA001": _entry("AA001", "/specification#name"), + "AA002": _entry("AA002", "specification#name"), + "AA003": _entry("AA003", "https://example.test/specification#limits"), + "AA004": _entry("AA004", "/ignored", origin=""), + } + ) + + urls = list(iter_authority_urls()) + + assert urls == [ + "https://example.test/specification#name", + "https://example.test/specification#limits", + ] From cd4c39c7421b23a9cb3ffe3b3d79a973670ba521 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 11:43:55 +0000 Subject: [PATCH 6/6] test: cover relative authority URL normalization Agent-Logs-Url: https://github.com/bitflight-devops/skilllint/sessions/2ad2d721-1e8d-4c3d-919d-65336297e775 Co-authored-by: Jamie-BitFlight <25075504+Jamie-BitFlight@users.noreply.github.com> --- packages/skilllint/rule_registry.py | 10 +++- .../skilllint/tests/test_rule_registry.py | 53 +++++++------------ 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/packages/skilllint/rule_registry.py b/packages/skilllint/rule_registry.py index 498c607..65501c9 100644 --- a/packages/skilllint/rule_registry.py +++ b/packages/skilllint/rule_registry.py @@ -197,4 +197,12 @@ def iter_authority_urls(*, unique: bool = True) -> Iterator[str]: yield normalized -__all__ = ["RULE_REGISTRY", "RuleAuthority", "RuleEntry", "get_rule", "iter_authority_urls", "list_rules", "skilllint_rule"] +__all__ = [ + "RULE_REGISTRY", + "RuleAuthority", + "RuleEntry", + "get_rule", + "iter_authority_urls", + "list_rules", + "skilllint_rule", +] diff --git a/packages/skilllint/tests/test_rule_registry.py b/packages/skilllint/tests/test_rule_registry.py index 99e169c..15bc66a 100644 --- a/packages/skilllint/tests/test_rule_registry.py +++ b/packages/skilllint/tests/test_rule_registry.py @@ -21,15 +21,13 @@ def _entry(rule_id: str, reference: str | None, origin: str = "example.test") -> def test_iter_authority_urls_unique_filters_empty_and_dedupes() -> None: """Default unique iteration dedupes while preserving first-seen order.""" RULE_REGISTRY.clear() - RULE_REGISTRY.update( - { - "AA001": _entry("AA001", "https://example.test/spec#a"), - "AA002": _entry("AA002", "https://example.test/spec#a"), - "AA003": _entry("AA003", None), - "AA004": _entry("AA004", ""), - "AA005": _entry("AA005", "https://example.test/spec#b"), - } - ) + RULE_REGISTRY.update({ + "AA001": _entry("AA001", "https://example.test/spec#a"), + "AA002": _entry("AA002", "https://example.test/spec#a"), + "AA003": _entry("AA003", None), + "AA004": _entry("AA004", ""), + "AA005": _entry("AA005", "https://example.test/spec#b"), + }) urls = list(iter_authority_urls()) @@ -39,38 +37,27 @@ def test_iter_authority_urls_unique_filters_empty_and_dedupes() -> None: def test_iter_authority_urls_non_unique_keeps_duplicates() -> None: """unique=False includes duplicated references from multiple rules.""" RULE_REGISTRY.clear() - RULE_REGISTRY.update( - { - "AA001": _entry("AA001", "https://example.test/spec#a"), - "AA002": _entry("AA002", "https://example.test/spec#a"), - "AA003": _entry("AA003", "https://example.test/spec#b"), - } - ) + RULE_REGISTRY.update({ + "AA001": _entry("AA001", "https://example.test/spec#a"), + "AA002": _entry("AA002", "https://example.test/spec#a"), + "AA003": _entry("AA003", "https://example.test/spec#b"), + }) urls = list(iter_authority_urls(unique=False)) - assert urls == [ - "https://example.test/spec#a", - "https://example.test/spec#a", - "https://example.test/spec#b", - ] + assert urls == ["https://example.test/spec#a", "https://example.test/spec#a", "https://example.test/spec#b"] def test_iter_authority_urls_resolves_relative_references() -> None: """Relative references are resolved against origin and deduped when normalized.""" RULE_REGISTRY.clear() - RULE_REGISTRY.update( - { - "AA001": _entry("AA001", "/specification#name"), - "AA002": _entry("AA002", "specification#name"), - "AA003": _entry("AA003", "https://example.test/specification#limits"), - "AA004": _entry("AA004", "/ignored", origin=""), - } - ) + RULE_REGISTRY.update({ + "AA001": _entry("AA001", "/specification#name"), + "AA002": _entry("AA002", "specification#name"), + "AA003": _entry("AA003", "https://example.test/specification#limits"), + "AA004": _entry("AA004", "/ignored", origin=""), + }) urls = list(iter_authority_urls()) - assert urls == [ - "https://example.test/specification#name", - "https://example.test/specification#limits", - ] + assert urls == ["https://example.test/specification#name", "https://example.test/specification#limits"]