Skip to content

fix: keep core bundled registry URLs scoped#580

Merged
djm81 merged 1 commit into
devfrom
bugfix/core-registry-download-url-scope
May 20, 2026
Merged

fix: keep core bundled registry URLs scoped#580
djm81 merged 1 commit into
devfrom
bugfix/core-registry-download-url-scope

Conversation

@djm81
Copy link
Copy Markdown
Collaborator

@djm81 djm81 commented May 20, 2026

Summary

Fixes the bundled registry publishing path so core modules from specfact-cli do not get snapshot entries pointing at nold-ai/specfact-cli-modules.

Root Cause

publish-modules.yml generated bundled registry fragments through scripts/publish-module.py without overriding that script's marketplace-oriented default --download-base-url, so core module snapshots inherited the modules-repo URL base.

Changes

  • Set an explicit bundled registry download base for both single-module and batch publish paths: https://github.com/nold-ai/specfact-cli/releases/download.
  • Add a fail-closed guard in scripts/update-registry-index.py that rejects known core bundled module IDs when their generated download_url points at nold-ai/specfact-cli-modules.
  • Correct existing bundled snapshot URLs for init, upgrade, and module-registry.
  • Add regression coverage for the workflow and updater guard.

Validation

  • Failing-before: new regression tests failed against the pre-fix behavior.
  • hatch run pytest tests/unit/scripts/test_update_registry_index.py tests/unit/workflows/test_trustworthy_green_checks.py -q
  • hatch run format
  • hatch run lint
  • hatch run workflows-lint
  • hatch run yaml-lint
  • hatch run type-check (exit 0; existing warning set only)
  • hatch run check-version-sources
  • git diff --check
  • Pre-commit on amended commit: passed, including SpecFact code review with 0 findings.

@strix-security
Copy link
Copy Markdown

Strix is installed on this repository, but we could not run this PR security review because this workspace does not have an active plan. If you'd like to continue receiving code reviews, you can add a payment method or manage billing here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Review Change Stack

📝 Walkthrough

This PR fixes bundled registry publishing so SpecFact CLI core bundled modules (init, upgrade, module-registry, bundle-mapper) are recorded and published as artifacts belonging to the core repository (nold-ai/specfact-cli) rather than inheriting the marketplace repository (nold-ai/specfact-cli-modules).

User-visible behavior and CLI surface

  • No change to the user-facing CLI, commands, options, or defaults. All changes are release/publishing infrastructure and registry-updater validation only.

Root cause

  • publish-modules workflow invoked scripts/publish-module.py without overriding its marketplace-oriented default --download-base-url, causing core module snapshot entries to reference the modules repository instead of the CLI repository.

Changes

  • .github/workflows/publish-modules.yml
    • Adds BUNDLED_REGISTRY_DOWNLOAD_BASE_URL: https://github.com/nold-ai/specfact-cli/releases/download
    • Ensures both single-module and bundled (auto-publish) publish steps pass --download-base-url "${BUNDLED_REGISTRY_DOWNLOAD_BASE_URL}" to scripts/publish-module.py (multi-line argument formatting).
  • resources/bundled-module-registry/index.json
    • Corrected download_url entries for init, upgrade, and module-registry to use the specfact-cli releases/download path. bundle-mapper remains present without a download_url field in this snapshot.
  • scripts/update-registry-index.py
    • Adds CORE_BUNDLED_MODULE_IDS = frozenset({"init", "module-registry", "upgrade", "bundle-mapper"})
    • Adds CORE_REPOSITORY_DOWNLOAD_URL_PREFIX = "https://github.com/nold-ai/specfact-cli/releases/download/"
    • Adds _validate_entry_scope(entry) called from _load_entry(), which fails closed (raises ValueError) when a core bundled module entry’s download_url does not start with the required core releases prefix.
  • Tests
    • tests/unit/scripts/test_update_registry_index.py
      • Adds UpdateRegistryIndexModule Protocol typing for dynamic import.
      • Introduces _write_entry_fragment helper for consistent fragment generation.
      • Adds test_main_rejects_core_module_entry_from_unexpected_repository that asserts the updater rejects core entries that target an unexpected repository and leaves index.json unchanged.
    • tests/unit/workflows/test_trustworthy_green_checks.py
      • Adds test_publish_modules_bundled_snapshot_targets_core_repository asserting the workflow sets BUNDLED_REGISTRY_DOWNLOAD_BASE_URL to the specfact-cli releases URL exactly twice, that the modules-repo URL is absent, and that publish-module.py invocations include the explicit --download-base-url / ${BUNDLED_REGISTRY_DOWNLOAD_BASE_URL} (and that index-fragment invocations also propagate it).

