diff --git a/docs/adapters/azuredevops.md b/docs/adapters/azuredevops.md index 142ae04..8529e34 100644 --- a/docs/adapters/azuredevops.md +++ b/docs/adapters/azuredevops.md @@ -187,7 +187,7 @@ adapter = AdoAdapter( ### Error diagnostics (PATCH failures) -When a work item PATCH fails (e.g. HTTP 400 during backlog refine or status update), the CLI shows the ADO error message and a hint in the console. With `--debug`, the log includes the ADO response snippet and the JSON Patch paths attempted so you can identify the failing field. See [Debug Logging – Examining ADO API Errors](../reference/debug-logging.md#examining-ado-api-errors) and [Troubleshooting – Backlog refine or work item PATCH fails (400/422)](../guides/troubleshooting.md#backlog-refine-or-work-item-patch-fails-400422). +When a work item PATCH fails (e.g. HTTP 400 during backlog refine or status update), the CLI shows the ADO error message and a hint in the console. With `--debug`, the log includes the ADO response snippet and the JSON Patch paths attempted so you can identify the failing field. See [Debug Logging – Examining ADO API Errors](https://docs.specfact.io/core-cli/debug-logging/#examining-ado-api-errors) and [Troubleshooting – Backlog refine or work item PATCH fails (400/422)](../guides/troubleshooting.md#backlog-refine-or-work-item-patch-fails-400422). ## Usage Examples diff --git a/docs/bundles/code-review/overview.md b/docs/bundles/code-review/overview.md index e6a7206..bb65032 100644 --- a/docs/bundles/code-review/overview.md +++ b/docs/bundles/code-review/overview.md @@ -16,7 +16,7 @@ Use it together with the [Codebase](../codebase/overview/) bundle (`import`, `an ## Prerequisites -- `specfact module install nold-ai/specfact-code-review` +- `specfact module install nold-ai/specfact-code-review` — the manifest `bundle_dependencies` list includes **`nold-ai/specfact-codebase`**, so SpecFact CLI **will automatically install** the Codebase bundle alongside this one for the full shared **`specfact code`** command surface (import, analyze, drift, and related commands live there). - Optional tool installs (Ruff, Radon, Semgrep, Pyright, etc.) as described in command help ## `specfact code review` — nested commands diff --git a/docs/guides/agile-scrum-workflows.md b/docs/guides/agile-scrum-workflows.md index 9ddc88e..5039185 100644 --- a/docs/guides/agile-scrum-workflows.md +++ b/docs/guides/agile-scrum-workflows.md @@ -1046,5 +1046,5 @@ If template rendering fails: ## Related Documentation - [Command Reference - Project Commands](../reference/commands.md#project---project-bundle-management) - Complete command documentation including `project merge` and `project resolve-conflict` -- [Project Bundle Structure](../reference/directory-structure.md) - Project bundle organization +- [Project Bundle Structure](https://docs.specfact.io/reference/directory-structure/) - Project bundle organization (core CLI docs) - See [Project Commands](../reference/commands.md#project---project-bundle-management) for template customization options diff --git a/docs/reference/README.md b/docs/reference/README.md index 2a22435..399e674 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -19,12 +19,12 @@ Complete technical reference for the official modules site and bundle-owned work - **[Command Syntax Policy](command-syntax-policy.md)** - Source-of-truth argument syntax conventions for docs - **[Authentication](authentication.md)** - Device code auth flows and token storage - **[Architecture](architecture.md)** - Technical design, module structure, and internals -- **[Debug Logging](debug-logging.md)** - Where and what is logged when using `--debug` +- **[Debug Logging](https://docs.specfact.io/core-cli/debug-logging/)** - Where and what is logged when using `--debug` (core CLI docs) - **[Operational Modes](modes.md)** - CI/CD vs CoPilot modes - **[Specmatic API](specmatic.md)** - Specmatic integration API reference (functions, classes, integration points) - **[Telemetry](telemetry.md)** - Opt-in analytics and privacy guarantees -- **[Feature Keys](feature-keys.md)** - Key normalization and formats -- **[Directory Structure](directory-structure.md)** - Project structure and organization +- **[Feature Keys](https://docs.specfact.io/reference/feature-keys/)** - Key normalization and formats (core CLI docs) +- **[Directory Structure](https://docs.specfact.io/reference/directory-structure/)** - Project structure and organization (core CLI docs) - **[Schema Versioning](schema-versioning.md)** - Bundle schema versions and backward compatibility (v1.0, v1.1) - **[Module Security](module-security.md)** - Marketplace/module integrity and publisher metadata - **[Module Categories](module-categories.md)** - Category grouping model, canonical module assignments, bundles, and first-run profiles diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md index 8403805..da4e46b 100644 --- a/docs/reference/architecture.md +++ b/docs/reference/architecture.md @@ -161,6 +161,6 @@ Formal ADR pages are not yet published on the modules docs site. The current arc ## Related Docs -- [Directory Structure](directory-structure.md) +- [Directory Structure](https://docs.specfact.io/reference/directory-structure/) (core CLI docs) - [Module Development Guide](/authoring/module-development/) - [Adapter Development Guide](/authoring/adapter-development/) diff --git a/docs/reference/schema-versioning.md b/docs/reference/schema-versioning.md index 010f1e9..2a12c6b 100644 --- a/docs/reference/schema-versioning.md +++ b/docs/reference/schema-versioning.md @@ -178,4 +178,4 @@ schema_metadata: - [Architecture - Change Tracking Models](../reference/architecture.md#change-tracking-models-v11-schema) - Technical details - [Architecture - Bridge Adapter Interface](../reference/architecture.md#bridge-adapter-interface) - Adapter implementation guide -- [Directory Structure](directory-structure.md) - Bundle file organization +- [Directory Structure](https://docs.specfact.io/reference/directory-structure/) - Bundle file organization (core CLI docs) diff --git a/openspec/CHANGE_ORDER.md b/openspec/CHANGE_ORDER.md index 87cd8b5..d393068 100644 --- a/openspec/CHANGE_ORDER.md +++ b/openspec/CHANGE_ORDER.md @@ -72,3 +72,9 @@ Adds bidirectional conversion between spec-kit feature folders and OpenSpec chan | speckit | 03 | speckit-03-change-proposal-bridge | [#116](https://github.com/nold-ai/specfact-cli-modules/issues/116) | specfact-cli/speckit-02-v04-adapter-alignment ([specfact-cli#453](https://github.com/nold-ai/specfact-cli/issues/453)) | **Cross-repo dependency**: Requires `speckit-02-v04-adapter-alignment` in `nold-ai/specfact-cli` to be implemented first (provides `ToolCapabilities.extension_commands` consumed by `SpecKitBacklogSync`). + +### Module bundle peer dependencies + +| Module | Order | Change folder | GitHub # | Blocked by | +|--------|-------|---------------|----------|------------| +| peer-deps | 01 | module-bundle-deps-auto-install | [#135](https://github.com/nold-ai/specfact-cli-modules/issues/135) | — | diff --git a/openspec/changes/module-bundle-deps-auto-install/.openspec.yaml b/openspec/changes/module-bundle-deps-auto-install/.openspec.yaml new file mode 100644 index 0000000..6a5db8c --- /dev/null +++ b/openspec/changes/module-bundle-deps-auto-install/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-02 diff --git a/openspec/changes/module-bundle-deps-auto-install/TDD_EVIDENCE.md b/openspec/changes/module-bundle-deps-auto-install/TDD_EVIDENCE.md new file mode 100644 index 0000000..6b5b799 --- /dev/null +++ b/openspec/changes/module-bundle-deps-auto-install/TDD_EVIDENCE.md @@ -0,0 +1,49 @@ +# TDD evidence — module-bundle-deps-auto-install + +## Tests + +- Added `tests/unit/test_registry_manifest_bundle_dependencies.py`: + - `test_registry_bundle_dependencies_match_manifests` — every registry module with a local `module-package.yaml` must have matching `bundle_dependencies`. + - `test_official_bundle_dependency_graph_is_acyclic` — no cycles among `nold-ai/*` edges in `registry/index.json`. +- Ran: `.venv/bin/pytest tests/unit/test_registry_manifest_bundle_dependencies.py` — **pass** (2 tests). +- Ran: `.venv/bin/pytest tests/unit/docs/test_bundle_overview_cli_examples.py` — **pass** (after overview doc update). + +## Implementation + +- `packages/specfact-code-review/module-package.yaml`: `bundle_dependencies` includes `nold-ai/specfact-codebase`; version **0.46.0** (minor bump per design). +- `registry/index.json` + `registry/modules/specfact-code-review-0.46.0.tar.gz` (+ `.sha256`) aligned with publish workflow layout. +- `docs/bundles/code-review/overview.md`: prerequisites note peer dependency / auto-install behavior. + +## Signing (required before CI merge) + +Manifest integrity was generated with **`hatch run sign-modules -- --allow-unsigned`** (checksum only) because the local signing key is encrypted and no passphrase was available in this environment. + +**Before opening the PR or merging**, sign with the org private key so CI passes `verify-modules-signature --require-signature`: + +```bash +hatch run sign-modules -- \ + --key-file "${SPECFACT_MODULE_PRIVATE_SIGN_KEY_FILE:-$HOME/.specfact/sign-keys/module-signing-private.pem}" \ + packages/specfact-code-review/module-package.yaml \ + --payload-from-filesystem +``` + +Then re-run: + +```bash +hatch run verify-modules-signature -- --require-signature --payload-from-filesystem +``` + +If the manifest checksum changes after signing, rebuild the registry tarball and refresh `registry/index.json` checksum for `specfact-code-review-0.46.0.tar.gz` (same Python step as publish workflow) or re-run the publish automation. + +## Quality gates (2026-04-02, worktree) + +- `hatch run format` — pass +- `hatch run yaml-lint` — pass +- `hatch run type-check` (scoped + full lint path) — pass via `hatch run lint` +- `hatch run lint` — pass +- `python scripts/verify-modules-signature.py --payload-from-filesystem` — pass (all 6 manifests) +- `python scripts/verify-modules-signature.py --require-signature --payload-from-filesystem` — **fails until manifest is signed** (expected until signing step above) +- `hatch run contract-test` — pass +- `hatch run smart-test` — pass +- `hatch run test` — pass +- `hatch run specfact code review run --json --out .specfact/code-review.json --scope changed` — not run (SpecFact CLI: `Command 'code' is not installed`); complete before PR per `tasks.md` 4.3. diff --git a/openspec/changes/module-bundle-deps-auto-install/design.md b/openspec/changes/module-bundle-deps-auto-install/design.md new file mode 100644 index 0000000..a1e2d9b --- /dev/null +++ b/openspec/changes/module-bundle-deps-auto-install/design.md @@ -0,0 +1,42 @@ +## Context + +Official bundles ship `module-package.yaml` with `commands` and optional `bundle_dependencies`. The registry copies `bundle_dependencies` into `registry/index.json` during publish (`publish-modules.yml`). `nold-ai/specfact-codebase` already depends on `nold-ai/specfact-project`; `nold-ai/specfact-code-review` shares the `code` command group but lists no peer dependencies, so installs can omit the codebase bundle until users discover missing `code` subcommands manually. + +## Goals / Non-Goals + +**Goals:** + +- Declare `bundle_dependencies` for code-review so manifest and registry advertise the need for the codebase bundle (and, transitively via codebase, project). +- Keep manifest and registry `bundle_dependencies` fields aligned after version bump and publish. +- Add automated checks or tests that prevent drift between YAML manifest and JSON registry for this metadata where practical. + +**Non-Goals:** + +- Changing SpecFact CLI marketplace installer logic in this repo (core lives in `specfact-cli`); transitive `bundle_dependencies` behavior is confirmed in core (see “Resolved” below). +- Re-evaluating every bundle’s full dependency graph beyond the known code-review gap (optional follow-up audits). + +## Decisions + +1. **Dependency list for code-review** — Add a single entry `nold-ai/specfact-codebase`. Rationale: codebase already depends on project; duplicating project on code-review would be redundant unless CLI only installs direct deps. If CLI resolves transitive `bundle_dependencies`, one entry is sufficient. If not, extend to also list `nold-ai/specfact-project` after verifying core behavior. +2. **Semver** — Treat as **minor** if users perceive new auto-install behavior; **patch** if manifest/registry alignment only. Default to minor when `bundle_dependencies` changes user-facing install resolution. +3. **Verification** — Prefer extending existing registry/manifest tests or `verify-modules-signature` expectations over one-off scripts. + +## Risks / Trade-offs + +- **Circular dependency** — Code-review must not create a cycle. Codebase does not depend on code-review → safe. +- **Larger install footprint** — Users installing only code-review will pull more bundles; acceptable per goal of “full command group.” +- **Core vs modules** — If CLI ignores `bundle_dependencies`, this change still documents intent; follow-up in specfact-cli. + +## Migration Plan + +1. Implement on a feature branch from `dev`; bump `specfact-code-review` version; update manifest + registry. +2. Run publish/sign verification locally; publish via normal workflow. +3. No data migration for end users beyond reinstalling or updating modules. + +## Resolved: transitive `bundle_dependencies` installs + +**Confirmed.** Marketplace installs recurse through `bundle_dependencies`: `_install_bundle_dependencies_for_module` in `specfact-cli` (`src/specfact_cli/registry/module_installer.py`) calls `install_module()` for each missing peer before placing the requested module, so transitive peers (e.g. codebase → project) are installed in order. + +**Spec evidence:** `specfact-cli` `openspec/specs/official-bundle-tier/spec.md` — requirement **“Module installer auto-installs bundle dependencies for official-tier bundles”** (installer SHALL automatically install listed dependencies when an official bundle declares `bundle_dependencies`). + +**This change’s delta spec:** `openspec/changes/module-bundle-deps-auto-install/specs/module-bundle-dependencies/spec.md` — manifest/registry parity and acyclicity for declared peers. diff --git a/openspec/changes/module-bundle-deps-auto-install/proposal.md b/openspec/changes/module-bundle-deps-auto-install/proposal.md new file mode 100644 index 0000000..5b44278 --- /dev/null +++ b/openspec/changes/module-bundle-deps-auto-install/proposal.md @@ -0,0 +1,40 @@ +# Change: Declare peer bundle dependencies for auto-install + +## Why + +Several official bundles share a top-level command group (for example `specfact code …`) but only declare `commands: [code]` in the manifest. Installing a single bundle (such as `nold-ai/specfact-code-review`) therefore does not pull in sibling bundles that register the rest of that group’s commands. Users hit missing subcommands until they manually install `nold-ai/specfact-codebase`. Other bundles already declare `bundle_dependencies` (codebase, spec, govern depend on project); code-review is inconsistent and should declare the peer bundle(s) needed for a complete `code` group experience. + +## What Changes + +- Set `bundle_dependencies` on `nold-ai/specfact-code-review` to include `nold-ai/specfact-codebase` so the CLI can auto-install the codebase bundle (and its existing dependency on project) when users install code review. +- Align `registry/index.json` metadata for `nold-ai/specfact-code-review` with the updated manifest (`bundle_dependencies` field). +- Bump `specfact-code-review` semver (patch or minor per scope of manifest-only vs. user-visible install behavior) and refresh integrity checksums/signatures per publish workflow. +- Add or extend tests that assert manifest and registry rows stay consistent for declared bundle dependencies. +- Optionally document the dependency in bundle overview or install docs if user-facing guidance should mention the relationship. + +## Capabilities + +### New Capabilities + +- `module-bundle-dependencies`: Official module manifests and registry entries declare `bundle_dependencies` so SpecFact CLI can install required peer bundles for full command-group coverage; code-review lists codebase as a dependency. + +### Modified Capabilities + +- (none) — no existing `openspec/specs/` requirement files change; this change introduces a new capability spec. + +## Impact + +- `packages/specfact-code-review/module-package.yaml` — `bundle_dependencies` populated. +- `registry/index.json` — matching `bundle_dependencies` for the code-review module entry. +- **Compatibility** — `bundle_dependencies` auto-install is implemented in specfact-cli; when bumping this bundle, **review `core_compatibility`** in both `packages/specfact-code-review/module-package.yaml` and the corresponding `registry/index.json` row so they stay aligned and reflect the **minimum specfact-cli** that supports `_extract_bundle_dependencies` / `_install_bundle_dependencies_for_module` (e.g. raise the lower bound to `>=0.44.0` if needed). +- Published artifact tarball and signatures after version bump; `.github/workflows/publish-modules.yml` path unchanged except normal publish flow. +- `tests/` — assertions for registry/manifest parity if not already covered. +- Potential docs: `docs/bundles/code-review/overview.md` or reference pages if we surface the dependency to users. + +## Source Tracking + + +- **GitHub Issue**: #135 +- **Issue URL**: https://github.com/nold-ai/specfact-cli-modules/issues/135 +- **Last Synced Status**: synced +- **Sanitized**: true diff --git a/openspec/changes/module-bundle-deps-auto-install/specs/module-bundle-dependencies/spec.md b/openspec/changes/module-bundle-deps-auto-install/specs/module-bundle-dependencies/spec.md new file mode 100644 index 0000000..fee5288 --- /dev/null +++ b/openspec/changes/module-bundle-deps-auto-install/specs/module-bundle-dependencies/spec.md @@ -0,0 +1,34 @@ +# module-bundle-dependencies Specification + +## Purpose + +Define how official module packages declare peer bundle dependencies so SpecFact CLI and the registry expose a consistent install graph for shared command groups (for example `code`). + +## ADDED Requirements + +### Requirement: Code-review module declares codebase peer dependency + +The `nold-ai/specfact-code-review` module SHALL list `nold-ai/specfact-codebase` in `bundle_dependencies` inside `packages/specfact-code-review/module-package.yaml` so that installing code review can resolve the peer bundle required for the full `code` command group. + +#### Scenario: Manifest names the codebase bundle + +- **WHEN** a maintainer reads `packages/specfact-code-review/module-package.yaml` +- **THEN** the `bundle_dependencies` sequence includes `nold-ai/specfact-codebase` + +### Requirement: Registry mirrors manifest bundle dependencies for code-review + +The `registry/index.json` entry for `nold-ai/specfact-code-review` SHALL list the same `bundle_dependencies` values as the published `module-package.yaml` for that module version. + +#### Scenario: Registry matches manifest after publish + +- **WHEN** the code-review module version is published and the registry row is updated +- **THEN** the `bundle_dependencies` array for `nold-ai/specfact-code-review` equals the manifest’s `bundle_dependencies` for that version + +### Requirement: Dependency declarations stay acyclic + +Official module `bundle_dependencies` SHALL NOT introduce a dependency cycle between official nold-ai bundles. + +#### Scenario: Code-review dependency does not create a cycle + +- **WHEN** code-review declares a dependency on codebase +- **THEN** no official bundle manifest transitively depends back on `nold-ai/specfact-code-review` in a way that forms a cycle diff --git a/openspec/changes/module-bundle-deps-auto-install/tasks.md b/openspec/changes/module-bundle-deps-auto-install/tasks.md new file mode 100644 index 0000000..35cad23 --- /dev/null +++ b/openspec/changes/module-bundle-deps-auto-install/tasks.md @@ -0,0 +1,25 @@ +## 1. Branch and baseline + +- [x] 1.1 Create branch `feature/module-bundle-deps-auto-install` from `origin/dev` (use a dedicated worktree if preferred). + +## 2. Tests first (TDD) + +- [x] 2.1 Add or extend tests that fail until `bundle_dependencies` for `nold-ai/specfact-code-review` in `module-package.yaml` matches `registry/index.json` (and cover acyclic dependency expectation if applicable). + +## 3. Implementation + +- [x] 3.1 Update `packages/specfact-code-review/module-package.yaml`: set `bundle_dependencies` to include `nold-ai/specfact-codebase`; bump semver per design. +- [x] 3.2 Update `registry/index.json` for `nold-ai/specfact-code-review` so `bundle_dependencies` matches the manifest; refresh checksums, download_url, and version fields per publish/sign workflow when artifacts are produced. +- [x] 3.3 Run signing / `verify-modules-signature` flow so integrity fields in the manifest stay valid after edits. (Checksum OK via `--payload-from-filesystem`; **Ed25519 signature** must be applied with the org private key before merge — see `TDD_EVIDENCE.md`.) +- [x] 3.4 Optionally update user-facing docs (for example code-review bundle overview) to mention that installing code review pulls the codebase bundle for the full `code` command group. + +## 4. Evidence and quality gates + +- [x] 4.1 Record failing/passing test notes in `openspec/changes/module-bundle-deps-auto-install/TDD_EVIDENCE.md`. +- [x] 4.2 Run full quality gate sequence from AGENTS.md (`format`, `type-check`, `lint`, `yaml-lint`, `verify-modules-signature`, `contract-test`, `smart-test`, `test`). (Full suite run; `verify-modules-signature` without `--require-signature` passes; **with** `--require-signature` pending until signing step above.) +- [ ] 4.3 Ensure `.specfact/code-review.json` is present and fresh relative to edits under `packages/`, `registry/`, `tests/`, and this change folder (excluding evidence-only `TDD_EVIDENCE.md` updates). Run `hatch run specfact code review run --json --out .specfact/code-review.json` with `--scope changed` while iterating and `--scope full` before the final PR; remediate all findings. **Blocked here:** `specfact code review` requires workflow bundles (`Command 'code' is not installed`); run after `specfact module install` / profile init locally. +- [x] 4.4 Open PR to `dev` and link GitHub issue below. — PR [#136](https://github.com/nold-ai/specfact-cli-modules/pull/136) (issue [#135](https://github.com/nold-ai/specfact-cli-modules/issues/135)). + +## 5. Source tracking + +- [x] 5.1 Keep `proposal.md` Source Tracking in sync with the GitHub issue number and URL after issue creation. diff --git a/openspec/config.yaml b/openspec/config.yaml index 7b2e78d..cfbf059 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -64,7 +64,8 @@ rules: - For publish/sign flows, reference `scripts/` entrypoints and integrity expectations. tasks: - - Enforce SDD+TDD order: branch/worktree → spec deltas → failing tests → implementation → passing tests → + - >- + Enforce SDD+TDD order: branch/worktree → spec deltas → failing tests → implementation → passing tests → TDD_EVIDENCE.md → quality gates → PR. - Include module signing / version-bump tasks when `module-package.yaml` or bundle payloads change (see AGENTS.md). - Record TDD evidence in `openspec/changes//TDD_EVIDENCE.md` for behavior changes. diff --git a/packages/specfact-backlog/module-package.yaml b/packages/specfact-backlog/module-package.yaml index 86cc202..522b743 100644 --- a/packages/specfact-backlog/module-package.yaml +++ b/packages/specfact-backlog/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-backlog -version: 0.41.16 +version: 0.41.17 commands: - backlog tier: official @@ -7,6 +7,16 @@ publisher: name: nold-ai email: hello@noldai.com bundle_dependencies: [] +pip_dependencies: + - beartype + - click + - icontract + - pydantic + - pyyaml + - questionary + - requests + - rich + - typer core_compatibility: '>=0.40.0,<1.0.0' description: Official SpecFact backlog bundle package. category: backlog @@ -17,5 +27,5 @@ schema_extensions: project_metadata: - backlog_core.backlog_config integrity: - checksum: sha256:8edbb9cad806a60976bfb5348f9a24863d71305583feb140d3af37c45ba29e59 - signature: X7hk6nWlHPGKyjXwxY04BQzfMwq5Bh2h/qnTZZb8l6sqGdC9mPLvmoMMNkrZ422wP5UuPSgTJSAu04tAyWcoAw== + checksum: sha256:6a6db1b45acc3aaf913a0f179e20c00639d7d40a655a3028e5d31b7e3f4903f5 + signature: 4UyAIQtzf+VDUE6dLdAN6XcliOAl2cPatq3Z/hNO3Vtjziekbsb/09wJgx5nLKccNxL99KzIhJTcp5Ezp6rKBQ== diff --git a/packages/specfact-code-review/module-package.yaml b/packages/specfact-code-review/module-package.yaml index 3f3867d..85ae462 100644 --- a/packages/specfact-code-review/module-package.yaml +++ b/packages/specfact-code-review/module-package.yaml @@ -1,12 +1,13 @@ name: nold-ai/specfact-code-review -version: 0.45.4 +version: 0.46.1 commands: - code tier: official publisher: name: nold-ai email: hello@noldai.com -bundle_dependencies: [] +bundle_dependencies: + - nold-ai/specfact-codebase pip_dependencies: - basedpyright - crosshair-tool @@ -17,10 +18,10 @@ pip_dependencies: - requests - ruff - semgrep -core_compatibility: '>=0.40.0,<1.0.0' +core_compatibility: '>=0.44.0,<1.0.0' description: Official SpecFact code review bundle package. category: codebase bundle_group_command: code integrity: - checksum: sha256:78f1426086fb967c2f0ba8b0f5e65c5368205cd2a2ecd6f9d2a98531ed3ee402 - signature: 8AadiIQUR3mxaxh0Mk6voN1zX94GeVLbL57ZUjTF//cqz8mQaOmd4Vz4/zEMVNRstYkQ4l9rrzays4C47tnpBQ== + checksum: sha256:13a5a27fc13e2db6d36573b42482a1fe9d294e100738f6150ad0468868581dd1 + signature: ulRchbUE1q1JhCuTRUPUolICZMiWDBCQgNmNt4YNbtyRysXMhiIcge/KJ1X1VqKtzWu4lzvEDf0OInPJ2lRxBA== diff --git a/packages/specfact-codebase/module-package.yaml b/packages/specfact-codebase/module-package.yaml index abab82f..9776e9d 100644 --- a/packages/specfact-codebase/module-package.yaml +++ b/packages/specfact-codebase/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-codebase -version: 0.41.3 +version: 0.41.4 commands: - code tier: official @@ -8,10 +8,21 @@ publisher: email: hello@noldai.com bundle_dependencies: - nold-ai/specfact-project +pip_dependencies: + - beartype + - click + - icontract + - pydantic + - pyyaml + - rich + - ruamel.yaml + - tomli + - tomlkit + - typer core_compatibility: '>=0.40.0,<1.0.0' description: Official SpecFact codebase bundle package. category: codebase bundle_group_command: code integrity: - checksum: sha256:bdd12151c2ba736e254552adf472d7e217036dfe7fdf661e90204e74be44a2da - signature: VMeonMTm8x/ZFn8CBzr6mcZHi0WcldLMiAQl1Ihhs6DuwK+cF1BI/U+G05j21rbdPE9+5qZyZ5QZVEj4V3yECQ== + checksum: sha256:436e9e3d0d56a6eae861c4a6bef0a8f805f00b8b5bd8b0e037c19dede4972117 + signature: FQOsqabH5ATcyLfjTVrVJPIC4KiuwwFbCQZL+BZAu6dFoOz/DLjk91NZAyc7z6oq5dpVfwMHFsKEYqzW4wlDDA== diff --git a/packages/specfact-govern/module-package.yaml b/packages/specfact-govern/module-package.yaml index d222e2a..c2b8bd9 100644 --- a/packages/specfact-govern/module-package.yaml +++ b/packages/specfact-govern/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-govern -version: 0.40.19 +version: 0.40.20 commands: - govern tier: official @@ -8,10 +8,16 @@ publisher: email: hello@noldai.com bundle_dependencies: - nold-ai/specfact-project +pip_dependencies: + - beartype + - icontract + - rich + - ruamel.yaml + - typer core_compatibility: '>=0.40.0,<1.0.0' description: Official SpecFact governance bundle package. category: govern bundle_group_command: govern integrity: - checksum: sha256:cc214acd8e184ec36380eccb86ee0a8fe550e125209f4f461e03393a7886f203 - signature: N90cvb+lukJwUwGyVoj8ofmO8N3unlE13PQ5hYhMaDBdYQVMSY/9OL9TC8RLF+0LBSsRJ2IQ00188ZXRZWi1BA== + checksum: sha256:ec360f43302550736cbef07fff66c51b0f8e6ca5062d515bb04d123b944e2560 + signature: LizVAjTgulXuKrDFlw+v4svJLc8mVpC6/fVb1Z7MaG+fRx6SwNUC/DylB7CaROFfrBYXtcRkHBu3MmjGzizHCw== diff --git a/packages/specfact-project/module-package.yaml b/packages/specfact-project/module-package.yaml index ada0e54..5ce109a 100644 --- a/packages/specfact-project/module-package.yaml +++ b/packages/specfact-project/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-project -version: 0.41.2 +version: 0.41.3 commands: - project tier: official @@ -7,10 +7,25 @@ publisher: name: nold-ai email: hello@noldai.com bundle_dependencies: [] +pip_dependencies: + - beartype + - gitpython + - icontract + - jinja2 + - lz4 + - networkx + - pydantic + - pyyaml + - requests + - rich + - ruamel.yaml + - tomli + - typer + - watchdog core_compatibility: '>=0.40.0,<1.0.0' description: Official SpecFact project bundle package. category: project bundle_group_command: project integrity: - checksum: sha256:af5decf47e8db14e860a9ef17a4392e13129cd998a11913cfb6a296bb69e0ccc - signature: L3T4/NjXtaJFdqoL8At9o7uyXWfFpIYfeA1f7UkDf3jAGki57cnTBAgl3+RBeF32sr0Xcg7SBZ8AogJbXddCDA== + checksum: sha256:96e0b0266d79064beaeada8dfa3a0c2decf80038c640a422200be9bc5d51a0db + signature: J05ibXhduI10YnFVlNiEZik8S+VHsTbpU1FkWEYYeb+LKS4d74+f7AdWMgAz8DKNUwSrlBkjfqvLpoOh65XhBQ== diff --git a/packages/specfact-spec/module-package.yaml b/packages/specfact-spec/module-package.yaml index 3e2bea7..c86bc11 100644 --- a/packages/specfact-spec/module-package.yaml +++ b/packages/specfact-spec/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-spec -version: 0.40.16 +version: 0.40.17 commands: - spec tier: official @@ -8,10 +8,18 @@ publisher: email: hello@noldai.com bundle_dependencies: - nold-ai/specfact-project +pip_dependencies: + - beartype + - icontract + - jinja2 + - pyyaml + - requests + - rich + - typer core_compatibility: '>=0.40.0,<1.0.0' description: Official SpecFact specification bundle package. category: spec bundle_group_command: spec integrity: - checksum: sha256:28ad2375fff8965efda0e9e7816549f7ab05140a99010170d0da1607eb8f037f - signature: t9D2viaejsv6WQmAqvAFz1Ll6VXO0Q8wGwkVIvMt32amqI2MZN3++ecmeytiiTjzEGGHWQ1+xZm/zQYu51BMAg== + checksum: sha256:c5d058196cbf7cc871f350cfb6483ca59a28a299d16e78c330d7a21705575323 + signature: uWxT+qpVqTjF3BmO2rqdnSwII3YNQi3aA7UScZT6/PPN3Q0KvbtboVf1xLgIIp8/jjiWvks1dZFgXgyDNzozDg== diff --git a/pyproject.toml b/pyproject.toml index 689fbe0..cb9fbc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,12 @@ dependencies = [ "pylint>=4.0.2", "pre-commit>=3.7.0", "typer>=0.20.0", + "pyyaml>=6.0.3", +] + +[tool.hatch.envs.hatch-test] +extra-dependencies = [ + "pyyaml>=6.0.3", ] [tool.hatch.envs.default.scripts] diff --git a/registry/index.json b/registry/index.json index afd7ff8..54459f6 100644 --- a/registry/index.json +++ b/registry/index.json @@ -2,35 +2,37 @@ "modules": [ { "id": "nold-ai/specfact-project", - "latest_version": "0.41.2", - "download_url": "modules/specfact-project-0.41.2.tar.gz", - "checksum_sha256": "eea508c5bb23544482e830e1ca393059f72040a987ee0fecb525bac7c0aa8167", + "latest_version": "0.41.3", + "download_url": "modules/specfact-project-0.41.3.tar.gz", + "checksum_sha256": "a3df973c103e0708bef7a6ad23ead9b45e3354ba2ecb878f4d64e753e163a817", "tier": "official", "publisher": { "name": "nold-ai", "email": "hello@noldai.com" }, "bundle_dependencies": [], - "description": "Official SpecFact project bundle package." + "description": "Official SpecFact project bundle package.", + "core_compatibility": ">=0.40.0,<1.0.0" }, { "id": "nold-ai/specfact-backlog", - "latest_version": "0.41.16", - "download_url": "modules/specfact-backlog-0.41.16.tar.gz", - "checksum_sha256": "ad2d9dfdf6bbae411f11920eaafb9578c5b24599ce699fa74c141bb88a331c5f", + "latest_version": "0.41.17", + "download_url": "modules/specfact-backlog-0.41.17.tar.gz", + "checksum_sha256": "7b8f90087427cda34eedf38cdfd411196229cf91e1a5172bc4085958f33bf420", "tier": "official", "publisher": { "name": "nold-ai", "email": "hello@noldai.com" }, "bundle_dependencies": [], - "description": "Official SpecFact backlog bundle package." + "description": "Official SpecFact backlog bundle package.", + "core_compatibility": ">=0.40.0,<1.0.0" }, { "id": "nold-ai/specfact-codebase", - "latest_version": "0.41.3", - "download_url": "modules/specfact-codebase-0.41.3.tar.gz", - "checksum_sha256": "389a2d0095bbcc670857b6f20e6716402e5b4e0622155cc7d602eb44093fcb6e", + "latest_version": "0.41.4", + "download_url": "modules/specfact-codebase-0.41.4.tar.gz", + "checksum_sha256": "18534ed0fa07e711f57c9a473db01ab83b5b0ebefba0039b969997919907e049", "tier": "official", "publisher": { "name": "nold-ai", @@ -39,13 +41,14 @@ "bundle_dependencies": [ "nold-ai/specfact-project" ], - "description": "Official SpecFact codebase bundle package." + "description": "Official SpecFact codebase bundle package.", + "core_compatibility": ">=0.40.0,<1.0.0" }, { "id": "nold-ai/specfact-spec", - "latest_version": "0.40.16", - "download_url": "modules/specfact-spec-0.40.16.tar.gz", - "checksum_sha256": "5daf94c624df09bf04ddf55df3f79cf1e98fdddd2fd6be254a5155746f913d4a", + "latest_version": "0.40.17", + "download_url": "modules/specfact-spec-0.40.17.tar.gz", + "checksum_sha256": "a793088c0f9b4958a9dc939b51d004c8257b53fb584218b82beb0c7e0f39d848", "tier": "official", "publisher": { "name": "nold-ai", @@ -54,13 +57,14 @@ "bundle_dependencies": [ "nold-ai/specfact-project" ], - "description": "Official SpecFact specification bundle package." + "description": "Official SpecFact specification bundle package.", + "core_compatibility": ">=0.40.0,<1.0.0" }, { "id": "nold-ai/specfact-govern", - "latest_version": "0.40.19", - "download_url": "modules/specfact-govern-0.40.19.tar.gz", - "checksum_sha256": "5406ba35bf516e5d47fa59674ca63438caf345a03729ca28fc23c98edb370caf", + "latest_version": "0.40.20", + "download_url": "modules/specfact-govern-0.40.20.tar.gz", + "checksum_sha256": "8f58e1c0194f4915301d7946e3aed65f521d1f03a011287267f40244f45eaf89", "tier": "official", "publisher": { "name": "nold-ai", @@ -69,20 +73,23 @@ "bundle_dependencies": [ "nold-ai/specfact-project" ], - "description": "Official SpecFact governance bundle package." + "description": "Official SpecFact governance bundle package.", + "core_compatibility": ">=0.40.0,<1.0.0" }, { "id": "nold-ai/specfact-code-review", - "latest_version": "0.45.4", - "download_url": "modules/specfact-code-review-0.45.4.tar.gz", - "checksum_sha256": "54f2318ebe85546631b65786c9c77f04ede8629b6a2f8fcbda2664c4fb68f56c", - "core_compatibility": ">=0.40.0,<1.0.0", + "latest_version": "0.46.1", + "download_url": "modules/specfact-code-review-0.46.1.tar.gz", + "checksum_sha256": "cbd954a055dfa83bab76880350f3bdc5652046ed4527d0c0ff0d1550e51be1af", + "core_compatibility": ">=0.44.0,<1.0.0", "tier": "official", "publisher": { "name": "nold-ai", "email": "hello@noldai.com" }, - "bundle_dependencies": [], + "bundle_dependencies": [ + "nold-ai/specfact-codebase" + ], "description": "Official SpecFact code review bundle package." } ] diff --git a/registry/modules/specfact-backlog-0.41.17.tar.gz b/registry/modules/specfact-backlog-0.41.17.tar.gz new file mode 100644 index 0000000..aa4f0a5 Binary files /dev/null and b/registry/modules/specfact-backlog-0.41.17.tar.gz differ diff --git a/registry/modules/specfact-backlog-0.41.17.tar.gz.sha256 b/registry/modules/specfact-backlog-0.41.17.tar.gz.sha256 new file mode 100644 index 0000000..cd61328 --- /dev/null +++ b/registry/modules/specfact-backlog-0.41.17.tar.gz.sha256 @@ -0,0 +1 @@ +7b8f90087427cda34eedf38cdfd411196229cf91e1a5172bc4085958f33bf420 diff --git a/registry/modules/specfact-code-review-0.46.0.tar.gz b/registry/modules/specfact-code-review-0.46.0.tar.gz new file mode 100644 index 0000000..0855af4 Binary files /dev/null and b/registry/modules/specfact-code-review-0.46.0.tar.gz differ diff --git a/registry/modules/specfact-code-review-0.46.0.tar.gz.sha256 b/registry/modules/specfact-code-review-0.46.0.tar.gz.sha256 new file mode 100644 index 0000000..e765e72 --- /dev/null +++ b/registry/modules/specfact-code-review-0.46.0.tar.gz.sha256 @@ -0,0 +1 @@ +a74e3f7a4c274a6ce28f56944ecb04e040dd1792b235325fbe4d91a7accd033a diff --git a/registry/modules/specfact-code-review-0.46.1.tar.gz b/registry/modules/specfact-code-review-0.46.1.tar.gz new file mode 100644 index 0000000..b0d0801 Binary files /dev/null and b/registry/modules/specfact-code-review-0.46.1.tar.gz differ diff --git a/registry/modules/specfact-code-review-0.46.1.tar.gz.sha256 b/registry/modules/specfact-code-review-0.46.1.tar.gz.sha256 new file mode 100644 index 0000000..d452193 --- /dev/null +++ b/registry/modules/specfact-code-review-0.46.1.tar.gz.sha256 @@ -0,0 +1 @@ +cbd954a055dfa83bab76880350f3bdc5652046ed4527d0c0ff0d1550e51be1af diff --git a/registry/modules/specfact-codebase-0.41.4.tar.gz b/registry/modules/specfact-codebase-0.41.4.tar.gz new file mode 100644 index 0000000..88a82cb Binary files /dev/null and b/registry/modules/specfact-codebase-0.41.4.tar.gz differ diff --git a/registry/modules/specfact-codebase-0.41.4.tar.gz.sha256 b/registry/modules/specfact-codebase-0.41.4.tar.gz.sha256 new file mode 100644 index 0000000..1856e01 --- /dev/null +++ b/registry/modules/specfact-codebase-0.41.4.tar.gz.sha256 @@ -0,0 +1 @@ +18534ed0fa07e711f57c9a473db01ab83b5b0ebefba0039b969997919907e049 diff --git a/registry/modules/specfact-govern-0.40.20.tar.gz b/registry/modules/specfact-govern-0.40.20.tar.gz new file mode 100644 index 0000000..76ee24f Binary files /dev/null and b/registry/modules/specfact-govern-0.40.20.tar.gz differ diff --git a/registry/modules/specfact-govern-0.40.20.tar.gz.sha256 b/registry/modules/specfact-govern-0.40.20.tar.gz.sha256 new file mode 100644 index 0000000..f71050b --- /dev/null +++ b/registry/modules/specfact-govern-0.40.20.tar.gz.sha256 @@ -0,0 +1 @@ +8f58e1c0194f4915301d7946e3aed65f521d1f03a011287267f40244f45eaf89 diff --git a/registry/modules/specfact-project-0.41.3.tar.gz b/registry/modules/specfact-project-0.41.3.tar.gz new file mode 100644 index 0000000..cc53b32 Binary files /dev/null and b/registry/modules/specfact-project-0.41.3.tar.gz differ diff --git a/registry/modules/specfact-project-0.41.3.tar.gz.sha256 b/registry/modules/specfact-project-0.41.3.tar.gz.sha256 new file mode 100644 index 0000000..fc912ad --- /dev/null +++ b/registry/modules/specfact-project-0.41.3.tar.gz.sha256 @@ -0,0 +1 @@ +a3df973c103e0708bef7a6ad23ead9b45e3354ba2ecb878f4d64e753e163a817 diff --git a/registry/modules/specfact-spec-0.40.17.tar.gz b/registry/modules/specfact-spec-0.40.17.tar.gz new file mode 100644 index 0000000..0187e53 Binary files /dev/null and b/registry/modules/specfact-spec-0.40.17.tar.gz differ diff --git a/registry/modules/specfact-spec-0.40.17.tar.gz.sha256 b/registry/modules/specfact-spec-0.40.17.tar.gz.sha256 new file mode 100644 index 0000000..b4b0b19 --- /dev/null +++ b/registry/modules/specfact-spec-0.40.17.tar.gz.sha256 @@ -0,0 +1 @@ +a793088c0f9b4958a9dc939b51d004c8257b53fb584218b82beb0c7e0f39d848 diff --git a/registry/signatures/specfact-code-review-0.46.1.tar.sig b/registry/signatures/specfact-code-review-0.46.1.tar.sig new file mode 100644 index 0000000..c560490 --- /dev/null +++ b/registry/signatures/specfact-code-review-0.46.1.tar.sig @@ -0,0 +1 @@ +ulRchbUE1q1JhCuTRUPUolICZMiWDBCQgNmNt4YNbtyRysXMhiIcge/KJ1X1VqKtzWu4lzvEDf0OInPJ2lRxBA== diff --git a/scripts/check-docs-commands.py b/scripts/check-docs-commands.py index 8cb7741..ef7cc07 100755 --- a/scripts/check-docs-commands.py +++ b/scripts/check-docs-commands.py @@ -18,7 +18,16 @@ REPO_ROOT = Path(__file__).resolve().parents[1] DOCS_ROOT = REPO_ROOT / "docs" CORE_DOCS_HOST = "docs.specfact.io" -ALLOWED_CORE_DOCS_ROUTES = frozenset({"/", "/reference/documentation-url-contract/"}) +ALLOWED_CORE_DOCS_ROUTES = frozenset( + { + "/", + "/reference/documentation-url-contract/", + # Core-owned reference / guides linked from modules docs (see specfact-cli docs/ permalinks) + "/core-cli/debug-logging/", + "/reference/directory-structure/", + "/reference/feature-keys/", + } +) CORE_COMMAND_PREFIXES = frozenset( { ("specfact",), diff --git a/tests/unit/test_check_docs_commands_script.py b/tests/unit/test_check_docs_commands_script.py index 04dfa0b..0b4de63 100644 --- a/tests/unit/test_check_docs_commands_script.py +++ b/tests/unit/test_check_docs_commands_script.py @@ -115,6 +115,23 @@ def test_validate_core_docs_links_rejects_unknown_route(tmp_path: Path) -> None: assert "missing/page" in findings[0].message +def test_validate_core_docs_links_allows_core_handoff_routes(tmp_path: Path) -> None: + """Handoff URLs used in modules docs must stay in ALLOWED_CORE_DOCS_ROUTES (see scripts/check-docs-commands.py).""" + script = _load_script() + doc_path = tmp_path / "handoff.md" + doc_path.write_text( + "[Debug](https://docs.specfact.io/core-cli/debug-logging/)\n" + "[Debug anchor](https://docs.specfact.io/core-cli/debug-logging/#examining-ado-api-errors)\n" + "[Directory](https://docs.specfact.io/reference/directory-structure/)\n" + "[Feature keys](https://docs.specfact.io/reference/feature-keys/)\n", + encoding="utf-8", + ) + + findings = _script_attr(script, "_validate_core_docs_links")({doc_path: doc_path.read_text(encoding="utf-8")}) + + assert not findings + + def test_docs_review_workflow_runs_docs_command_validation() -> None: workflow = (REPO_ROOT / ".github" / "workflows" / "docs-review.yml").read_text(encoding="utf-8") diff --git a/tests/unit/test_registry_manifest_bundle_dependencies.py b/tests/unit/test_registry_manifest_bundle_dependencies.py new file mode 100644 index 0000000..9d0721f --- /dev/null +++ b/tests/unit/test_registry_manifest_bundle_dependencies.py @@ -0,0 +1,83 @@ +"""Registry `bundle_dependencies` must match each package `module-package.yaml`.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[2] + + +def _registry_modules() -> list[dict[str, object]]: + data = json.loads((ROOT / "registry" / "index.json").read_text(encoding="utf-8")) + modules = data.get("modules") + assert isinstance(modules, list) + return [m for m in modules if isinstance(m, dict)] + + +def _manifest_bundle_dependencies(module_id: str) -> list[str] | None: + """Return bundle_dependencies from packages//module-package.yaml for nold-ai/.""" + prefix = "nold-ai/" + if not module_id.startswith(prefix): + return None + bundle = module_id[len(prefix) :] + manifest = ROOT / "packages" / bundle / "module-package.yaml" + if not manifest.exists(): + return None + raw = yaml.safe_load(manifest.read_text(encoding="utf-8")) + if not isinstance(raw, dict): + return None + deps = raw.get("bundle_dependencies") + if deps is None: + return [] + assert isinstance(deps, list) + return [str(x) for x in deps] + + +def test_registry_bundle_dependencies_match_manifests() -> None: + for entry in _registry_modules(): + module_id = str(entry.get("id") or "").strip() + if not module_id: + continue + manifest_deps = _manifest_bundle_dependencies(module_id) + if manifest_deps is None: + continue + reg_deps = entry.get("bundle_dependencies") + assert isinstance(reg_deps, list), f"{module_id}: registry bundle_dependencies must be a list" + assert [str(x) for x in reg_deps] == manifest_deps, ( + f"{module_id}: registry bundle_dependencies {reg_deps!r} must match module-package.yaml {manifest_deps!r}" + ) + + +def test_official_bundle_dependency_graph_is_acyclic() -> None: + """Declared peer dependencies among official modules must not form a cycle.""" + edges: dict[str, list[str]] = {} + for entry in _registry_modules(): + mid = str(entry.get("id") or "").strip() + if not mid.startswith("nold-ai/"): + continue + deps = entry.get("bundle_dependencies") + if not isinstance(deps, list): + continue + edges[mid] = [str(d) for d in deps if str(d).startswith("nold-ai/")] + + visiting: set[str] = set() + visited: set[str] = set() + + def visit(node: str) -> None: + if node in visited: + return + if node in visiting: + msg = f"cycle detected involving {node}" + raise AssertionError(msg) + visiting.add(node) + for succ in edges.get(node, []): + visit(succ) + visiting.remove(node) + visited.add(node) + + for n in edges: + visit(n) diff --git a/tests/unit/test_validate_repo_manifests_bundle_deps.py b/tests/unit/test_validate_repo_manifests_bundle_deps.py new file mode 100644 index 0000000..4eb22ce --- /dev/null +++ b/tests/unit/test_validate_repo_manifests_bundle_deps.py @@ -0,0 +1,68 @@ +"""Tests for bundle_dependencies → registry index validation.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + + +def _load_validate_repo_module(): + """Load tools/validate_repo_manifests.py with a unique module name. + + Avoids ``sys.modules['validate_repo_manifests']`` collisions if another import + path cached an older copy of the module during the same pytest session. + """ + root = Path(__file__).resolve().parents[2] + path = root / "tools" / "validate_repo_manifests.py" + name = "specfact_cli_modules_validate_repo_manifests" + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def test_validate_manifest_bundle_dependency_refs_flags_dangling_id(tmp_path: Path) -> None: + v = _load_validate_repo_module() + registry_path = tmp_path / "index.json" + registry_path.write_text( + json.dumps({"modules": [{"id": "nold-ai/specfact-project", "latest_version": "0.1.0"}]}), + encoding="utf-8", + ) + registry_ids = v.registry_module_ids(registry_path) + manifest = tmp_path / "module-package.yaml" + manifest.write_text( + "name: nold-ai/specfact-code-review\n" + "bundle_dependencies:\n" + " - nold-ai/specfact-project\n" + " - nold-ai/specfact-not-in-registry\n", + encoding="utf-8", + ) + errors = v.validate_manifest_bundle_dependency_refs(manifest, registry_ids) + assert len(errors) == 1 + assert "specfact-not-in-registry" in errors[0] + assert str(manifest) in errors[0] + + +def test_validate_manifest_bundle_dependency_refs_ok_when_all_present(tmp_path: Path) -> None: + v = _load_validate_repo_module() + registry_path = tmp_path / "index.json" + registry_path.write_text( + json.dumps( + { + "modules": [ + {"id": "nold-ai/specfact-project", "latest_version": "0.1.0"}, + {"id": "nold-ai/specfact-codebase", "latest_version": "0.1.0"}, + ] + } + ), + encoding="utf-8", + ) + registry_ids = v.registry_module_ids(registry_path) + manifest = tmp_path / "module-package.yaml" + manifest.write_text( + "name: nold-ai/specfact-code-review\nbundle_dependencies:\n - nold-ai/specfact-codebase\n", + encoding="utf-8", + ) + assert v.validate_manifest_bundle_dependency_refs(manifest, registry_ids) == [] diff --git a/tools/validate_repo_manifests.py b/tools/validate_repo_manifests.py index a988fab..c873708 100755 --- a/tools/validate_repo_manifests.py +++ b/tools/validate_repo_manifests.py @@ -7,6 +7,8 @@ import re from pathlib import Path +import yaml + ROOT = Path(__file__).resolve().parent.parent REQUIRED_KEYS = {"name", "version", "tier", "publisher", "description", "bundle_group_command"} @@ -35,14 +37,58 @@ def _validate_registry(path: Path) -> list[str]: return [] +def registry_module_ids(registry_path: Path) -> set[str]: + data = json.loads(registry_path.read_text(encoding="utf-8")) + modules = data.get("modules") + if not isinstance(modules, list): + return set() + return {str(m["id"]).strip() for m in modules if isinstance(m, dict) and str(m.get("id") or "").strip()} + + +def validate_manifest_bundle_dependency_refs(manifest_path: Path, registry_ids: set[str]) -> list[str]: + """Ensure each bundle_dependencies entry targets a module id present in registry/index.json.""" + errors: list[str] = [] + try: + raw = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc: + return [f"{manifest_path}: cannot parse YAML for bundle_dependencies check ({exc})"] + if not isinstance(raw, dict): + return [] + deps = raw.get("bundle_dependencies") + if deps is None: + return [] + if not isinstance(deps, list): + return [f"{manifest_path}: bundle_dependencies must be a list when present"] + for dep in deps: + dep_id = str(dep).strip() + if not dep_id: + continue + if dep_id not in registry_ids: + errors.append( + f"{manifest_path}: bundle_dependencies references unknown module id {dep_id!r} " + f"(no matching entry in registry/index.json)" + ) + return errors + + def main() -> int: manifest_paths = sorted(ROOT.glob("packages/*/module-package.yaml")) errors: list[str] = [] + registry_path = ROOT / "registry" / "index.json" + errors.extend(_validate_registry(registry_path)) + + registry_ids: set[str] | None = None + if not errors: + try: + registry_ids = registry_module_ids(registry_path) + except (json.JSONDecodeError, OSError, TypeError, KeyError) as exc: + errors.append(f"{registry_path}: cannot load module ids ({exc})") + for manifest in manifest_paths: errors.extend(_validate_yaml(manifest)) - - errors.extend(_validate_registry(ROOT / "registry" / "index.json")) + if registry_ids is not None: + errors.extend(validate_manifest_bundle_dependency_refs(manifest, registry_ids)) if errors: print("Manifest/registry validation failed:")