@jaguilar87/gaia-ops 4.4.0 → 4.7.2
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +12 -3
- package/ARCHITECTURE.md +9 -8
- package/CHANGELOG.md +34 -0
- package/README.md +43 -11
- package/agents/terraform-architect.md +1 -1
- package/bin/README.md +2 -2
- package/bin/gaia-doctor.js +18 -5
- package/bin/gaia-history.js +0 -1
- package/bin/gaia-metrics.js +2 -2
- package/bin/gaia-scan.py +23 -1
- package/bin/gaia-update.js +346 -54
- package/bin/pre-publish-validate.js +33 -10
- package/commands/gaia.md +37 -0
- package/config/README.md +3 -9
- package/config/context-contracts.json +47 -15
- package/config/surface-routing.json +9 -1
- package/dist/gaia-ops/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
- package/dist/gaia-ops/agents/devops-developer.md +57 -0
- package/dist/gaia-ops/agents/gaia-system.md +58 -0
- package/dist/gaia-ops/agents/gitops-operator.md +60 -0
- package/dist/gaia-ops/agents/speckit-planner.md +71 -0
- package/dist/gaia-ops/agents/terraform-architect.md +60 -0
- package/dist/gaia-ops/commands/gaia.md +37 -0
- package/dist/gaia-ops/config/README.md +58 -0
- package/dist/gaia-ops/config/cloud/aws.json +140 -0
- package/dist/gaia-ops/config/cloud/gcp.json +145 -0
- package/dist/gaia-ops/config/context-contracts.json +131 -0
- package/dist/gaia-ops/config/git_standards.json +72 -0
- package/dist/gaia-ops/config/surface-routing.json +197 -0
- package/dist/gaia-ops/config/universal-rules.json +10 -0
- package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-ops/hooks/adapters/base.py +219 -0
- package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
- package/dist/gaia-ops/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-ops/hooks/adapters/types.py +194 -0
- package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
- package/dist/gaia-ops/hooks/hooks.json +126 -0
- package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
- package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-ops/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-ops/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
- package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-ops/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-ops/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-ops/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-ops/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-ops/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-ops/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-ops/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-ops/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-ops/hooks/post_compact.py +43 -0
- package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
- package/dist/gaia-ops/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-ops/hooks/session_start.py +69 -0
- package/dist/gaia-ops/hooks/stop_hook.py +69 -0
- package/dist/gaia-ops/hooks/subagent_start.py +71 -0
- package/dist/gaia-ops/hooks/subagent_stop.py +288 -0
- package/dist/gaia-ops/hooks/task_completed.py +70 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-ops/settings.json +72 -0
- package/dist/gaia-ops/skills/README.md +109 -0
- package/dist/gaia-ops/skills/agent-protocol/SKILL.md +105 -0
- package/dist/gaia-ops/skills/agent-protocol/examples.md +170 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +53 -0
- package/dist/gaia-ops/skills/approval/SKILL.md +85 -0
- package/dist/gaia-ops/skills/approval/examples.md +140 -0
- package/dist/gaia-ops/skills/approval/reference.md +57 -0
- package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
- package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
- package/dist/gaia-ops/skills/context-updater/SKILL.md +76 -0
- package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
- package/dist/gaia-ops/skills/developer-patterns/SKILL.md +93 -0
- package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
- package/dist/gaia-ops/skills/execution/SKILL.md +66 -0
- package/dist/gaia-ops/skills/fast-queries/SKILL.md +47 -0
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +92 -0
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +22 -0
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +48 -0
- package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +73 -0
- package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
- package/dist/gaia-ops/skills/investigation/SKILL.md +77 -0
- package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +64 -0
- package/dist/gaia-ops/skills/reference.md +134 -0
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +61 -0
- package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +119 -0
- package/dist/gaia-ops/skills/specification/SKILL.md +186 -0
- package/dist/gaia-ops/skills/speckit-workflow/SKILL.md +165 -0
- package/dist/gaia-ops/skills/speckit-workflow/reference.md +117 -0
- package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +63 -0
- package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
- package/dist/gaia-ops/speckit/README.md +516 -0
- package/dist/gaia-ops/speckit/scripts/.gitkeep +0 -0
- package/dist/gaia-ops/speckit/templates/adr-template.md +118 -0
- package/dist/gaia-ops/speckit/templates/agent-file-template.md +23 -0
- package/dist/gaia-ops/speckit/templates/plan-template.md +227 -0
- package/dist/gaia-ops/speckit/templates/spec-template.md +140 -0
- package/dist/gaia-ops/speckit/templates/tasks-template.md +257 -0
- package/dist/gaia-ops/tools/context/README.md +132 -0
- package/dist/gaia-ops/tools/context/__init__.py +42 -0
- package/dist/gaia-ops/tools/context/_paths.py +20 -0
- package/dist/gaia-ops/tools/context/context_provider.py +476 -0
- package/dist/gaia-ops/tools/context/context_section_reader.py +330 -0
- package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
- package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
- package/dist/gaia-ops/tools/context/surface_router.py +278 -0
- package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
- package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
- package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
- package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
- package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
- package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
- package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
- package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
- package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
- package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +262 -0
- package/dist/gaia-ops/tools/memory/README.md +0 -0
- package/dist/gaia-ops/tools/memory/__init__.py +20 -0
- package/dist/gaia-ops/tools/memory/episodic.py +1196 -0
- package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
- package/dist/gaia-ops/tools/review/__init__.py +1 -0
- package/dist/gaia-ops/tools/review/review_engine.py +157 -0
- package/dist/gaia-ops/tools/scan/__init__.py +35 -0
- package/dist/gaia-ops/tools/scan/config.py +247 -0
- package/dist/gaia-ops/tools/scan/merge.py +212 -0
- package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
- package/dist/gaia-ops/tools/scan/registry.py +127 -0
- package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
- package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
- package/dist/gaia-ops/tools/scan/scanners/environment.py +324 -0
- package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
- package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
- package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
- package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
- package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
- package/dist/gaia-ops/tools/scan/setup.py +753 -0
- package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
- package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
- package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
- package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
- package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
- package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
- package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
- package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
- package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
- package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
- package/dist/gaia-ops/tools/scan/ui.py +624 -0
- package/dist/gaia-ops/tools/scan/verify.py +266 -0
- package/dist/gaia-ops/tools/scan/walk.py +118 -0
- package/dist/gaia-ops/tools/scan/workspace.py +85 -0
- package/dist/gaia-ops/tools/validation/README.md +244 -0
- package/dist/gaia-ops/tools/validation/__init__.py +17 -0
- package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
- package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
- package/dist/gaia-security/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-security/config/universal-rules.json +10 -0
- package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-security/hooks/adapters/base.py +219 -0
- package/dist/gaia-security/hooks/adapters/channel.py +17 -0
- package/dist/gaia-security/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-security/hooks/adapters/types.py +194 -0
- package/dist/gaia-security/hooks/adapters/utils.py +25 -0
- package/dist/gaia-security/hooks/hooks.json +57 -0
- package/dist/gaia-security/hooks/modules/__init__.py +15 -0
- package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-security/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-security/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-security/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-security/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-security/hooks/modules/core/state.py +179 -0
- package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-security/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-security/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-security/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-security/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-security/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-security/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-security/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-security/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-security/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-security/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-security/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-security/hooks/post_tool_use.py +54 -0
- package/dist/gaia-security/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-security/hooks/session_start.py +69 -0
- package/dist/gaia-security/hooks/stop_hook.py +69 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-security/settings.json +58 -0
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +8 -6
- package/hooks/adapters/channel.py +0 -25
- package/hooks/adapters/claude_code.py +364 -125
- package/hooks/elicitation_result.py +132 -0
- package/hooks/hooks.json +10 -1
- package/hooks/modules/README.md +3 -2
- package/hooks/modules/agents/contract_validator.py +3 -51
- package/hooks/modules/agents/response_contract.py +4 -8
- package/hooks/modules/agents/transcript_reader.py +4 -5
- package/hooks/modules/audit/__init__.py +4 -6
- package/hooks/modules/audit/event_detector.py +0 -2
- package/hooks/modules/audit/metrics.py +108 -187
- package/hooks/modules/audit/workflow_auditor.py +0 -4
- package/hooks/modules/audit/workflow_recorder.py +0 -5
- package/hooks/modules/context/compact_context_builder.py +1 -0
- package/hooks/modules/context/context_cache.py +129 -0
- package/hooks/modules/context/context_injector.py +18 -40
- package/hooks/modules/context/context_writer.py +1 -25
- package/hooks/modules/context/contracts_loader.py +7 -10
- package/hooks/modules/core/hook_entry.py +1 -0
- package/hooks/modules/core/paths.py +12 -13
- package/hooks/modules/core/plugin_mode.py +74 -4
- package/hooks/modules/core/plugin_setup.py +395 -23
- package/hooks/modules/events/__init__.py +1 -0
- package/hooks/modules/events/event_writer.py +210 -0
- package/hooks/modules/identity/ops_identity.py +18 -27
- package/hooks/modules/memory/episode_writer.py +1 -6
- package/hooks/modules/orchestrator/__init__.py +1 -0
- package/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/hooks/modules/security/__init__.py +2 -4
- package/hooks/modules/security/approval_constants.py +5 -1
- package/hooks/modules/security/approval_grants.py +189 -6
- package/hooks/modules/security/approval_messages.py +9 -21
- package/hooks/modules/security/blocked_commands.py +98 -34
- package/hooks/modules/security/command_semantics.py +0 -4
- package/hooks/modules/security/gitops_validator.py +1 -11
- package/hooks/modules/security/mutative_verbs.py +179 -38
- package/hooks/modules/security/tiers.py +1 -19
- package/hooks/modules/session/session_event_injector.py +1 -25
- package/hooks/modules/tools/bash_validator.py +310 -94
- package/hooks/modules/tools/shell_parser.py +0 -1
- package/hooks/modules/tools/task_validator.py +9 -29
- package/hooks/post_tool_use.py +0 -72
- package/hooks/pre_tool_use.py +42 -102
- package/hooks/session_start.py +4 -2
- package/hooks/subagent_start.py +6 -2
- package/hooks/subagent_stop.py +1 -13
- package/hooks/user_prompt_submit.py +119 -37
- package/index.js +1 -1
- package/package.json +5 -3
- package/skills/README.md +3 -5
- package/skills/agent-protocol/SKILL.md +17 -16
- package/skills/agent-protocol/examples.md +6 -6
- package/skills/agent-response/SKILL.md +11 -14
- package/skills/approval/SKILL.md +28 -13
- package/skills/approval/reference.md +2 -2
- package/skills/execution/SKILL.md +1 -1
- package/skills/gaia-patterns/SKILL.md +2 -3
- package/skills/orchestrator-approval/SKILL.md +22 -50
- package/skills/security-tiers/SKILL.md +1 -1
- package/templates/README.md +9 -9
- package/templates/managed-settings.template.json +43 -0
- package/tools/gaia_simulator/runner.py +34 -1
- package/tools/scan/orchestrator.py +13 -0
- package/tools/scan/scanners/base.py +8 -0
- package/tools/scan/scanners/git.py +78 -0
- package/tools/scan/scanners/infrastructure.py +65 -0
- package/tools/scan/scanners/stack.py +110 -0
- package/tools/scan/setup.py +120 -13
- package/tools/scan/workspace.py +85 -0
- package/config/context-contracts.aws.json +0 -42
- package/config/context-contracts.gcp.json +0 -39
- package/skills/project-dispatch/SKILL.md +0 -34
- package/templates/settings.template.json +0 -226
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the Stack Scanner (T020).
|
|
3
|
+
|
|
4
|
+
Tests language detection, framework detection, build tool detection,
|
|
5
|
+
monorepo detection, and project_identity extraction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from tools.scan.scanners.stack import StackScanner
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def scanner() -> StackScanner:
|
|
19
|
+
"""Create a StackScanner instance."""
|
|
20
|
+
return StackScanner()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Scanner basics
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestStackScannerBasics:
|
|
29
|
+
"""Test scanner metadata and basic contract."""
|
|
30
|
+
|
|
31
|
+
def test_scanner_name(self, scanner: StackScanner) -> None:
|
|
32
|
+
assert scanner.SCANNER_NAME == "stack"
|
|
33
|
+
|
|
34
|
+
def test_scanner_version(self, scanner: StackScanner) -> None:
|
|
35
|
+
assert scanner.SCANNER_VERSION == "1.1.0"
|
|
36
|
+
|
|
37
|
+
def test_owned_sections(self, scanner: StackScanner) -> None:
|
|
38
|
+
assert "project_identity" in scanner.OWNED_SECTIONS
|
|
39
|
+
assert "stack" in scanner.OWNED_SECTIONS
|
|
40
|
+
|
|
41
|
+
def test_source_tag(self, scanner: StackScanner) -> None:
|
|
42
|
+
assert scanner.source_tag == "scanner:stack"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Language detection
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestLanguageDetection:
|
|
51
|
+
"""Test language detection from manifest files."""
|
|
52
|
+
|
|
53
|
+
def test_detect_nodejs_from_package_json(
|
|
54
|
+
self, scanner: StackScanner, node_project: Path
|
|
55
|
+
) -> None:
|
|
56
|
+
result = scanner.scan(node_project)
|
|
57
|
+
languages = result.sections["stack"]["languages"]
|
|
58
|
+
lang_names = [lang["name"] for lang in languages]
|
|
59
|
+
assert "javascript" in lang_names
|
|
60
|
+
|
|
61
|
+
def test_detect_typescript_with_tsconfig(
|
|
62
|
+
self, scanner: StackScanner, node_project: Path
|
|
63
|
+
) -> None:
|
|
64
|
+
(node_project / "tsconfig.json").write_text("{}")
|
|
65
|
+
result = scanner.scan(node_project)
|
|
66
|
+
languages = result.sections["stack"]["languages"]
|
|
67
|
+
lang_names = [lang["name"] for lang in languages]
|
|
68
|
+
assert "typescript" in lang_names
|
|
69
|
+
assert "javascript" not in lang_names
|
|
70
|
+
|
|
71
|
+
def test_detect_typescript_with_tsconfig_in_subdirectory(
|
|
72
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
73
|
+
) -> None:
|
|
74
|
+
"""TypeScript detected when tsconfig.json is in a monorepo subdirectory."""
|
|
75
|
+
pkg = {"name": "mono", "private": True, "workspaces": ["packages/*"]}
|
|
76
|
+
(tmp_path / "package.json").write_text(json.dumps(pkg))
|
|
77
|
+
sub = tmp_path / "packages" / "api"
|
|
78
|
+
sub.mkdir(parents=True)
|
|
79
|
+
(sub / "package.json").write_text('{"name": "@mono/api"}')
|
|
80
|
+
(sub / "tsconfig.json").write_text("{}")
|
|
81
|
+
result = scanner.scan(tmp_path)
|
|
82
|
+
languages = result.sections["stack"]["languages"]
|
|
83
|
+
lang_names = [lang["name"] for lang in languages]
|
|
84
|
+
assert "typescript" in lang_names
|
|
85
|
+
assert "javascript" not in lang_names
|
|
86
|
+
|
|
87
|
+
def test_detect_typescript_with_tsconfig_build_variant(
|
|
88
|
+
self, scanner: StackScanner, node_project: Path
|
|
89
|
+
) -> None:
|
|
90
|
+
"""TypeScript detected from tsconfig.build.json (glob pattern)."""
|
|
91
|
+
(node_project / "tsconfig.build.json").write_text("{}")
|
|
92
|
+
result = scanner.scan(node_project)
|
|
93
|
+
languages = result.sections["stack"]["languages"]
|
|
94
|
+
lang_names = [lang["name"] for lang in languages]
|
|
95
|
+
assert "typescript" in lang_names
|
|
96
|
+
|
|
97
|
+
def test_detect_typescript_from_ts_extension(
|
|
98
|
+
self, scanner: StackScanner, node_project: Path
|
|
99
|
+
) -> None:
|
|
100
|
+
"""TypeScript detected from .ts file extension even without tsconfig."""
|
|
101
|
+
(node_project / "index.ts").write_text("console.log('hello');")
|
|
102
|
+
result = scanner.scan(node_project)
|
|
103
|
+
languages = result.sections["stack"]["languages"]
|
|
104
|
+
lang_names = [lang["name"] for lang in languages]
|
|
105
|
+
assert "typescript" in lang_names
|
|
106
|
+
|
|
107
|
+
def test_detect_python_from_pyproject_toml(
|
|
108
|
+
self, scanner: StackScanner, python_project: Path
|
|
109
|
+
) -> None:
|
|
110
|
+
result = scanner.scan(python_project)
|
|
111
|
+
languages = result.sections["stack"]["languages"]
|
|
112
|
+
lang_names = [lang["name"] for lang in languages]
|
|
113
|
+
assert "python" in lang_names
|
|
114
|
+
|
|
115
|
+
def test_detect_python_from_setup_py(
|
|
116
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
117
|
+
) -> None:
|
|
118
|
+
(tmp_path / "setup.py").write_text(
|
|
119
|
+
'from setuptools import setup\nsetup(name="test")\n'
|
|
120
|
+
)
|
|
121
|
+
result = scanner.scan(tmp_path)
|
|
122
|
+
languages = result.sections["stack"]["languages"]
|
|
123
|
+
lang_names = [lang["name"] for lang in languages]
|
|
124
|
+
assert "python" in lang_names
|
|
125
|
+
|
|
126
|
+
def test_detect_python_from_requirements_txt(
|
|
127
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
128
|
+
) -> None:
|
|
129
|
+
(tmp_path / "requirements.txt").write_text("flask>=2.0\n")
|
|
130
|
+
result = scanner.scan(tmp_path)
|
|
131
|
+
languages = result.sections["stack"]["languages"]
|
|
132
|
+
lang_names = [lang["name"] for lang in languages]
|
|
133
|
+
assert "python" in lang_names
|
|
134
|
+
|
|
135
|
+
def test_detect_go_from_go_mod(
|
|
136
|
+
self, scanner: StackScanner, go_project: Path
|
|
137
|
+
) -> None:
|
|
138
|
+
result = scanner.scan(go_project)
|
|
139
|
+
languages = result.sections["stack"]["languages"]
|
|
140
|
+
lang_names = [lang["name"] for lang in languages]
|
|
141
|
+
assert "go" in lang_names
|
|
142
|
+
|
|
143
|
+
def test_detect_rust_from_cargo_toml(
|
|
144
|
+
self, scanner: StackScanner, rust_project: Path
|
|
145
|
+
) -> None:
|
|
146
|
+
result = scanner.scan(rust_project)
|
|
147
|
+
languages = result.sections["stack"]["languages"]
|
|
148
|
+
lang_names = [lang["name"] for lang in languages]
|
|
149
|
+
assert "rust" in lang_names
|
|
150
|
+
|
|
151
|
+
def test_detect_java_from_pom_xml(
|
|
152
|
+
self, scanner: StackScanner, java_maven_project: Path
|
|
153
|
+
) -> None:
|
|
154
|
+
result = scanner.scan(java_maven_project)
|
|
155
|
+
languages = result.sections["stack"]["languages"]
|
|
156
|
+
lang_names = [lang["name"] for lang in languages]
|
|
157
|
+
assert "java" in lang_names
|
|
158
|
+
|
|
159
|
+
def test_detect_java_from_build_gradle(
|
|
160
|
+
self, scanner: StackScanner, java_gradle_project: Path
|
|
161
|
+
) -> None:
|
|
162
|
+
result = scanner.scan(java_gradle_project)
|
|
163
|
+
languages = result.sections["stack"]["languages"]
|
|
164
|
+
lang_names = [lang["name"] for lang in languages]
|
|
165
|
+
assert "java" in lang_names
|
|
166
|
+
|
|
167
|
+
def test_detect_php_from_composer_json(
|
|
168
|
+
self, scanner: StackScanner, php_project: Path
|
|
169
|
+
) -> None:
|
|
170
|
+
result = scanner.scan(php_project)
|
|
171
|
+
languages = result.sections["stack"]["languages"]
|
|
172
|
+
lang_names = [lang["name"] for lang in languages]
|
|
173
|
+
assert "php" in lang_names
|
|
174
|
+
|
|
175
|
+
def test_detect_ruby_from_gemfile(
|
|
176
|
+
self, scanner: StackScanner, ruby_project: Path
|
|
177
|
+
) -> None:
|
|
178
|
+
result = scanner.scan(ruby_project)
|
|
179
|
+
languages = result.sections["stack"]["languages"]
|
|
180
|
+
lang_names = [lang["name"] for lang in languages]
|
|
181
|
+
assert "ruby" in lang_names
|
|
182
|
+
|
|
183
|
+
def test_detect_csharp_from_csproj(
|
|
184
|
+
self, scanner: StackScanner, csharp_project: Path
|
|
185
|
+
) -> None:
|
|
186
|
+
result = scanner.scan(csharp_project)
|
|
187
|
+
languages = result.sections["stack"]["languages"]
|
|
188
|
+
lang_names = [lang["name"] for lang in languages]
|
|
189
|
+
assert "csharp" in lang_names
|
|
190
|
+
|
|
191
|
+
def test_primary_language_flag(
|
|
192
|
+
self, scanner: StackScanner, node_project: Path
|
|
193
|
+
) -> None:
|
|
194
|
+
result = scanner.scan(node_project)
|
|
195
|
+
languages = result.sections["stack"]["languages"]
|
|
196
|
+
primary_langs = [lang for lang in languages if lang.get("primary")]
|
|
197
|
+
assert len(primary_langs) == 1
|
|
198
|
+
|
|
199
|
+
def test_empty_project_no_languages(
|
|
200
|
+
self, scanner: StackScanner, empty_project: Path
|
|
201
|
+
) -> None:
|
|
202
|
+
result = scanner.scan(empty_project)
|
|
203
|
+
languages = result.sections["stack"]["languages"]
|
|
204
|
+
assert languages == []
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Framework detection
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestFrameworkDetection:
|
|
213
|
+
"""Test framework detection from dependency declarations."""
|
|
214
|
+
|
|
215
|
+
def test_detect_nestjs_from_nestjs_core(
|
|
216
|
+
self, scanner: StackScanner, node_project: Path
|
|
217
|
+
) -> None:
|
|
218
|
+
result = scanner.scan(node_project)
|
|
219
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
220
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
221
|
+
assert "nestjs" in fw_names
|
|
222
|
+
|
|
223
|
+
def test_detect_express(
|
|
224
|
+
self, scanner: StackScanner, node_project: Path
|
|
225
|
+
) -> None:
|
|
226
|
+
result = scanner.scan(node_project)
|
|
227
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
228
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
229
|
+
assert "express" in fw_names
|
|
230
|
+
|
|
231
|
+
def test_detect_react(
|
|
232
|
+
self, scanner: StackScanner, node_project: Path
|
|
233
|
+
) -> None:
|
|
234
|
+
result = scanner.scan(node_project)
|
|
235
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
236
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
237
|
+
assert "react" in fw_names
|
|
238
|
+
|
|
239
|
+
def test_detect_fastapi_from_pyproject(
|
|
240
|
+
self, scanner: StackScanner, python_project: Path
|
|
241
|
+
) -> None:
|
|
242
|
+
result = scanner.scan(python_project)
|
|
243
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
244
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
245
|
+
assert "fastapi" in fw_names
|
|
246
|
+
|
|
247
|
+
def test_detect_fastapi_from_requirements_txt(
|
|
248
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
249
|
+
) -> None:
|
|
250
|
+
(tmp_path / "requirements.txt").write_text("fastapi>=0.100.0\nuvicorn\n")
|
|
251
|
+
result = scanner.scan(tmp_path)
|
|
252
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
253
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
254
|
+
assert "fastapi" in fw_names
|
|
255
|
+
|
|
256
|
+
def test_detect_flask(
|
|
257
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
258
|
+
) -> None:
|
|
259
|
+
(tmp_path / "requirements.txt").write_text("flask>=2.0\n")
|
|
260
|
+
result = scanner.scan(tmp_path)
|
|
261
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
262
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
263
|
+
assert "flask" in fw_names
|
|
264
|
+
|
|
265
|
+
def test_detect_django(
|
|
266
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
267
|
+
) -> None:
|
|
268
|
+
(tmp_path / "requirements.txt").write_text("django>=4.0\n")
|
|
269
|
+
result = scanner.scan(tmp_path)
|
|
270
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
271
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
272
|
+
assert "django" in fw_names
|
|
273
|
+
|
|
274
|
+
def test_nestjs_promoted_above_express(
|
|
275
|
+
self, scanner: StackScanner, node_project: Path
|
|
276
|
+
) -> None:
|
|
277
|
+
"""NestJS should be first framework when both NestJS and Express present."""
|
|
278
|
+
result = scanner.scan(node_project)
|
|
279
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
280
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
281
|
+
assert fw_names[0] == "nestjs"
|
|
282
|
+
|
|
283
|
+
def test_express_marked_as_underlying_when_nestjs_present(
|
|
284
|
+
self, scanner: StackScanner, node_project: Path
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Express should be marked as underlying when NestJS is detected."""
|
|
287
|
+
result = scanner.scan(node_project)
|
|
288
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
289
|
+
express = [fw for fw in frameworks if fw["name"] == "express"][0]
|
|
290
|
+
assert express.get("role") == "underlying"
|
|
291
|
+
|
|
292
|
+
def test_detect_vue(
|
|
293
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
294
|
+
) -> None:
|
|
295
|
+
pkg = {"name": "vue-app", "dependencies": {"vue": "^3.0.0"}}
|
|
296
|
+
(tmp_path / "package.json").write_text(json.dumps(pkg))
|
|
297
|
+
result = scanner.scan(tmp_path)
|
|
298
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
299
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
300
|
+
assert "vue" in fw_names
|
|
301
|
+
|
|
302
|
+
def test_detect_angular(
|
|
303
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
304
|
+
) -> None:
|
|
305
|
+
pkg = {"name": "angular-app", "dependencies": {"@angular/core": "^17.0.0"}}
|
|
306
|
+
(tmp_path / "package.json").write_text(json.dumps(pkg))
|
|
307
|
+
result = scanner.scan(tmp_path)
|
|
308
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
309
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
310
|
+
assert "angular" in fw_names
|
|
311
|
+
|
|
312
|
+
def test_detect_nextjs(
|
|
313
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
314
|
+
) -> None:
|
|
315
|
+
pkg = {"name": "next-app", "dependencies": {"next": "^14.0.0", "react": "^18.0.0"}}
|
|
316
|
+
(tmp_path / "package.json").write_text(json.dumps(pkg))
|
|
317
|
+
result = scanner.scan(tmp_path)
|
|
318
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
319
|
+
fw_names = [fw["name"] for fw in frameworks]
|
|
320
|
+
assert "next.js" in fw_names
|
|
321
|
+
|
|
322
|
+
def test_framework_version_extracted(
|
|
323
|
+
self, scanner: StackScanner, node_project: Path
|
|
324
|
+
) -> None:
|
|
325
|
+
result = scanner.scan(node_project)
|
|
326
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
327
|
+
express = [fw for fw in frameworks if fw["name"] == "express"][0]
|
|
328
|
+
assert express["version"] is not None
|
|
329
|
+
assert "4.18.0" in express["version"]
|
|
330
|
+
|
|
331
|
+
def test_empty_project_no_frameworks(
|
|
332
|
+
self, scanner: StackScanner, empty_project: Path
|
|
333
|
+
) -> None:
|
|
334
|
+
result = scanner.scan(empty_project)
|
|
335
|
+
frameworks = result.sections["stack"]["frameworks"]
|
|
336
|
+
assert frameworks == []
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
# Build tool detection
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class TestBuildToolDetection:
|
|
345
|
+
"""Test build tool detection from lock files and manifests."""
|
|
346
|
+
|
|
347
|
+
def test_detect_npm_from_package_lock(
|
|
348
|
+
self, scanner: StackScanner, node_project: Path
|
|
349
|
+
) -> None:
|
|
350
|
+
result = scanner.scan(node_project)
|
|
351
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
352
|
+
tool_names = [t["name"] for t in build_tools]
|
|
353
|
+
assert "npm" in tool_names
|
|
354
|
+
|
|
355
|
+
def test_detect_pnpm_from_lock(
|
|
356
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
357
|
+
) -> None:
|
|
358
|
+
(tmp_path / "package.json").write_text('{"name": "test"}')
|
|
359
|
+
(tmp_path / "pnpm-lock.yaml").write_text("lockfileVersion: 5.4\n")
|
|
360
|
+
result = scanner.scan(tmp_path)
|
|
361
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
362
|
+
tool_names = [t["name"] for t in build_tools]
|
|
363
|
+
assert "pnpm" in tool_names
|
|
364
|
+
|
|
365
|
+
def test_detect_yarn_from_lock(
|
|
366
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
367
|
+
) -> None:
|
|
368
|
+
(tmp_path / "package.json").write_text('{"name": "test"}')
|
|
369
|
+
(tmp_path / "yarn.lock").write_text("# yarn lockfile v1\n")
|
|
370
|
+
result = scanner.scan(tmp_path)
|
|
371
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
372
|
+
tool_names = [t["name"] for t in build_tools]
|
|
373
|
+
assert "yarn" in tool_names
|
|
374
|
+
|
|
375
|
+
def test_detect_cargo_from_lock(
|
|
376
|
+
self, scanner: StackScanner, rust_project: Path
|
|
377
|
+
) -> None:
|
|
378
|
+
result = scanner.scan(rust_project)
|
|
379
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
380
|
+
tool_names = [t["name"] for t in build_tools]
|
|
381
|
+
assert "cargo" in tool_names
|
|
382
|
+
|
|
383
|
+
def test_detect_go_build_tool(
|
|
384
|
+
self, scanner: StackScanner, go_project: Path
|
|
385
|
+
) -> None:
|
|
386
|
+
result = scanner.scan(go_project)
|
|
387
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
388
|
+
tool_names = [t["name"] for t in build_tools]
|
|
389
|
+
assert "go" in tool_names
|
|
390
|
+
|
|
391
|
+
def test_detect_maven_from_pom(
|
|
392
|
+
self, scanner: StackScanner, java_maven_project: Path
|
|
393
|
+
) -> None:
|
|
394
|
+
result = scanner.scan(java_maven_project)
|
|
395
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
396
|
+
tool_names = [t["name"] for t in build_tools]
|
|
397
|
+
assert "maven" in tool_names
|
|
398
|
+
|
|
399
|
+
def test_detect_gradle_from_build_gradle(
|
|
400
|
+
self, scanner: StackScanner, java_gradle_project: Path
|
|
401
|
+
) -> None:
|
|
402
|
+
result = scanner.scan(java_gradle_project)
|
|
403
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
404
|
+
tool_names = [t["name"] for t in build_tools]
|
|
405
|
+
assert "gradle" in tool_names
|
|
406
|
+
|
|
407
|
+
def test_detect_pip_from_requirements(
|
|
408
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
409
|
+
) -> None:
|
|
410
|
+
(tmp_path / "requirements.txt").write_text("flask>=2.0\n")
|
|
411
|
+
result = scanner.scan(tmp_path)
|
|
412
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
413
|
+
tool_names = [t["name"] for t in build_tools]
|
|
414
|
+
assert "pip" in tool_names
|
|
415
|
+
|
|
416
|
+
def test_detect_turborepo_in_build_tools(
|
|
417
|
+
self, scanner: StackScanner, monorepo_project: Path
|
|
418
|
+
) -> None:
|
|
419
|
+
"""Turborepo should appear in build_tools when turbo.json exists."""
|
|
420
|
+
result = scanner.scan(monorepo_project)
|
|
421
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
422
|
+
tool_names = [t["name"] for t in build_tools]
|
|
423
|
+
assert "turborepo" in tool_names
|
|
424
|
+
|
|
425
|
+
def test_turborepo_build_tool_has_config_file(
|
|
426
|
+
self, scanner: StackScanner, monorepo_project: Path
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Turborepo build tool entry should reference the config file."""
|
|
429
|
+
result = scanner.scan(monorepo_project)
|
|
430
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
431
|
+
turbo = [t for t in build_tools if t["name"] == "turborepo"][0]
|
|
432
|
+
assert turbo["detected_by"] == "config_file"
|
|
433
|
+
assert turbo["config_file"] == "turbo.json"
|
|
434
|
+
|
|
435
|
+
def test_turborepo_detected_in_subdirectory(
|
|
436
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Turborepo detected when turbo.json is in a monorepo subdirectory."""
|
|
439
|
+
sub = tmp_path / "qxo-monorepo"
|
|
440
|
+
sub.mkdir()
|
|
441
|
+
(sub / "turbo.json").write_text('{"pipeline": {}}')
|
|
442
|
+
(sub / "package.json").write_text('{"name": "mono", "private": true}')
|
|
443
|
+
result = scanner.scan(tmp_path)
|
|
444
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
445
|
+
tool_names = [t["name"] for t in build_tools]
|
|
446
|
+
assert "turborepo" in tool_names
|
|
447
|
+
|
|
448
|
+
def test_empty_project_no_build_tools(
|
|
449
|
+
self, scanner: StackScanner, empty_project: Path
|
|
450
|
+
) -> None:
|
|
451
|
+
result = scanner.scan(empty_project)
|
|
452
|
+
build_tools = result.sections["stack"]["build_tools"]
|
|
453
|
+
assert build_tools == []
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ---------------------------------------------------------------------------
|
|
457
|
+
# Monorepo detection
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class TestMonorepoDetection:
|
|
462
|
+
"""Test monorepo detection from workspace configs."""
|
|
463
|
+
|
|
464
|
+
def test_detect_monorepo_with_turbo(
|
|
465
|
+
self, scanner: StackScanner, monorepo_project: Path
|
|
466
|
+
) -> None:
|
|
467
|
+
result = scanner.scan(monorepo_project)
|
|
468
|
+
identity = result.sections["project_identity"]
|
|
469
|
+
assert identity["monorepo"]["detected"] is True
|
|
470
|
+
assert identity["monorepo"]["tool"] == "turborepo"
|
|
471
|
+
|
|
472
|
+
def test_detect_monorepo_multiple_languages(
|
|
473
|
+
self, scanner: StackScanner, monorepo_project: Path
|
|
474
|
+
) -> None:
|
|
475
|
+
result = scanner.scan(monorepo_project)
|
|
476
|
+
languages = result.sections["stack"]["languages"]
|
|
477
|
+
lang_names = [lang["name"] for lang in languages]
|
|
478
|
+
assert "javascript" in lang_names
|
|
479
|
+
assert "python" in lang_names
|
|
480
|
+
|
|
481
|
+
def test_detect_pnpm_workspace_monorepo(
|
|
482
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
483
|
+
) -> None:
|
|
484
|
+
(tmp_path / "package.json").write_text('{"name": "mono", "private": true}')
|
|
485
|
+
(tmp_path / "pnpm-workspace.yaml").write_text("packages:\n - 'apps/*'\n")
|
|
486
|
+
result = scanner.scan(tmp_path)
|
|
487
|
+
identity = result.sections["project_identity"]
|
|
488
|
+
assert identity["monorepo"]["detected"] is True
|
|
489
|
+
|
|
490
|
+
def test_detect_npm_workspaces_monorepo(
|
|
491
|
+
self, scanner: StackScanner, tmp_path: Path
|
|
492
|
+
) -> None:
|
|
493
|
+
pkg = {"name": "mono", "private": True, "workspaces": ["packages/*"]}
|
|
494
|
+
(tmp_path / "package.json").write_text(json.dumps(pkg))
|
|
495
|
+
result = scanner.scan(tmp_path)
|
|
496
|
+
identity = result.sections["project_identity"]
|
|
497
|
+
assert identity["monorepo"]["detected"] is True
|
|
498
|
+
|
|
499
|
+
def test_no_monorepo_single_language(
|
|
500
|
+
self, scanner: StackScanner, node_project: Path
|
|
501
|
+
) -> None:
|
|
502
|
+
result = scanner.scan(node_project)
|
|
503
|
+
identity = result.sections["project_identity"]
|
|
504
|
+
assert identity["monorepo"]["detected"] is False
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ---------------------------------------------------------------------------
|
|
508
|
+
# Project identity
|
|
509
|
+
# ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class TestProjectIdentity:
|
|
513
|
+
"""Test project identity extraction."""
|
|
514
|
+
|
|
515
|
+
def test_name_from_package_json(
|
|
516
|
+
self, scanner: StackScanner, node_project: Path
|
|
517
|
+
) -> None:
|
|
518
|
+
result = scanner.scan(node_project)
|
|
519
|
+
identity = result.sections["project_identity"]
|
|
520
|
+
assert identity["name"] == "test-node-project"
|
|
521
|
+
|
|
522
|
+
def test_name_from_pyproject_toml(
|
|
523
|
+
self, scanner: StackScanner, python_project: Path
|
|
524
|
+
) -> None:
|
|
525
|
+
result = scanner.scan(python_project)
|
|
526
|
+
identity = result.sections["project_identity"]
|
|
527
|
+
assert identity["name"] == "test-python-project"
|
|
528
|
+
|
|
529
|
+
def test_name_from_go_mod(
|
|
530
|
+
self, scanner: StackScanner, go_project: Path
|
|
531
|
+
) -> None:
|
|
532
|
+
result = scanner.scan(go_project)
|
|
533
|
+
identity = result.sections["project_identity"]
|
|
534
|
+
assert identity["name"] == "test-go-project"
|
|
535
|
+
|
|
536
|
+
def test_name_from_cargo_toml(
|
|
537
|
+
self, scanner: StackScanner, rust_project: Path
|
|
538
|
+
) -> None:
|
|
539
|
+
result = scanner.scan(rust_project)
|
|
540
|
+
identity = result.sections["project_identity"]
|
|
541
|
+
assert identity["name"] == "test-rust-project"
|
|
542
|
+
|
|
543
|
+
def test_fallback_to_directory_name(
|
|
544
|
+
self, scanner: StackScanner, empty_project: Path
|
|
545
|
+
) -> None:
|
|
546
|
+
result = scanner.scan(empty_project)
|
|
547
|
+
identity = result.sections["project_identity"]
|
|
548
|
+
assert identity["name"] == empty_project.name
|
|
549
|
+
|
|
550
|
+
def test_empty_project_type_unknown(
|
|
551
|
+
self, scanner: StackScanner, empty_project: Path
|
|
552
|
+
) -> None:
|
|
553
|
+
result = scanner.scan(empty_project)
|
|
554
|
+
identity = result.sections["project_identity"]
|
|
555
|
+
assert identity["type"] == "unknown"
|
|
556
|
+
|
|
557
|
+
def test_monorepo_type(
|
|
558
|
+
self, scanner: StackScanner, monorepo_project: Path
|
|
559
|
+
) -> None:
|
|
560
|
+
result = scanner.scan(monorepo_project)
|
|
561
|
+
identity = result.sections["project_identity"]
|
|
562
|
+
assert identity["type"] == "monorepo"
|
|
563
|
+
|
|
564
|
+
def test_description_from_package_json(
|
|
565
|
+
self, scanner: StackScanner, node_project: Path
|
|
566
|
+
) -> None:
|
|
567
|
+
result = scanner.scan(node_project)
|
|
568
|
+
identity = result.sections["project_identity"]
|
|
569
|
+
assert identity["description"] == "A test Node.js project"
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
# ---------------------------------------------------------------------------
|
|
573
|
+
# ScanResult contract
|
|
574
|
+
# ---------------------------------------------------------------------------
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class TestScanResultContract:
|
|
578
|
+
"""Test that scan results follow the expected contract."""
|
|
579
|
+
|
|
580
|
+
def test_result_has_source_tags(
|
|
581
|
+
self, scanner: StackScanner, node_project: Path
|
|
582
|
+
) -> None:
|
|
583
|
+
result = scanner.scan(node_project)
|
|
584
|
+
assert result.sections["project_identity"]["_source"] == "scanner:stack"
|
|
585
|
+
assert result.sections["stack"]["_source"] == "scanner:stack"
|
|
586
|
+
|
|
587
|
+
def test_result_has_duration(
|
|
588
|
+
self, scanner: StackScanner, node_project: Path
|
|
589
|
+
) -> None:
|
|
590
|
+
result = scanner.scan(node_project)
|
|
591
|
+
assert result.duration_ms >= 0
|
|
592
|
+
|
|
593
|
+
def test_result_scanner_name(
|
|
594
|
+
self, scanner: StackScanner, node_project: Path
|
|
595
|
+
) -> None:
|
|
596
|
+
result = scanner.scan(node_project)
|
|
597
|
+
assert result.scanner == "stack"
|
|
598
|
+
|
|
599
|
+
def test_empty_project_returns_both_sections(
|
|
600
|
+
self, scanner: StackScanner, empty_project: Path
|
|
601
|
+
) -> None:
|
|
602
|
+
result = scanner.scan(empty_project)
|
|
603
|
+
assert "project_identity" in result.sections
|
|
604
|
+
assert "stack" in result.sections
|