Contract / API impact

  • None to public packages or runtime APIs: no changes to CLI commands, public functions, Pydantic models, or module package boundaries. The updater script changes add internal validation constants and a helper function; these are internal to scripts/ and the publishing pipeline.

Testing and quality gates

  • New regression/unit tests added to guard both workflow configuration and updater validation.
  • Validation performed by author: new regression tests failed prior to fix; unit tests run (hatch run pytest tests/unit/scripts/test_update_registry_index.py tests/unit/workflows/test_trustworthy_green_checks.py -q) and pass after the fix.
  • Repository quality gates exercised: formatting, linting, workflow-lint, YAML lint, type-check (noting an existing warning but exit 0), version-source checks, git diff --check, and pre-commit on amended commit passed. SpecFact code review returned 0 findings.

Docs / changelog / signing / versioning

  • No changes to docs/ or CHANGELOG.md in this PR. No module signing or version bump changes included.

Summary impact

  • This is a low-risk infrastructure fix that prevents incorrectly-scoped bundled-module snapshot entries and adds a defensive, fail-closed guard in the registry updater plus regression coverage to prevent regressions in the publish pipeline.

Walkthrough

Adds validation to reject core bundled module entries that reference the marketplace repository, passes an explicit BUNDLED_REGISTRY_DOWNLOAD_BASE_URL through publish jobs and publish-module.py invocations, updates bundled-registry entries to point to the core releases repo, and adds unit tests for both validation and workflow configuration.

Changes

Bundled Module Repository Enforcement

Layer / File(s) Summary
Core module validation constants and rules
scripts/update-registry-index.py
Adds CORE_BUNDLED_MODULE_IDS and CORE_REPOSITORY_DOWNLOAD_URL_PREFIX, and _validate_entry_scope(entry) which raises ValueError when a core-bundled module's download_url does not start with the required specfact-cli releases prefix. _load_entry() now calls this validation.
Workflow configuration for bundled registry downloads
.github/workflows/publish-modules.yml
Defines BUNDLED_REGISTRY_DOWNLOAD_BASE_URL in both publish and auto-publish job environments. Updates scripts/publish-module.py invocations (single-module and batched) to pass --download-base-url "${BUNDLED_REGISTRY_DOWNLOAD_BASE_URL}" while preserving -o dist and --index-fragment.
Registry data URL alignment
resources/bundled-module-registry/index.json
Repoints download_url for init, upgrade, and module-registry entries from github.com/nold-ai/specfact-cli-modules/releases/... to github.com/nold-ai/specfact-cli/releases/...; ids, latest_version, and checksums unchanged.
Test harness typing, helpers, and core validation tests
tests/unit/scripts/test_update_registry_index.py
Adds UpdateRegistryIndexModule Protocol, types _load_script_module() return, introduces _write_entry_fragment() helper, refactors existing tests to use the helper, and adds test_main_rejects_core_module_entry_from_unexpected_repository parametrized test asserting rejection (exit code 1) and unchanged index.
Workflow configuration integrity test
tests/unit/workflows/test_trustworthy_green_checks.py
Adds test_publish_modules_bundled_snapshot_targets_core_repository() to assert BUNDLED_REGISTRY_DOWNLOAD_BASE_URL points to specfact-cli releases (exact occurrence count), that specfact-cli-modules URL is not present, and that all scripts/publish-module.py blocks using --index-fragment include --download-base-url ${BUNDLED_REGISTRY_DOWNLOAD_BASE_URL}.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

module-system

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: keep core bundled registry URLs scoped' follows Conventional Commits style with 'fix:' prefix and clearly describes the main change.
Description check ✅ Passed The description provides clear summary, root cause analysis, specific changes, and comprehensive validation evidence demonstrating testing of the fix.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% 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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/core-registry-download-url-scope

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

