specfact-cli 0.46.4__tar.gz → 0.46.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/PKG-INFO +1 -1
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/pyproject.toml +2 -1
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/__init__.py +1 -1
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/__init__.py +1 -1
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/code_analyzer.py +20 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/cli.py +23 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/module-package.yaml +3 -3
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/src/commands.py +33 -13
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_registry/module-package.yaml +3 -3
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_registry/src/commands.py +72 -7
- specfact_cli-0.46.9/src/specfact_cli/registry/module_availability.py +214 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_discovery.py +104 -33
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_packages.py +22 -5
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/optional_deps.py +7 -1
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/.gitignore +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/LICENSE +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/README.md +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/bundled-module-registry/index.json +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/keys/README.md +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/keys/module-signing-public.pem +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/persona/architect.md.j2 +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/persona/developer.md.j2 +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/persona/product-owner.md.j2 +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/policies/kanban.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/policies/mixed.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/policies/safe.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/policies/scrum.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/__main__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/ado.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/backlog_base.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/base.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/github.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/openspec.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/openspec_parser.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/registry.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/speckit.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/analyze_agent.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/adapters/base.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/converter.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/filters.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/base.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/_bundle_shim.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/analyze.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/contract_cmd.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/drift.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/enforce.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/generate.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/import_cmd.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/init.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/migrate.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/plan.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/project_cmd.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/repro.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/sdd.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/spec.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/sync.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/update.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/validate.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/bundle_factory.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/logger_setup.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/contracts/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/contracts/crosshair_props.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/contracts/module_interface.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/contract_generator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/openapi_extractor.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/persona_exporter.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/task_generator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/test_to_openapi.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/codebase_group.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/govern_group.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/member_group.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/project_group.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/spec_group.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/importers/speckit_converter.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/integrations/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/integrations/specmatic.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/merge/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/merge/resolver.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/migrations/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/migrations/plan_migrator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/backlog_item.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/bridge.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/capabilities.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/change.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/contract.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/dor_config.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/module_package.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/persona_template.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/project.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/quality.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/sdd.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/source_tracking.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/task.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/validation.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/_bundle_import.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/src/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/src/app.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/src/first_run_selection.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_io_shim.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_registry/src/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_registry/src/app.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/upgrade/module-package.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/upgrade/src/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/upgrade/src/app.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/upgrade/src/commands.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/parsers/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/parsers/persona_importer.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/alias_manager.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/bootstrap.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/bridge_registry.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/crypto_validator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/custom_registries.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/dependency_resolver.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/extension_registry.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/help_cache.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/marketplace_client.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/metadata.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_grouping.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_installer.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_lifecycle.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_security.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_state.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/registry.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/runtime.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_probe.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_openspec_md_parse.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_requirement_from_proposal.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_requirement_helpers.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_tasks_from_proposal.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_what_changes_format.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_write_openspec_from_proposal.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_watch.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/change_detector.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/code_to_spec.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/drift_detector.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/spec_to_code.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/spec_to_tests.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/telemetry.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/registry.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/specification_templates.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/auth_tokens.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/bundle_converters.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/bundle_loader.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/code_change_detector.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/content_sanitizer.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/context_detection.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/contract_predicates.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/enrichment_context.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/env_manager.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/icontract_helpers.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/ide_setup.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/incremental_check.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/metadata.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/performance.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/persona_ownership.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/progress.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/project_artifact_write.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/sdd_discovery.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/source_scanner.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/startup_checks.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/structured_io.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/suggestions.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/terminal.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validation/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validation/command_audit.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/agile_validation.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/cli_first_validator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/contract_validator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/repro_checker.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/schema.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/models.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/versioning/__init__.py +0 -0
- {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/versioning/analyzer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specfact-cli
|
|
3
|
-
Version: 0.46.
|
|
3
|
+
Version: 0.46.9
|
|
4
4
|
Summary: The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfact-cli"
|
|
7
|
-
version = "0.46.
|
|
7
|
+
version = "0.46.9"
|
|
8
8
|
description = "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -221,6 +221,7 @@ test-cov = "pytest --cov=src --cov-report=term-missing {args}"
|
|
|
221
221
|
type-check = "basedpyright --pythonpath $(python -c 'import sys; print(sys.executable)') {args}"
|
|
222
222
|
# basedpyright --level error: suppress warning noise in pre-commit (Block 1 runs `hatch run lint`).
|
|
223
223
|
lint = "ruff format . --check && basedpyright --level error --pythonpath $(python -c 'import sys; print(sys.executable)') && ruff check . && pylint src tests tools && python scripts/verify_safe_project_writes.py"
|
|
224
|
+
lint-changed = "python scripts/run_changed_lint.py {args}"
|
|
224
225
|
governance = "pylint src tests tools --reports=y --output-format=parseable"
|
|
225
226
|
format = "ruff check . --fix && ruff format ."
|
|
226
227
|
|
|
@@ -32,6 +32,24 @@ from specfact_cli.utils.feature_keys import to_classname_key, to_sequential_key
|
|
|
32
32
|
console = Console()
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _ensure_semgrep_runtime_dir(repo_path: Path, relative: str) -> str:
|
|
36
|
+
"""Create and return a stable repo-local runtime directory for Semgrep."""
|
|
37
|
+
path = (repo_path / relative).resolve()
|
|
38
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return str(path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_semgrep_env(repo_path: Path) -> dict[str, str]:
|
|
43
|
+
"""Build a repo-local Semgrep runtime env so CLI startup stays deterministic."""
|
|
44
|
+
env = dict(os.environ)
|
|
45
|
+
env["XDG_CONFIG_HOME"] = _ensure_semgrep_runtime_dir(repo_path, ".specfact/config")
|
|
46
|
+
env["XDG_CACHE_HOME"] = _ensure_semgrep_runtime_dir(repo_path, ".specfact/cache")
|
|
47
|
+
env["SEMGREP_VERSION_CACHE_PATH"] = _ensure_semgrep_runtime_dir(repo_path, ".specfact/cache/semgrep")
|
|
48
|
+
semgrep_log_dir = _ensure_semgrep_runtime_dir(repo_path, ".specfact/logs")
|
|
49
|
+
env["SEMGREP_LOG_FILE"] = str((Path(semgrep_log_dir) / "semgrep.log").resolve())
|
|
50
|
+
return env
|
|
51
|
+
|
|
52
|
+
|
|
35
53
|
@dataclass
|
|
36
54
|
class _SemgrepFeatureBuckets:
|
|
37
55
|
api_endpoints: list[str] = field(default_factory=list)
|
|
@@ -457,6 +475,7 @@ class CodeAnalyzer:
|
|
|
457
475
|
capture_output=True,
|
|
458
476
|
text=True,
|
|
459
477
|
timeout=5, # Increased timeout to 5s (Semgrep may need time to initialize)
|
|
478
|
+
env=_build_semgrep_env(self.repo_path),
|
|
460
479
|
)
|
|
461
480
|
return result.returncode == 0
|
|
462
481
|
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
@@ -566,6 +585,7 @@ class CodeAnalyzer:
|
|
|
566
585
|
capture_output=True,
|
|
567
586
|
text=True,
|
|
568
587
|
timeout=timeout,
|
|
588
|
+
env=_build_semgrep_env(self.repo_path),
|
|
569
589
|
)
|
|
570
590
|
|
|
571
591
|
# Semgrep may return non-zero for valid findings
|
|
@@ -66,6 +66,7 @@ from specfact_cli.registry import CommandRegistry
|
|
|
66
66
|
from specfact_cli.registry.alias_manager import resolve_command
|
|
67
67
|
from specfact_cli.registry.bootstrap import register_builtin_commands
|
|
68
68
|
from specfact_cli.registry.metadata import CommandMetadata
|
|
69
|
+
from specfact_cli.registry.module_availability import ModuleAvailabilityStatus, classify_module_availability
|
|
69
70
|
from specfact_cli.runtime import get_configured_console, init_debug_log_file, set_debug_mode
|
|
70
71
|
from specfact_cli.utils.progressive_disclosure import ProgressiveDisclosureGroup
|
|
71
72
|
from specfact_cli.utils.structured_io import StructuredFormat
|
|
@@ -124,6 +125,28 @@ def _print_missing_bundle_command_help(invoked: str) -> None:
|
|
|
124
125
|
module_id = _INVOKED_TO_MARKETPLACE_MODULE.get(invoked)
|
|
125
126
|
console = get_configured_console()
|
|
126
127
|
if module_id is not None:
|
|
128
|
+
availability = classify_module_availability(module_id=module_id, command_name=invoked)
|
|
129
|
+
if availability.status is ModuleAvailabilityStatus.DISABLED:
|
|
130
|
+
console.print(
|
|
131
|
+
f"[bold red]Module '{availability.module_id or module_id}' is installed but disabled.[/bold red]\n"
|
|
132
|
+
f"The [bold]{invoked}[/bold] command group is provided by that module. "
|
|
133
|
+
f"Enable with [bold]{availability.recovery_command}[/bold]."
|
|
134
|
+
)
|
|
135
|
+
return
|
|
136
|
+
if availability.status is ModuleAvailabilityStatus.SKIPPED:
|
|
137
|
+
console.print(
|
|
138
|
+
f"[bold red]Module '{availability.module_id or module_id}' is installed but skipped.[/bold red]\n"
|
|
139
|
+
f"Reason: {availability.reason}. "
|
|
140
|
+
"Inspect with [bold]specfact module list --show-origin[/bold]."
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
if availability.status is ModuleAvailabilityStatus.SHADOWED:
|
|
144
|
+
console.print(
|
|
145
|
+
f"[bold red]Module '{availability.module_id or module_id}' is shadowed in this workspace.[/bold red]\n"
|
|
146
|
+
f"Shadowed by: {availability.shadowed_by}. "
|
|
147
|
+
"Inspect with [bold]specfact module list --show-origin[/bold]."
|
|
148
|
+
)
|
|
149
|
+
return
|
|
127
150
|
console.print(
|
|
128
151
|
f"[bold red]Module '{module_id}' is not installed.[/bold red]\n"
|
|
129
152
|
f"The [bold]{invoked}[/bold] command group is provided by that module. "
|
{specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/module-package.yaml
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: init
|
|
2
|
-
version: 0.1.
|
|
2
|
+
version: 0.1.31
|
|
3
3
|
commands:
|
|
4
4
|
- init
|
|
5
5
|
category: core
|
|
@@ -17,5 +17,5 @@ publisher:
|
|
|
17
17
|
description: Initialize SpecFact workspace and bootstrap local configuration.
|
|
18
18
|
license: Apache-2.0
|
|
19
19
|
integrity:
|
|
20
|
-
checksum: sha256:
|
|
21
|
-
signature:
|
|
20
|
+
checksum: sha256:0f7bc54a823bea14033fcb143ecb6c83d2bca2b5da661f03a0b545100acebe5b
|
|
21
|
+
signature: dTatkqgBUtti4tL/pmcFBZY9bsJ61gY/V0lP9gZU8Y5W3YWK+wpgRx1oewlAmfKzkxca2NhalKcjLACQjTNvAA==
|
|
@@ -106,7 +106,7 @@ def _resolve_field_mapping_templates_dir(repo_path: Path) -> Path | None:
|
|
|
106
106
|
package_templates_dir = Path(str(templates_ref)).resolve()
|
|
107
107
|
if package_templates_dir.exists():
|
|
108
108
|
return package_templates_dir
|
|
109
|
-
except
|
|
109
|
+
except (ImportError, OSError, ValueError):
|
|
110
110
|
try:
|
|
111
111
|
import importlib.util
|
|
112
112
|
|
|
@@ -118,8 +118,8 @@ def _resolve_field_mapping_templates_dir(repo_path: Path) -> Path | None:
|
|
|
118
118
|
).resolve()
|
|
119
119
|
if package_templates_dir.exists():
|
|
120
120
|
return package_templates_dir
|
|
121
|
-
except
|
|
122
|
-
|
|
121
|
+
except (ImportError, OSError, ValueError):
|
|
122
|
+
return None
|
|
123
123
|
return None
|
|
124
124
|
|
|
125
125
|
|
|
@@ -434,7 +434,15 @@ def _is_valid_repo_path(repo: Path) -> bool:
|
|
|
434
434
|
|
|
435
435
|
|
|
436
436
|
@beartype
|
|
437
|
-
def
|
|
437
|
+
def _marketplace_ids_for_bundles(bundle_ids: list[str]) -> list[str]:
|
|
438
|
+
return [
|
|
439
|
+
first_run_selection.MARKETPLACE_ONLY_BUNDLES[bid]
|
|
440
|
+
for bid in bundle_ids
|
|
441
|
+
if bid in first_run_selection.MARKETPLACE_ONLY_BUNDLES
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _install_profile_bundles(profile: str, install_root: Path, non_interactive: bool) -> list[str]:
|
|
438
446
|
"""Resolve profile to bundle list and install via module installer."""
|
|
439
447
|
bundle_ids = first_run_selection.resolve_profile_bundles(profile)
|
|
440
448
|
if bundle_ids:
|
|
@@ -444,10 +452,11 @@ def _install_profile_bundles(profile: str, install_root: Path, non_interactive:
|
|
|
444
452
|
install_root,
|
|
445
453
|
non_interactive=non_interactive,
|
|
446
454
|
)
|
|
455
|
+
return _marketplace_ids_for_bundles(bundle_ids)
|
|
447
456
|
|
|
448
457
|
|
|
449
458
|
@beartype
|
|
450
|
-
def _install_bundle_list(install_arg: str, install_root: Path, non_interactive: bool) ->
|
|
459
|
+
def _install_bundle_list(install_arg: str, install_root: Path, non_interactive: bool) -> list[str]:
|
|
451
460
|
"""Parse comma-separated or 'all' and install bundles via module installer."""
|
|
452
461
|
bundle_ids = first_run_selection.resolve_install_bundles(install_arg)
|
|
453
462
|
if bundle_ids:
|
|
@@ -457,20 +466,32 @@ def _install_bundle_list(install_arg: str, install_root: Path, non_interactive:
|
|
|
457
466
|
install_root,
|
|
458
467
|
non_interactive=non_interactive,
|
|
459
468
|
)
|
|
469
|
+
return _marketplace_ids_for_bundles(bundle_ids)
|
|
460
470
|
|
|
461
471
|
|
|
462
|
-
def _apply_profile_or_install_bundles(profile: str | None, install: str | None) ->
|
|
472
|
+
def _apply_profile_or_install_bundles(profile: str | None, install: str | None) -> list[str]:
|
|
463
473
|
try:
|
|
464
474
|
non_interactive = is_non_interactive()
|
|
465
475
|
if profile is not None:
|
|
466
|
-
_install_profile_bundles(profile, INIT_USER_MODULES_ROOT, non_interactive=non_interactive)
|
|
467
|
-
|
|
468
|
-
_install_bundle_list(install or "", INIT_USER_MODULES_ROOT, non_interactive=non_interactive)
|
|
476
|
+
return _install_profile_bundles(profile, INIT_USER_MODULES_ROOT, non_interactive=non_interactive)
|
|
477
|
+
return _install_bundle_list(install or "", INIT_USER_MODULES_ROOT, non_interactive=non_interactive)
|
|
469
478
|
except ValueError as e:
|
|
470
479
|
console.print(f"[red]Error:[/red] {e}")
|
|
471
480
|
raise typer.Exit(1) from e
|
|
472
481
|
|
|
473
482
|
|
|
483
|
+
def _refresh_init_module_state(repo_path: Path, enabled_module_ids: list[str]) -> list[dict[str, Any]]:
|
|
484
|
+
modules_list = get_discovered_modules_for_state(
|
|
485
|
+
enable_ids=enabled_module_ids,
|
|
486
|
+
disable_ids=[],
|
|
487
|
+
base_path=repo_path,
|
|
488
|
+
preserve_existing=True,
|
|
489
|
+
)
|
|
490
|
+
if modules_list:
|
|
491
|
+
write_modules_state(modules_list)
|
|
492
|
+
return modules_list
|
|
493
|
+
|
|
494
|
+
|
|
474
495
|
def _run_interactive_first_run_install() -> None:
|
|
475
496
|
try:
|
|
476
497
|
bundle_ids = _interactive_first_run_bundle_selection()
|
|
@@ -702,8 +723,9 @@ def init(
|
|
|
702
723
|
|
|
703
724
|
repo_path = repo.resolve()
|
|
704
725
|
|
|
726
|
+
enabled_module_ids: list[str] = []
|
|
705
727
|
if profile is not None or install is not None:
|
|
706
|
-
_apply_profile_or_install_bundles(profile, install)
|
|
728
|
+
enabled_module_ids = _apply_profile_or_install_bundles(profile, install)
|
|
707
729
|
elif is_first_run(user_root=INIT_USER_MODULES_ROOT) and is_non_interactive():
|
|
708
730
|
console.print(
|
|
709
731
|
"[red]Error:[/red] In CI/CD (non-interactive) mode, first-run init requires "
|
|
@@ -718,9 +740,7 @@ def init(
|
|
|
718
740
|
_run_interactive_first_run_install()
|
|
719
741
|
|
|
720
742
|
_init_user_visible_step("[cyan]→[/cyan] Discovering installed modules and writing registry state…")
|
|
721
|
-
modules_list =
|
|
722
|
-
if modules_list:
|
|
723
|
-
write_modules_state(modules_list)
|
|
743
|
+
modules_list = _refresh_init_module_state(repo_path, enabled_module_ids)
|
|
724
744
|
|
|
725
745
|
_init_user_visible_step("[cyan]→[/cyan] Indexing CLI commands for help cache…")
|
|
726
746
|
run_discovery_and_write_cache(__version__)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: module-registry
|
|
2
|
-
version: 0.1.
|
|
2
|
+
version: 0.1.23
|
|
3
3
|
commands:
|
|
4
4
|
- module
|
|
5
5
|
category: core
|
|
@@ -17,5 +17,5 @@ publisher:
|
|
|
17
17
|
description: 'Manage modules: search, list, show, install, and upgrade.'
|
|
18
18
|
license: Apache-2.0
|
|
19
19
|
integrity:
|
|
20
|
-
checksum: sha256:
|
|
21
|
-
signature:
|
|
20
|
+
checksum: sha256:f500281d2249d712be23a1b25b5660374694dfa47634f60ae4378eb2cdb753ca
|
|
21
|
+
signature: cunuat95bD44IcNUBs35NTlHYOR8atydVz4ZyZTjIzwJL6Wz5YOfBYGq/WtgDnOFv3KplfvhRZbTo3CwmS/KBQ==
|
{specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_registry/src/commands.py
RENAMED
|
@@ -20,12 +20,14 @@ from packaging.version import InvalidVersion, Version
|
|
|
20
20
|
from rich.console import Console
|
|
21
21
|
from rich.table import Table
|
|
22
22
|
|
|
23
|
+
from specfact_cli import __version__
|
|
23
24
|
from specfact_cli.models.module_package import ModulePackageMetadata
|
|
24
25
|
from specfact_cli.modules import module_io_shim
|
|
25
26
|
from specfact_cli.registry.alias_manager import create_alias, list_aliases, remove_alias
|
|
26
27
|
from specfact_cli.registry.custom_registries import add_registry, fetch_all_indexes, list_registries, remove_registry
|
|
28
|
+
from specfact_cli.registry.help_cache import run_discovery_and_write_cache
|
|
27
29
|
from specfact_cli.registry.marketplace_client import fetch_registry_index
|
|
28
|
-
from specfact_cli.registry.module_discovery import discover_all_modules
|
|
30
|
+
from specfact_cli.registry.module_discovery import discover_all_modules, discover_all_modules_for_project
|
|
29
31
|
from specfact_cli.registry.module_installer import (
|
|
30
32
|
REGISTRY_ID_FILE,
|
|
31
33
|
USER_MODULES_ROOT,
|
|
@@ -42,7 +44,9 @@ from specfact_cli.registry.module_lifecycle import (
|
|
|
42
44
|
render_modules_table,
|
|
43
45
|
select_module_ids_interactive,
|
|
44
46
|
)
|
|
47
|
+
from specfact_cli.registry.module_packages import get_discovered_modules_for_state
|
|
45
48
|
from specfact_cli.registry.module_security import ensure_publisher_trusted, is_official_publisher
|
|
49
|
+
from specfact_cli.registry.module_state import read_modules_state, write_modules_state
|
|
46
50
|
from specfact_cli.registry.registry import CommandRegistry
|
|
47
51
|
from specfact_cli.runtime import is_non_interactive
|
|
48
52
|
|
|
@@ -178,15 +182,64 @@ def _resolve_install_target_root(scope_normalized: str, repo: Path | None) -> Pa
|
|
|
178
182
|
return USER_MODULES_ROOT if scope_normalized == "user" else repo_path / ".specfact" / "modules"
|
|
179
183
|
|
|
180
184
|
|
|
185
|
+
def _normalize_project_repo(repo: Path | None) -> Path | None:
|
|
186
|
+
"""Resolve a project-scoped repo argument to the nearest workspace root."""
|
|
187
|
+
if repo is None:
|
|
188
|
+
return None
|
|
189
|
+
repo_path = repo.resolve()
|
|
190
|
+
for candidate in [repo_path, *repo_path.parents]:
|
|
191
|
+
if (candidate / ".git").exists():
|
|
192
|
+
return candidate
|
|
193
|
+
return repo_path
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _read_installed_manifest_id(module_dir: Path, fallback_name: str) -> str:
|
|
197
|
+
manifest_path = module_dir / "module-package.yaml"
|
|
198
|
+
try:
|
|
199
|
+
raw = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
|
|
200
|
+
except (OSError, yaml.YAMLError):
|
|
201
|
+
return fallback_name
|
|
202
|
+
if isinstance(raw, dict):
|
|
203
|
+
manifest = cast(dict[str, Any], raw)
|
|
204
|
+
if manifest.get("name"):
|
|
205
|
+
return str(manifest["name"])
|
|
206
|
+
return fallback_name
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _enable_if_disabled(module_id: str, base_path: Path | None = None) -> bool:
|
|
210
|
+
state = read_modules_state()
|
|
211
|
+
if state.get(module_id, {}).get("enabled", True) is not False:
|
|
212
|
+
return False
|
|
213
|
+
modules = get_discovered_modules_for_state(
|
|
214
|
+
enable_ids=[module_id],
|
|
215
|
+
disable_ids=[],
|
|
216
|
+
base_path=base_path,
|
|
217
|
+
preserve_existing=True,
|
|
218
|
+
)
|
|
219
|
+
write_modules_state(modules)
|
|
220
|
+
run_discovery_and_write_cache(__version__)
|
|
221
|
+
return any(str(row.get("id", "")) == module_id and bool(row.get("enabled", True)) for row in modules)
|
|
222
|
+
|
|
223
|
+
|
|
181
224
|
def _install_skip_if_already_satisfied(
|
|
182
225
|
scope_normalized: str,
|
|
183
226
|
requested_name: str,
|
|
184
227
|
target_root: Path,
|
|
228
|
+
repo: Path | None,
|
|
185
229
|
reinstall: bool,
|
|
186
230
|
discovered_by_name: dict[str, Any],
|
|
187
231
|
) -> bool:
|
|
188
|
-
|
|
189
|
-
|
|
232
|
+
installed_dir = target_root / requested_name
|
|
233
|
+
if (installed_dir / "module-package.yaml").exists() and not reinstall:
|
|
234
|
+
module_id = _read_installed_manifest_id(installed_dir, requested_name)
|
|
235
|
+
enabled = _enable_if_disabled(module_id, base_path=repo if scope_normalized == "project" else None)
|
|
236
|
+
if enabled:
|
|
237
|
+
console.print(
|
|
238
|
+
f"[yellow]Module '{module_id}' is already installed in {target_root}; "
|
|
239
|
+
"enabled it in module state.[/yellow]"
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
console.print(f"[yellow]Module '{module_id}' is already installed in {target_root}.[/yellow]")
|
|
190
243
|
return True
|
|
191
244
|
skip_sources = {"builtin", "project", "user", "custom"}
|
|
192
245
|
if scope_normalized == "project":
|
|
@@ -195,9 +248,14 @@ def _install_skip_if_already_satisfied(
|
|
|
195
248
|
skip_sources.discard("project")
|
|
196
249
|
existing = discovered_by_name.get(requested_name)
|
|
197
250
|
if existing is not None and existing.source in skip_sources:
|
|
251
|
+
enabled = _enable_if_disabled(
|
|
252
|
+
existing.metadata.name,
|
|
253
|
+
base_path=repo if scope_normalized == "project" else None,
|
|
254
|
+
)
|
|
255
|
+
state_hint = " Enabled it in module state." if enabled else ""
|
|
198
256
|
console.print(
|
|
199
|
-
f"[yellow]Module '{
|
|
200
|
-
"No marketplace install needed.[/yellow]"
|
|
257
|
+
f"[yellow]Module '{existing.metadata.name}' is already available from source '{existing.source}'. "
|
|
258
|
+
f"No marketplace install needed.{state_hint}[/yellow]"
|
|
201
259
|
)
|
|
202
260
|
return True
|
|
203
261
|
return False
|
|
@@ -274,6 +332,7 @@ class _InstallOneParams:
|
|
|
274
332
|
scope_normalized: str
|
|
275
333
|
source_normalized: str
|
|
276
334
|
target_root: Path
|
|
335
|
+
repo: Path | None
|
|
277
336
|
version: str | None
|
|
278
337
|
reinstall: bool
|
|
279
338
|
trust_non_official: bool
|
|
@@ -289,6 +348,7 @@ def _install_one(module_id: str, params: _InstallOneParams) -> bool:
|
|
|
289
348
|
params.scope_normalized,
|
|
290
349
|
requested_name,
|
|
291
350
|
params.target_root,
|
|
351
|
+
params.repo,
|
|
292
352
|
params.reinstall,
|
|
293
353
|
params.discovered_by_name,
|
|
294
354
|
):
|
|
@@ -392,12 +452,17 @@ def _install_impl(module_ids: list[str], **kwargs: Any) -> None:
|
|
|
392
452
|
)
|
|
393
453
|
raise typer.Exit(1)
|
|
394
454
|
scope_normalized, source_normalized = _parse_install_scope_and_source(scope, source)
|
|
395
|
-
|
|
396
|
-
|
|
455
|
+
normalized_repo = _normalize_project_repo(repo) if scope_normalized == "project" else None
|
|
456
|
+
target_root = _resolve_install_target_root(scope_normalized, normalized_repo)
|
|
457
|
+
discovered = (
|
|
458
|
+
discover_all_modules_for_project(normalized_repo) if normalized_repo is not None else discover_all_modules()
|
|
459
|
+
)
|
|
460
|
+
discovered_by_name = {entry.metadata.name: entry for entry in discovered}
|
|
397
461
|
params = _InstallOneParams(
|
|
398
462
|
scope_normalized=scope_normalized,
|
|
399
463
|
source_normalized=source_normalized,
|
|
400
464
|
target_root=target_root,
|
|
465
|
+
repo=normalized_repo,
|
|
401
466
|
version=version,
|
|
402
467
|
reinstall=reinstall,
|
|
403
468
|
trust_non_official=trust_non_official,
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Metadata-only module availability classification for user-facing diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from beartype import beartype
|
|
10
|
+
from icontract import ensure, require
|
|
11
|
+
|
|
12
|
+
from specfact_cli import __version__ as cli_version
|
|
13
|
+
from specfact_cli.registry.module_discovery import DiscoveredModule, discover_all_modules_for_project_with_shadowed
|
|
14
|
+
from specfact_cli.registry.module_packages import (
|
|
15
|
+
_check_core_compatibility,
|
|
16
|
+
_validate_module_dependencies,
|
|
17
|
+
merge_module_state,
|
|
18
|
+
)
|
|
19
|
+
from specfact_cli.registry.module_state import read_modules_state
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ModuleAvailabilityStatus(StrEnum):
|
|
23
|
+
"""User-facing module availability states."""
|
|
24
|
+
|
|
25
|
+
ABSENT = "absent"
|
|
26
|
+
AVAILABLE = "available"
|
|
27
|
+
DISABLED = "disabled"
|
|
28
|
+
SKIPPED = "skipped"
|
|
29
|
+
SHADOWED = "shadowed"
|
|
30
|
+
AMBIGUOUS = "ambiguous"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ModuleAvailability:
|
|
35
|
+
"""Availability classification without importing module command code."""
|
|
36
|
+
|
|
37
|
+
status: ModuleAvailabilityStatus
|
|
38
|
+
module_id: str | None = None
|
|
39
|
+
source: str | None = None
|
|
40
|
+
package_dir: Path | None = None
|
|
41
|
+
reason: str = ""
|
|
42
|
+
recovery_command: str = ""
|
|
43
|
+
shadowed_by: Path | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _module_id_tail(module_id: str) -> str:
|
|
47
|
+
"""Return the final path segment of a module id."""
|
|
48
|
+
return module_id.rsplit("/", 1)[-1].strip()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _module_id_matches(requested: str | None, discovered_id: str) -> bool:
|
|
52
|
+
"""Return True when a requested module id refers to a discovered manifest id."""
|
|
53
|
+
if requested is None:
|
|
54
|
+
return False
|
|
55
|
+
requested_clean = requested.strip()
|
|
56
|
+
if "/" in requested_clean:
|
|
57
|
+
return requested_clean == discovered_id
|
|
58
|
+
return requested_clean == discovered_id or _module_id_tail(requested_clean) == _module_id_tail(discovered_id)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _entry_matches(entry: DiscoveredModule, *, module_id: str | None, command_name: str | None) -> bool:
|
|
62
|
+
meta = entry.metadata
|
|
63
|
+
if _module_id_matches(module_id, meta.name):
|
|
64
|
+
return True
|
|
65
|
+
if command_name is None:
|
|
66
|
+
return False
|
|
67
|
+
return command_name in set(meta.commands) or getattr(meta, "bundle_group_command", None) == command_name
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _availability_matches(
|
|
71
|
+
discovered: list[DiscoveredModule],
|
|
72
|
+
*,
|
|
73
|
+
module_id: str | None,
|
|
74
|
+
command_name: str | None,
|
|
75
|
+
) -> list[DiscoveredModule]:
|
|
76
|
+
module_matches = [entry for entry in discovered if _module_id_matches(module_id, entry.metadata.name)]
|
|
77
|
+
if module_matches:
|
|
78
|
+
return module_matches
|
|
79
|
+
if module_id is not None and "/" in module_id.strip():
|
|
80
|
+
return []
|
|
81
|
+
return [entry for entry in discovered if _entry_matches(entry, module_id=module_id, command_name=command_name)]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _ambiguous_bare_module_id_match(
|
|
85
|
+
module_id: str | None,
|
|
86
|
+
matches: list[DiscoveredModule],
|
|
87
|
+
) -> bool:
|
|
88
|
+
if module_id is None:
|
|
89
|
+
return False
|
|
90
|
+
requested = module_id.strip()
|
|
91
|
+
if not requested or "/" in requested:
|
|
92
|
+
return False
|
|
93
|
+
matched_ids = {entry.metadata.name for entry in matches}
|
|
94
|
+
return len(matched_ids) > 1
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _recovery_command(status: ModuleAvailabilityStatus, module_id: str) -> str:
|
|
98
|
+
if status is ModuleAvailabilityStatus.DISABLED:
|
|
99
|
+
return f"specfact module enable {module_id}"
|
|
100
|
+
if status is ModuleAvailabilityStatus.ABSENT:
|
|
101
|
+
return f"specfact module install {module_id}"
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _skip_reason(entry: DiscoveredModule, enabled_map: dict[str, bool]) -> str:
|
|
106
|
+
meta = entry.metadata
|
|
107
|
+
if not _check_core_compatibility(meta, cli_version):
|
|
108
|
+
return f"requires {meta.core_compatibility}, cli is {cli_version}"
|
|
109
|
+
deps_ok, missing = _validate_module_dependencies(meta, enabled_map)
|
|
110
|
+
if not deps_ok:
|
|
111
|
+
return f"missing dependencies: {', '.join(missing)}"
|
|
112
|
+
return ""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _absent_availability(module_id: str | None, requested_id: str) -> ModuleAvailability:
|
|
116
|
+
return ModuleAvailability(
|
|
117
|
+
status=ModuleAvailabilityStatus.ABSENT,
|
|
118
|
+
module_id=module_id,
|
|
119
|
+
reason="not installed",
|
|
120
|
+
recovery_command=_recovery_command(ModuleAvailabilityStatus.ABSENT, requested_id) if requested_id else "",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _ambiguous_availability(module_id: str) -> ModuleAvailability:
|
|
125
|
+
return ModuleAvailability(
|
|
126
|
+
status=ModuleAvailabilityStatus.AMBIGUOUS,
|
|
127
|
+
module_id=module_id,
|
|
128
|
+
reason="multiple installed modules share this short id; use namespace/name",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _shadowed_duplicate(primary: DiscoveredModule, matches: list[DiscoveredModule]) -> DiscoveredModule | None:
|
|
133
|
+
duplicate = next((entry for entry in matches[1:] if entry.metadata.name == primary.metadata.name), None)
|
|
134
|
+
if duplicate is None:
|
|
135
|
+
return None
|
|
136
|
+
if primary.source == "project" and duplicate.source in {"user", "marketplace"}:
|
|
137
|
+
return duplicate
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _shadowed_availability(primary: DiscoveredModule, duplicate: DiscoveredModule) -> ModuleAvailability:
|
|
142
|
+
return ModuleAvailability(
|
|
143
|
+
status=ModuleAvailabilityStatus.SHADOWED,
|
|
144
|
+
module_id=duplicate.metadata.name,
|
|
145
|
+
source=duplicate.source,
|
|
146
|
+
package_dir=duplicate.package_dir,
|
|
147
|
+
reason=f"shadowed by {primary.source} scope",
|
|
148
|
+
shadowed_by=primary.package_dir,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _disabled_availability(primary: DiscoveredModule) -> ModuleAvailability:
|
|
153
|
+
module_name = primary.metadata.name
|
|
154
|
+
return ModuleAvailability(
|
|
155
|
+
status=ModuleAvailabilityStatus.DISABLED,
|
|
156
|
+
module_id=module_name,
|
|
157
|
+
source=primary.source,
|
|
158
|
+
package_dir=primary.package_dir,
|
|
159
|
+
reason="disabled in modules.json",
|
|
160
|
+
recovery_command=_recovery_command(ModuleAvailabilityStatus.DISABLED, module_name),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _available_or_skipped_availability(
|
|
165
|
+
primary: DiscoveredModule,
|
|
166
|
+
enabled_map: dict[str, bool],
|
|
167
|
+
) -> ModuleAvailability:
|
|
168
|
+
reason = _skip_reason(primary, enabled_map)
|
|
169
|
+
if reason:
|
|
170
|
+
return ModuleAvailability(
|
|
171
|
+
status=ModuleAvailabilityStatus.SKIPPED,
|
|
172
|
+
module_id=primary.metadata.name,
|
|
173
|
+
source=primary.source,
|
|
174
|
+
package_dir=primary.package_dir,
|
|
175
|
+
reason=reason,
|
|
176
|
+
)
|
|
177
|
+
return ModuleAvailability(
|
|
178
|
+
status=ModuleAvailabilityStatus.AVAILABLE,
|
|
179
|
+
module_id=primary.metadata.name,
|
|
180
|
+
source=primary.source,
|
|
181
|
+
package_dir=primary.package_dir,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@beartype
|
|
186
|
+
@require(
|
|
187
|
+
lambda module_id, command_name, base_path: bool(module_id or command_name), "module_id or command_name required"
|
|
188
|
+
)
|
|
189
|
+
@ensure(lambda result: isinstance(result, ModuleAvailability), "must return module availability")
|
|
190
|
+
def classify_module_availability(
|
|
191
|
+
*,
|
|
192
|
+
module_id: str | None = None,
|
|
193
|
+
command_name: str | None = None,
|
|
194
|
+
base_path: Path | None = None,
|
|
195
|
+
) -> ModuleAvailability:
|
|
196
|
+
"""Classify module availability using manifests and modules.json only."""
|
|
197
|
+
discovered = discover_all_modules_for_project_with_shadowed(base_path)
|
|
198
|
+
matches = _availability_matches(discovered, module_id=module_id, command_name=command_name)
|
|
199
|
+
requested_id = module_id or command_name or ""
|
|
200
|
+
if not matches:
|
|
201
|
+
return _absent_availability(module_id, requested_id)
|
|
202
|
+
if _ambiguous_bare_module_id_match(module_id, matches):
|
|
203
|
+
return _ambiguous_availability(requested_id.strip())
|
|
204
|
+
|
|
205
|
+
discovered_list = [(entry.metadata.name, entry.metadata.version) for entry in discovered]
|
|
206
|
+
enabled_map = merge_module_state(discovered_list, read_modules_state(), [], [])
|
|
207
|
+
primary = matches[0]
|
|
208
|
+
module_name = primary.metadata.name
|
|
209
|
+
duplicate = _shadowed_duplicate(primary, matches)
|
|
210
|
+
if duplicate is not None:
|
|
211
|
+
return _shadowed_availability(primary, duplicate)
|
|
212
|
+
if not enabled_map.get(module_name, True):
|
|
213
|
+
return _disabled_availability(primary)
|
|
214
|
+
return _available_or_skipped_availability(primary, enabled_map)
|