@djm81 djm81 self-assigned this May 20, 2026
@djm81 djm81 added the bug Something isn't working label May 20, 2026
@djm81 djm81 moved this from Todo to In Progress in SpecFact CLI May 20, 2026
@djm81 djm81 marked this pull request as ready for review May 20, 2026 22:18
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@scripts/update-registry-index.py`:
- Around line 67-71: The current check only rejects download URLs containing
MODULES_REPOSITORY_URL_MARKER; tighten it to an explicit allowlist for core
modules by verifying that for any module_id in CORE_BUNDLED_MODULE_IDS the
download_url positively matches the expected repository (e.g., contains
"nold-ai/specfact-cli" or the exact canonical repo URL pattern you use) and
raise ValueError otherwise; update the guard around module_id and download_url
to test for the allowed repo string/pattern (instead of just excluding
MODULES_REPOSITORY_URL_MARKER) so no other hosts/paths can be persisted for
CORE_BUNDLED_MODULE_IDS.

In `@tests/unit/workflows/test_trustworthy_green_checks.py`:
- Around line 236-249: The test only checks that the bundled download base
appears somewhere in the output (expected_base in raw) which can pass if only
one publish job defines it; update the test to verify each publish job block
sets the env var and/or the download flag. Iterate over publish_blocks (the list
built from raw.split("\n\n") filtering on "scripts/publish-module.py") and for
every block assert expected_base in block (or assert "--download-base-url" and
"${BUNDLED_REGISTRY_DOWNLOAD_BASE_URL}" appear in each block); also keep the
existing special-case checks for blocks with "--index-fragment" to ensure they
include "--download-base-url" and the variable.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a9236a1d-d328-4d33-a248-102d3b84e372

📥 Commits

Reviewing files that changed from the base of the PR and between 7d88cb6 and 19bb6ae.

📒 Files selected for processing (5)
  • .github/workflows/publish-modules.yml
  • resources/bundled-module-registry/index.json
  • scripts/update-registry-index.py
  • tests/unit/scripts/test_update_registry_index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests (Python 3.12)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python-github-rules.mdc)

**/*.py: Maintain minimum 80% test coverage, with 100% coverage for critical paths in Python code
Use clear naming and self-documenting code, preferring clear names over comments
Ensure each function/class has a single clear purpose (Single Responsibility Principle)
Extract common patterns to avoid code duplication (DRY principle)
Apply SOLID object-oriented design principles in Python code
Use type hints everywhere in Python code and enable basedpyright strict mode
Use Pydantic models for data validation and serialization in Python
Use async/await for I/O operations in Python code
Use context managers for resource management in Python
Use dataclasses for simple data containers in Python
Enforce maximum line length of 120 characters in Python code
Use 4 spaces for indentation in Python code (no tabs)
Use 2 blank lines between classes and 1 blank line between methods in Python
Organize imports in order: Standard library → Third party → Local in Python files
Use snake_case for variables and functions in Python
Use PascalCase for class names in Python
Use UPPER_SNAKE_CASE for constants in Python
Use leading underscore (_) for private methods in Python classes
Use snake_case for Python file names
Enable basedpyright strict mode with strict type checking configuration in Python
Use Google-style docstrings for functions and classes in Python
Include comprehensive exception handling with specific exception types in Python code
Use logging with structured context (extra parameters) instead of print statements
Use retry logic with tenacity decorators (@retry) for operations that might fail
Use Pydantic BaseSettings for environment-based configuration in Python
Validate user input using Pydantic validators in Python models
Use @lru_cache and Redis-based caching for expensive calculations in Python
Run code formatting with Black (120 character line length) and isort in Python
Run type checking with basedpyright on all Python files
Run linting with ruff and pylint on all Pyth...

Files:

  • scripts/update-registry-index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py
  • tests/unit/scripts/test_update_registry_index.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{py,pyi}: After any code changes, follow these steps in order: (1) Apply linting and formatting to ensure code quality: hatch run format, (2) Type checking: hatch run type-check (basedpyright), (3) Contract-first approach: Run hatch run contract-test for contract validation, (4) Run full test suite: hatch test --cover -v, (5) Verify all tests pass and contracts are satisfied, (6) Fix any issues and repeat steps until all tests pass
All public APIs must have @icontract decorators and @beartype type checking
Use Pydantic models for all data structures with data validation
Only write high-value comments if at all. Avoid talking to the user through comments

Files:

  • scripts/update-registry-index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py
  • tests/unit/scripts/test_update_registry_index.py
scripts/**/*.py

⚙️ CodeRabbit configuration file

scripts/**/*.py: Deterministic tooling: subprocess safety, Hatch integration, and parity with documented
quality gates (format, type-check, module signing).

Files:

  • scripts/update-registry-index.py
**/test_*.py

📄 CodeRabbit inference engine (.cursor/rules/python-github-rules.mdc)

**/test_*.py: Write tests first in test-driven development (TDD) using the Red-Green-Refactor cycle
Ensure each test is independent and repeatable with no shared state between tests
Organize Python imports in tests using unittest.mock for Mock and patch
Use setup_method() for test initialization and Arrange-Act-Assert pattern in test files
Use @pytest.mark.asyncio decorator for async test functions in Python
Organize test files in structure: tests/unit/, tests/integration/, tests/e2e/ by module

Files:

  • tests/unit/workflows/test_trustworthy_green_checks.py
  • tests/unit/scripts/test_update_registry_index.py
tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/spec-fact-cli-rules.mdc)

Tests must be meaningful and test actual functionality, cover both success and failure cases, be independent and repeatable, and have clear, descriptive names. NO EXCEPTIONS - no placeholder or empty tests.

tests/**/*.py: Trim low-value unit tests when a contract covers the same assertion (type/shape/raises on negative checks)
Delete tests that only assert input validation, datatype/shape enforcement, or raises on negative conditions now guarded by contracts and runtime typing
Convert repeated edge-case permutations into one Hypothesis property with contracts acting as oracles

Secret redaction via LoggerSetup.redact_secrets must be covered by unit tests

Files:

  • tests/unit/workflows/test_trustworthy_green_checks.py
  • tests/unit/scripts/test_update_registry_index.py

⚙️ CodeRabbit configuration file

tests/**/*.py: Contract-first testing: meaningful scenarios, not redundant assertions already covered by
contracts. Flag flakiness, environment coupling, and missing coverage for changed behavior.

Files:

  • tests/unit/workflows/test_trustworthy_green_checks.py
  • tests/unit/scripts/test_update_registry_index.py
@(src|tests)/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/spec-fact-cli-rules.mdc)

Linting must pass with no errors using: pylint src tests

Files:

  • tests/unit/workflows/test_trustworthy_green_checks.py
  • tests/unit/scripts/test_update_registry_index.py
.github/workflows/*.{yml,yaml}

📄 CodeRabbit inference engine (.cursor/rules/testing-and-build-guide.mdc)

Validate GitHub workflow files using hatch run lint-workflows before committing

.github/workflows/*.{yml,yaml}: Use actionlint for semantic validation of GitHub Actions workflows
Format GitHub Actions workflows using hatch run workflows-fmt and lint them with hatch run workflows-lint after editing

Files:

  • .github/workflows/publish-modules.yml
.github/workflows/!(tests).{yml,yaml}

📄 CodeRabbit inference engine (.cursor/rules/testing-and-build-guide.mdc)

Do not re-run the full test suite in other CI workflows; tests are enforced only in the dedicated Tests workflow (.github/workflows/tests.yml)

Files:

  • .github/workflows/publish-modules.yml
.github/workflows/**

⚙️ CodeRabbit configuration file

.github/workflows/**: CI safety: secrets usage, workflow dependencies, alignment with hatch test / contract-test
gates, and action versions.

Files:

  • .github/workflows/publish-modules.yml
🔀 Multi-repo context nold-ai/specfact-cli-modules

nold-ai/specfact-cli-modules

  • .github/workflows/publish-modules.yml — workflow exists that could publish module artifacts and influence download base URLs used for bundled snapshots. [::nold-ai/specfact-cli-modules::.github/workflows/publish-modules.yml]
  • scripts/publish_module.py — module-publish helper present in this repo (validate/publish inputs). Changes in specfact-cli that alter publish flags/--download-base-url may need to be aligned with this script or vice-versa. [::nold-ai/specfact-cli-modules::scripts/publish_module.py]
  • tests asserting repo URLs / docs/site expectations:
    • tests/unit/test_modules_docs_site_contract.py — contains assertions referencing both nold-ai.github.io/specfact-cli-modules and nold-ai.github.io/specfact-cli (indicates tests care about which repo is referenced in docs/registry outputs). [::nold-ai/specfact-cli-modules::tests/unit/test_modules_docs_site_contract.py]
    • multiple unit tests reference "nold-ai/specfact-cli" or "nold-ai/specfact-cli-modules" (search hits across tests), showing consumers/tests expect correct repo scoping for core vs modules artifacts. [::nold-ai/specfact-cli-modules::tests/]

Findings summary

  • This repository contains workflows and publish helpers that are relevant to the PR's change (download-base-url and bundled registry publishing). Tests here already distinguish between specfact-cli and specfact-cli-modules URIs, so mismatches in download_url scope (core vs modules repository) could cause failing assertions here. [::nold-ai/specfact-cli-modules::]
🔇 Additional comments (4)
scripts/update-registry-index.py (1)

20-21: LGTM!

Also applies to: 58-66

.github/workflows/publish-modules.yml (1)

44-45: LGTM!

Also applies to: 118-120, 207-208, 275-277

resources/bundled-module-registry/index.json (1)

5-5: LGTM!

Also applies to: 11-11, 17-17

tests/unit/scripts/test_update_registry_index.py (1)

8-8: LGTM!

Also applies to: 11-19, 27-31, 35-38, 46-58, 89-95, 106-124

Comment thread scripts/update-registry-index.py Outdated
Comment thread tests/unit/workflows/test_trustworthy_green_checks.py
@djm81 djm81 force-pushed the bugfix/core-registry-download-url-scope branch from 19bb6ae to d54bfa4 Compare May 20, 2026 22:27
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/unit/scripts/test_update_registry_index.py (1)

108-134: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a positive-path assertion for core URLs.

Lines 108-134 only validate rejection cases. A regression that rejects valid core URLs (specfact-cli/releases/download) would still pass and can break bundled core publishing.

Proposed test addition
+def test_main_accepts_core_module_entry_from_core_repository(tmp_path: Path) -> None:
+    """Core bundled module entries should be accepted when scoped to core releases."""
+    module = _load_script_module()
+    index_path = tmp_path / "index.json"
+    entry_path = tmp_path / "entry.yaml"
+    index_path.write_text(json.dumps({"schema_version": "1.0.0", "modules": []}), encoding="utf-8")
+    _write_entry_fragment(
+        entry_path,
+        "upgrade",
+        "0.1.20",
+        "https://github.com/nold-ai/specfact-cli/releases/download/upgrade-0.1.20.tar.gz",
+        "abc",
+    )
+
+    exit_code = module.main(["--index-path", str(index_path), "--entry-fragment", str(entry_path)])
+
+    assert exit_code == 0
+    payload = json.loads(index_path.read_text(encoding="utf-8"))
+    assert payload["modules"][0]["id"] == "upgrade"

As per coding guidelines, "Tests must be meaningful and test actual functionality, cover both success and failure cases."

🤖 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/unit/scripts/test_update_registry_index.py` around lines 108 - 134, The
test only covers rejection of non-core download URLs; add a positive-path check
that a valid core repository URL is accepted by module.main so regressions that
erroneously reject core uploads are caught. Create a new test (or extend
test_main_rejects_core_module_entry_from_unexpected_repository) that uses
_write_entry_fragment to write an entry with a canonical core URL (e.g.,
matching specfact-cli/releases/download), call module.main(["--index-path",
str(index_path), "--entry-fragment", str(entry_path)]), and assert exit_code ==
0 and that index_path JSON now contains the new module entry (inspect the
modules list) to verify successful acceptance.
resources/bundled-module-registry/index.json (1)

22-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

bundle-mapper entry lacks download_url per OpenSpec contract.

The bundle-mapper entry in the registry index is intentionally unpublished (no download_url or checksum_sha256). However, the OpenSpec module-publishing spec explicitly states that all registry entries MUST include id, latest_version, download_url, and checksum_sha256. This entry currently violates that contract.

The current validation in _validate_entry_scope() will not catch this because validation only applies to NEW entry fragments processed by update-registry-index.py, not to existing entries already in the index. When bundle-mapper is eventually published, the generated fragment will be validated correctly (requiring the URL to reference the core repository).

To align with OpenSpec: either (1) only add bundle-mapper to the index once it's published with a download URL, or (2) update the OpenSpec contract and validation logic to explicitly allow unpublished core module placeholders.

🤖 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 `@resources/bundled-module-registry/index.json` around lines 22 - 24, The
registry index contains a "bundle-mapper" entry missing required fields
(download_url and checksum_sha256) and thus violates the OpenSpec; fix by either
removing the "bundle-mapper" object from the index until a published artifact
with a valid download_url and checksum_sha256 exists, or update the validation
logic in update-registry-index.py (specifically _validate_entry_scope()) and the
OpenSpec contract to explicitly allow unpublished core placeholders by adding an
explicit flag (e.g., "unpublished": true) and ensuring _validate_entry_scope()
accepts that flag as exempt from requiring download_url/checksum_sha256; target
the "bundle-mapper" JSON object and the _validate_entry_scope() function when
making changes.
🧹 Nitpick comments (1)
scripts/update-registry-index.py (1)

62-71: 💤 Low value

Consider defense-in-depth validation as suggested in past review.

The current positive-allowlist check (ensuring the URL starts with the correct prefix) addresses the core issue. However, the past review suggested also adding an explicit negative check to reject URLs containing MODULES_REPOSITORY_URL_MARKER anywhere in the path—this would catch edge cases where a URL might start correctly but still reference the modules repo downstream (e.g., via redirect or complex path).

The current implementation is functionally correct for the immediate fix, but the additional check would provide defense-in-depth at minimal cost.

🛡️ Optional hardening per past review suggestion
 def _validate_entry_scope(entry: dict[str, Any]) -> None:
     """Reject core bundled module entries that are not scoped to the core repository."""
     module_id = str(entry["id"])
     download_url = str(entry["download_url"])
     if module_id in CORE_BUNDLED_MODULE_IDS and not download_url.startswith(CORE_REPOSITORY_DOWNLOAD_URL_PREFIX):
         raise ValueError(
             f"Core bundled module entry {module_id!r} must reference nold-ai/specfact-cli, got {download_url!r}"
         )
+    if module_id in CORE_BUNDLED_MODULE_IDS and "specfact-cli-modules" in download_url:
+        raise ValueError(
+            f"Core bundled module entry {module_id!r} must not reference specfact-cli-modules, got {download_url!r}"
+        )
🤖 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/update-registry-index.py` around lines 62 - 71, In
_validate_entry_scope, add a defense-in-depth negative check: after computing
download_url (and alongside the existing startswith check using
CORE_REPOSITORY_DOWNLOAD_URL_PREFIX and membership check against
CORE_BUNDLED_MODULE_IDS), also reject any entry where
MODULES_REPOSITORY_URL_MARKER appears anywhere in download_url by raising the
same ValueError; update the conditional logic so core-bundled modules must both
start with CORE_REPOSITORY_DOWNLOAD_URL_PREFIX and must not contain
MODULES_REPOSITORY_URL_MARKER.
🤖 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.

Outside diff comments:
In `@resources/bundled-module-registry/index.json`:
- Around line 22-24: The registry index contains a "bundle-mapper" entry missing
required fields (download_url and checksum_sha256) and thus violates the
OpenSpec; fix by either removing the "bundle-mapper" object from the index until
a published artifact with a valid download_url and checksum_sha256 exists, or
update the validation logic in update-registry-index.py (specifically
_validate_entry_scope()) and the OpenSpec contract to explicitly allow
unpublished core placeholders by adding an explicit flag (e.g., "unpublished":
true) and ensuring _validate_entry_scope() accepts that flag as exempt from
requiring download_url/checksum_sha256; target the "bundle-mapper" JSON object
and the _validate_entry_scope() function when making changes.

In `@tests/unit/scripts/test_update_registry_index.py`:
- Around line 108-134: The test only covers rejection of non-core download URLs;
add a positive-path check that a valid core repository URL is accepted by
module.main so regressions that erroneously reject core uploads are caught.
Create a new test (or extend
test_main_rejects_core_module_entry_from_unexpected_repository) that uses
_write_entry_fragment to write an entry with a canonical core URL (e.g.,
matching specfact-cli/releases/download), call module.main(["--index-path",
str(index_path), "--entry-fragment", str(entry_path)]), and assert exit_code ==
0 and that index_path JSON now contains the new module entry (inspect the
modules list) to verify successful acceptance.

---

Nitpick comments:
In `@scripts/update-registry-index.py`:
- Around line 62-71: In _validate_entry_scope, add a defense-in-depth negative
check: after computing download_url (and alongside the existing startswith check
using CORE_REPOSITORY_DOWNLOAD_URL_PREFIX and membership check against
CORE_BUNDLED_MODULE_IDS), also reject any entry where
MODULES_REPOSITORY_URL_MARKER appears anywhere in download_url by raising the
same ValueError; update the conditional logic so core-bundled modules must both
start with CORE_REPOSITORY_DOWNLOAD_URL_PREFIX and must not contain
MODULES_REPOSITORY_URL_MARKER.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dca96fad-e135-479d-b51d-bfd338b07a07

📥 Commits

Reviewing files that changed from the base of the PR and between 19bb6ae and d54bfa4.

📒 Files selected for processing (5)
  • .github/workflows/publish-modules.yml
  • resources/bundled-module-registry/index.json
  • scripts/update-registry-index.py
  • tests/unit/scripts/test_update_registry_index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Type Checking (basedpyright)
  • GitHub Check: Linting (ruff, pylint, safe-write guard)
  • GitHub Check: Contract-First CI
  • GitHub Check: Tests (Python 3.12)
  • GitHub Check: Compatibility (Python 3.11)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/python-github-rules.mdc)

**/*.py: Maintain minimum 80% test coverage, with 100% coverage for critical paths in Python code
Use clear naming and self-documenting code, preferring clear names over comments
Ensure each function/class has a single clear purpose (Single Responsibility Principle)
Extract common patterns to avoid code duplication (DRY principle)
Apply SOLID object-oriented design principles in Python code
Use type hints everywhere in Python code and enable basedpyright strict mode
Use Pydantic models for data validation and serialization in Python
Use async/await for I/O operations in Python code
Use context managers for resource management in Python
Use dataclasses for simple data containers in Python
Enforce maximum line length of 120 characters in Python code
Use 4 spaces for indentation in Python code (no tabs)
Use 2 blank lines between classes and 1 blank line between methods in Python
Organize imports in order: Standard library → Third party → Local in Python files
Use snake_case for variables and functions in Python
Use PascalCase for class names in Python
Use UPPER_SNAKE_CASE for constants in Python
Use leading underscore (_) for private methods in Python classes
Use snake_case for Python file names
Enable basedpyright strict mode with strict type checking configuration in Python
Use Google-style docstrings for functions and classes in Python
Include comprehensive exception handling with specific exception types in Python code
Use logging with structured context (extra parameters) instead of print statements
Use retry logic with tenacity decorators (@retry) for operations that might fail
Use Pydantic BaseSettings for environment-based configuration in Python
Validate user input using Pydantic validators in Python models
Use @lru_cache and Redis-based caching for expensive calculations in Python
Run code formatting with Black (120 character line length) and isort in Python
Run type checking with basedpyright on all Python files
Run linting with ruff and pylint on all Pyth...

Files:

  • scripts/update-registry-index.py
  • tests/unit/scripts/test_update_registry_index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{py,pyi}: After any code changes, follow these steps in order: (1) Apply linting and formatting to ensure code quality: hatch run format, (2) Type checking: hatch run type-check (basedpyright), (3) Contract-first approach: Run hatch run contract-test for contract validation, (4) Run full test suite: hatch test --cover -v, (5) Verify all tests pass and contracts are satisfied, (6) Fix any issues and repeat steps until all tests pass
All public APIs must have @icontract decorators and @beartype type checking
Use Pydantic models for all data structures with data validation
Only write high-value comments if at all. Avoid talking to the user through comments

Files:

  • scripts/update-registry-index.py
  • tests/unit/scripts/test_update_registry_index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py
scripts/**/*.py

⚙️ CodeRabbit configuration file

scripts/**/*.py: Deterministic tooling: subprocess safety, Hatch integration, and parity with documented
quality gates (format, type-check, module signing).

Files:

  • scripts/update-registry-index.py
**/test_*.py

📄 CodeRabbit inference engine (.cursor/rules/python-github-rules.mdc)

**/test_*.py: Write tests first in test-driven development (TDD) using the Red-Green-Refactor cycle
Ensure each test is independent and repeatable with no shared state between tests
Organize Python imports in tests using unittest.mock for Mock and patch
Use setup_method() for test initialization and Arrange-Act-Assert pattern in test files
Use @pytest.mark.asyncio decorator for async test functions in Python
Organize test files in structure: tests/unit/, tests/integration/, tests/e2e/ by module

Files:

  • tests/unit/scripts/test_update_registry_index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py
tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/spec-fact-cli-rules.mdc)

Tests must be meaningful and test actual functionality, cover both success and failure cases, be independent and repeatable, and have clear, descriptive names. NO EXCEPTIONS - no placeholder or empty tests.

tests/**/*.py: Trim low-value unit tests when a contract covers the same assertion (type/shape/raises on negative checks)
Delete tests that only assert input validation, datatype/shape enforcement, or raises on negative conditions now guarded by contracts and runtime typing
Convert repeated edge-case permutations into one Hypothesis property with contracts acting as oracles

Secret redaction via LoggerSetup.redact_secrets must be covered by unit tests

Files:

  • tests/unit/scripts/test_update_registry_index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py

⚙️ CodeRabbit configuration file

tests/**/*.py: Contract-first testing: meaningful scenarios, not redundant assertions already covered by
contracts. Flag flakiness, environment coupling, and missing coverage for changed behavior.

Files:

  • tests/unit/scripts/test_update_registry_index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py
@(src|tests)/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/spec-fact-cli-rules.mdc)

Linting must pass with no errors using: pylint src tests

Files:

  • tests/unit/scripts/test_update_registry_index.py
  • tests/unit/workflows/test_trustworthy_green_checks.py
.github/workflows/*.{yml,yaml}

📄 CodeRabbit inference engine (.cursor/rules/testing-and-build-guide.mdc)

Validate GitHub workflow files using hatch run lint-workflows before committing

.github/workflows/*.{yml,yaml}: Use actionlint for semantic validation of GitHub Actions workflows
Format GitHub Actions workflows using hatch run workflows-fmt and lint them with hatch run workflows-lint after editing

Files:

  • .github/workflows/publish-modules.yml
.github/workflows/!(tests).{yml,yaml}

📄 CodeRabbit inference engine (.cursor/rules/testing-and-build-guide.mdc)

Do not re-run the full test suite in other CI workflows; tests are enforced only in the dedicated Tests workflow (.github/workflows/tests.yml)

Files:

  • .github/workflows/publish-modules.yml
.github/workflows/**

⚙️ CodeRabbit configuration file

.github/workflows/**: CI safety: secrets usage, workflow dependencies, alignment with hatch test / contract-test
gates, and action versions.

Files:

  • .github/workflows/publish-modules.yml
🔀 Multi-repo context nold-ai/specfact-cli-modules

Findings

nold-ai/specfact-cli-modules

  • Workflow that publishes modules and builds bundled registry snapshots: .github/workflows/publish-modules.yml — contains the publish job and the step that runs scripts/publish-module.py to generate registry artifacts. [::nold-ai/specfact-cli-modules::.github/workflows/publish-modules.yml]

  • Compatibility wrapper for the publish helper: scripts/publish-module.py — imports and delegates to scripts.publish_module (note wrapper name differs from internal script). [::nold-ai/specfact-cli-modules::scripts/publish-module.py]

  • Publish helper implementation: scripts/publish_module.py — performs manifest vs registry version checks and is the canonical publish validation used by the workflow. (Head of file present in repo.) [::nold-ai/specfact-cli-modules::scripts/publish_module.py]

  • Tests that assert which repository/hosts are expected:

    • tests/unit/test_modules_docs_site_contract.py — contains expectations about docs/hosts and asserts absence/presence of nold-ai.github.io/specfact-cli-modules vs nold-ai.github.io/specfact-cli. Changes to download_url scoping could affect these assertions. [::nold-ai/specfact-cli-modules::tests/unit/test_modules_docs_site_contract.py]
    • Multiple unit tests reference "nold-ai/specfact-cli" and "nold-ai/specfact-cli-modules" (e.g., tests under tests/unit/specfact_backlog/ and tests/unit/scripts/) — indicates consumers and tests in this repo are sensitive to repo scoping and URL strings. [::nold-ai/specfact-cli-modules::tests/]
  • Registry and publishing-related codepaths:

    • registry/index.json and registry/modules are referenced and manipulated by the workflow and scripts (publish step reads registry/index.json in workflow). [::nold-ai/specfact-cli-modules::.github/workflows/publish-modules.yml]
    • docs and openspec mention publish-modules workflow as canonical for updating registry; changing workflow flags (--download-base-url) or adding validation in other repo (specfact-cli) is relevant. [::nold-ai/specfact-cli-modules::docs, ::openspec]

Conclusion: specfact-cli-modules contains the workflow and publish helpers that produce bundled registry entries and tests that assert correct repo/host scoping. The PR changes in specfact-cli (explicit download base, guard rejecting core modules from unexpected repo) are directly relevant — mismatched download_url bases or altered flags will affect artifacts produced by this repo's publish workflow and may impact the tests noted above.

🔇 Additional comments (7)
tests/unit/scripts/test_update_registry_index.py (1)

8-106: LGTM!

tests/unit/workflows/test_trustworthy_green_checks.py (1)

233-251: LGTM!

scripts/update-registry-index.py (2)

20-21: LGTM!


58-58: LGTM!

.github/workflows/publish-modules.yml (2)

44-44: LGTM!

Also applies to: 207-207


118-120: LGTM!

Also applies to: 275-277

resources/bundled-module-registry/index.json (1)

5-5: 🏗️ Heavy lift

No actionable changes required. The URL format in the bundled registry is correct by design and matches the schema enforced by scripts/update-registry-index.py and scripts/publish-module.py.

The bundled registry file (resources/bundled-module-registry/index.json) serves as metadata for the publishing workflow and cross-repo module dependencies, not as live download sources. The download_url entries follow the intentional pattern {base_url}/{module-id}-{version}.tar.gz generated by publish-module.py (line: f"{download_base_url.rstrip('/')}/{tarball_name}"). The version identifier is embedded in the tarball name itself (e.g., init-0.1.33.tar.gz), eliminating the need for an intermediate TAG directory. The repository-scoping guard in scripts/update-registry-index.py already validates that core modules reference https://github.com/nold-ai/specfact-cli/releases/download/ as intended.

@djm81 djm81 merged commit b58f924 into dev May 20, 2026
22 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in SpecFact CLI May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bugfix] Suppress benign pipx spaced-home warning during upgrade Spaces in paths warning

1 participant