wicked-garden 12.27.0
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/archetypes.json +166 -0
- package/.claude-plugin/components.json +75 -0
- package/.claude-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +174 -0
- package/.claude-plugin/specialist.json +48 -0
- package/.claude-plugin/stack-registry.json +96 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +712 -0
- package/CONTRIBUTING.md +99 -0
- package/ETHOS.md +83 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/WICKED_GARDEN_BUS_EVENTS.md +149 -0
- package/agents/agentic/architect.md +449 -0
- package/agents/agentic/performance-analyst.md +702 -0
- package/agents/agentic/safety-reviewer.md +662 -0
- package/agents/crew/implementer.md +163 -0
- package/agents/crew/researcher.md +94 -0
- package/agents/crew/reviewer.md +184 -0
- package/agents/data/data-engineer.md +227 -0
- package/agents/engineering/api-documentarian.md +440 -0
- package/agents/engineering/migration-engineer.md +301 -0
- package/agents/engineering/solution-architect.md +208 -0
- package/agents/jam/brainstorm-facilitator.md +297 -0
- package/agents/jam/council.md +322 -0
- package/agents/jam/quick-facilitator.md +85 -0
- package/agents/persona/persona-agent.md +121 -0
- package/agents/platform/compliance-officer.md +266 -0
- package/agents/platform/privacy-expert.md +496 -0
- package/agents/platform/security-engineer.md +287 -0
- package/agents/product/a11y-expert.md +323 -0
- package/agents/product/requirements-analyst.md +238 -0
- package/agents/product/ui-reviewer.md +180 -0
- package/agents/product/ux-designer.md +199 -0
- package/agents/product/value-strategist.md +269 -0
- package/agents/qe/semantic-reviewer.md +78 -0
- package/commands/agentic/audit.md +24 -0
- package/commands/agentic/design.md +24 -0
- package/commands/agentic/frameworks.md +25 -0
- package/commands/agentic/review.md +27 -0
- package/commands/archetype/build.md +12 -0
- package/commands/archetype/decide.md +12 -0
- package/commands/archetype/explore.md +12 -0
- package/commands/archetype/incident.md +12 -0
- package/commands/archetype/migrate.md +12 -0
- package/commands/archetype/modernize.md +12 -0
- package/commands/archetype/review.md +14 -0
- package/commands/archetype/ship.md +12 -0
- package/commands/archetype/specify.md +12 -0
- package/commands/archetype/triage.md +12 -0
- package/commands/compile.md +37 -0
- package/commands/data/analyze.md +18 -0
- package/commands/data/data.md +21 -0
- package/commands/data/ml.md +18 -0
- package/commands/data/ontology.md +22 -0
- package/commands/data/pipeline.md +17 -0
- package/commands/deliberate.md +181 -0
- package/commands/engineering/add-field.md +77 -0
- package/commands/engineering/apply.md +58 -0
- package/commands/engineering/arch.md +26 -0
- package/commands/engineering/debug.md +27 -0
- package/commands/engineering/docs.md +27 -0
- package/commands/engineering/new-generator.md +257 -0
- package/commands/engineering/patch-plan.md +59 -0
- package/commands/engineering/plan.md +29 -0
- package/commands/engineering/remove.md +48 -0
- package/commands/engineering/rename.md +44 -0
- package/commands/engineering/review.md +32 -0
- package/commands/help.md +128 -0
- package/commands/install.md +118 -0
- package/commands/intent.md +54 -0
- package/commands/jam/brainstorm.md +37 -0
- package/commands/jam/council.md +49 -0
- package/commands/jam/quick.md +21 -0
- package/commands/jam/revisit.md +79 -0
- package/commands/persona/as.md +136 -0
- package/commands/persona/define.md +24 -0
- package/commands/persona/list.md +118 -0
- package/commands/platform/actions.md +20 -0
- package/commands/platform/assert.md +18 -0
- package/commands/platform/audit.md +23 -0
- package/commands/platform/compliance.md +20 -0
- package/commands/platform/gh.md +22 -0
- package/commands/platform/health.md +22 -0
- package/commands/platform/incident.md +23 -0
- package/commands/platform/infra.md +24 -0
- package/commands/platform/peer-health.md +103 -0
- package/commands/platform/plugin-health.md +58 -0
- package/commands/platform/security.md +38 -0
- package/commands/platform/toolchain.md +96 -0
- package/commands/platform/traces.md +20 -0
- package/commands/product/a11y.md +19 -0
- package/commands/product/acceptance.md +19 -0
- package/commands/product/align.md +20 -0
- package/commands/product/analyze.md +23 -0
- package/commands/product/elicit.md +19 -0
- package/commands/product/listen.md +24 -0
- package/commands/product/mockup.md +20 -0
- package/commands/product/screenshot.md +19 -0
- package/commands/product/strategy.md +40 -0
- package/commands/product/synthesize.md +23 -0
- package/commands/product/ux-review.md +42 -0
- package/commands/product/ux.md +20 -0
- package/commands/product/visual-direction.md +34 -0
- package/commands/prove.md +47 -0
- package/commands/report-issue.md +243 -0
- package/commands/reset.md +144 -0
- package/commands/search/blast-radius.md +58 -0
- package/commands/search/hotspots.md +55 -0
- package/commands/search/index.md +38 -0
- package/commands/search/lineage.md +47 -0
- package/commands/search/service-map.md +79 -0
- package/commands/setup.md +363 -0
- package/commands/smaht/briefing.md +200 -0
- package/commands/smaht/events-import.md +95 -0
- package/commands/smaht/state.md +19 -0
- package/commands/where-am-i.md +58 -0
- package/docs/README.md +32 -0
- package/docs/adr/0001-code-relationship-graph-engine.md +116 -0
- package/docs/adr/0002-major-cleanup-keep-cut.md +91 -0
- package/docs/adr/0003-peers-gate-required-layers-optin.md +41 -0
- package/docs/adr/0004-code-graph-moves-to-wicked-brain.md +47 -0
- package/docs/brain-chunk-format.md +135 -0
- package/docs/compiler.md +95 -0
- package/docs/daemon-api.md +70 -0
- package/docs/domains.md +197 -0
- package/docs/getting-started.md +208 -0
- package/docs/plans/2026-06-08-wicked-loom-conduct.md +1337 -0
- package/docs/plans/2026-06-08-wicked-loom-contract.md +638 -0
- package/docs/plans/2026-06-08-wicked-loom-cutover.md +1300 -0
- package/docs/plans/2026-06-08-wicked-loom-expand-compose.md +843 -0
- package/docs/plans/2026-06-08-wicked-loom-flow-cutover.md +403 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1a-graph-core.md +838 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1b-injected-edges.md +44 -0
- package/docs/required-peers.md +60 -0
- package/docs/specs/2026-06-08-wicked-loom-extraction.md +225 -0
- package/docs/specs/2026-06-10-codegraph-to-brain-migration.md +168 -0
- package/docs/v11/archetypes.md +231 -0
- package/hooks/hooks.json +275 -0
- package/hooks/scripts/bootstrap.py +1797 -0
- package/hooks/scripts/invoke.py +62 -0
- package/hooks/scripts/notification.py +166 -0
- package/hooks/scripts/permission_request.py +223 -0
- package/hooks/scripts/post_tool.py +1374 -0
- package/hooks/scripts/pre_compact.py +272 -0
- package/hooks/scripts/pre_tool.py +1278 -0
- package/hooks/scripts/prompt_submit.py +1109 -0
- package/hooks/scripts/session_end.py +95 -0
- package/hooks/scripts/stop.py +624 -0
- package/hooks/scripts/subagent_lifecycle.py +464 -0
- package/hooks/scripts/subscribers/on_gate_decided.py +84 -0
- package/hooks/scripts/task_completed.py +322 -0
- package/hooks/scripts/worktree_guard.py +148 -0
- package/hooks/subscriptions/on_gate_decided.json +9 -0
- package/install.mjs +128 -0
- package/package.json +41 -0
- package/pyproject.toml +59 -0
- package/schemas/README.md +32 -0
- package/schemas/assert_contracts.json +37 -0
- package/schemas/health_probe.json +46 -0
- package/schemas/plugin_status.json +37 -0
- package/scripts/_adapters/__init__.py +166 -0
- package/scripts/_adapters/jira_adapter.py +18 -0
- package/scripts/_adapters/linear_adapter.py +18 -0
- package/scripts/_adapters/miro_adapter.py +18 -0
- package/scripts/_adapters/notion_adapter.py +18 -0
- package/scripts/_agents.py +357 -0
- package/scripts/_brain_ingest/__init__.py +2 -0
- package/scripts/_brain_ingest/session_fact_extractor.py +341 -0
- package/scripts/_brain_port.py +163 -0
- package/scripts/_bus.py +1081 -0
- package/scripts/_bus_catalog_gen.py +93 -0
- package/scripts/_bus_consumers.json +46 -0
- package/scripts/_bus_emit.py +43 -0
- package/scripts/_capability_registry.py +334 -0
- package/scripts/_capability_resolver.py +322 -0
- package/scripts/_domain_store.py +559 -0
- package/scripts/_event_log_reader.py +154 -0
- package/scripts/_event_schema.py +305 -0
- package/scripts/_event_store.py +397 -0
- package/scripts/_heavy_cadence.py +324 -0
- package/scripts/_integration_resolver.py +321 -0
- package/scripts/_logger.py +151 -0
- package/scripts/_loom.py +482 -0
- package/scripts/_paths.py +133 -0
- package/scripts/_python.sh +22 -0
- package/scripts/_run.py +137 -0
- package/scripts/_session.py +502 -0
- package/scripts/_validate_registry.py +1042 -0
- package/scripts/_wicked_testing_probe.py +277 -0
- package/scripts/agentic/analyze_agents.py +391 -0
- package/scripts/agentic/detect_framework.py +420 -0
- package/scripts/agentic/issue_taxonomy.py +357 -0
- package/scripts/agentic/pattern_scorer.py +434 -0
- package/scripts/classify/persist.py +169 -0
- package/scripts/compiler/compile.py +552 -0
- package/scripts/compiler/phase0/VERDICT.md +60 -0
- package/scripts/compiler/phase0/detect.py +292 -0
- package/scripts/compiler/phase0/emit.py +228 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.bindings.json +30 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/command_iq.bindings.json +45 -0
- package/scripts/compiler/phase0/evidence/command_iq.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/memos.bindings.json +28 -0
- package/scripts/compiler/phase0/evidence/memos.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/verdict.json +104 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.bindings.json +29 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/wire-vault-transcript.txt +60 -0
- package/scripts/compiler/phase0/run_probe.py +234 -0
- package/scripts/compiler/phase0/wire_vault.py +310 -0
- package/scripts/crew/__init__.py +0 -0
- package/scripts/crew/_task_audit_writer.py +243 -0
- package/scripts/crew/_task_reader.py +389 -0
- package/scripts/crew/archetypes_v11.py +348 -0
- package/scripts/crew/detectors/__init__.py +12 -0
- package/scripts/crew/flow_compiler.py +121 -0
- package/scripts/crew/phase_manager.py +703 -0
- package/scripts/crew/scope_delta.py +264 -0
- package/scripts/crew/specialist_resolver.py +218 -0
- package/scripts/crew/stack_registry.py +189 -0
- package/scripts/data/data_profiler.py +271 -0
- package/scripts/data/ontology_recommender.py +231 -0
- package/scripts/data/schema_validator.py +267 -0
- package/scripts/delivery/drift.py +739 -0
- package/scripts/delivery/telemetry.py +614 -0
- package/scripts/engineering/patch/codegraph_db.py +125 -0
- package/scripts/engineering/patch/generators/__init__.py +56 -0
- package/scripts/engineering/patch/generators/base.py +373 -0
- package/scripts/engineering/patch/generators/csharp_generator.py +494 -0
- package/scripts/engineering/patch/generators/go_generator.py +430 -0
- package/scripts/engineering/patch/generators/java_generator.py +928 -0
- package/scripts/engineering/patch/generators/jsp_generator.py +454 -0
- package/scripts/engineering/patch/generators/kotlin_generator.py +561 -0
- package/scripts/engineering/patch/generators/perl_generator.py +510 -0
- package/scripts/engineering/patch/generators/php_generator.py +644 -0
- package/scripts/engineering/patch/generators/propagation_engine.py +929 -0
- package/scripts/engineering/patch/generators/python_generator.py +487 -0
- package/scripts/engineering/patch/generators/ruby_generator.py +463 -0
- package/scripts/engineering/patch/generators/rust_generator.py +400 -0
- package/scripts/engineering/patch/generators/sql_generator.py +594 -0
- package/scripts/engineering/patch/generators/typescript_generator.py +439 -0
- package/scripts/engineering/patch/patch.py +800 -0
- package/scripts/engineering/patch/safety.py +429 -0
- package/scripts/jam/__init__.py +0 -0
- package/scripts/jam/_bus_consumers.py +271 -0
- package/scripts/jam/agentic_cli_registry.py +703 -0
- package/scripts/jam/consensus.py +737 -0
- package/scripts/jam/detect_clis.py +409 -0
- package/scripts/jam/jam.py +283 -0
- package/scripts/jam/save_transcript.py +171 -0
- package/scripts/loom/__init__.py +22 -0
- package/scripts/loom/cli.py +170 -0
- package/scripts/loom/compose.py +180 -0
- package/scripts/loom/gate.py +120 -0
- package/scripts/loom/manifest.py +140 -0
- package/scripts/loom/resolve.py +59 -0
- package/scripts/persona/__init__.py +0 -0
- package/scripts/persona/registry.py +688 -0
- package/scripts/platform/guard_pipeline.py +1214 -0
- package/scripts/platform/guard_profiles.py +216 -0
- package/scripts/platform/observability/assert_contracts.py +434 -0
- package/scripts/platform/observability/health_probe.py +915 -0
- package/scripts/platform/observability/ops_log_viewer.py +209 -0
- package/scripts/platform/prereq_doctor.py +567 -0
- package/scripts/qe/conditions_manifest.py +260 -0
- package/scripts/qe/content_sanitizer.py +160 -0
- package/scripts/qe/coverage_tracker.py +535 -0
- package/scripts/qe/discover_scenarios.py +243 -0
- package/scripts/qe/evidence_tracker.py +232 -0
- package/scripts/qe/prove.py +196 -0
- package/scripts/qe/registry_lookup.py +80 -0
- package/scripts/qe/registry_store.py +176 -0
- package/scripts/qe/semantic_review.py +1005 -0
- package/scripts/qe/vault_gate.py +382 -0
- package/scripts/qe/verdict_audit.py +175 -0
- package/scripts/qe/verdict_schema.py +335 -0
- package/scripts/reset.py +220 -0
- package/scripts/resolve_path.py +22 -0
- package/scripts/sentinel/invariants.py +402 -0
- package/scripts/sentinel/pre_push.py +73 -0
- package/scripts/setup/__init__.py +1 -0
- package/scripts/setup/detect_state.py +114 -0
- package/scripts/setup/migrate_legacy.py +67 -0
- package/scripts/setup/migrate_v6_projects.py +232 -0
- package/scripts/setup/onboarding.py +114 -0
- package/scripts/smaht/_bus_consumers.py +98 -0
- package/scripts/smaht/adapters/__init__.py +83 -0
- package/scripts/smaht/adapters/brain_adapter.py +254 -0
- package/scripts/smaht/adapters/context7_adapter.py +438 -0
- package/scripts/smaht/adapters/delegation_adapter.py +104 -0
- package/scripts/smaht/adapters/domain_adapter.py +206 -0
- package/scripts/smaht/adapters/events_adapter.py +231 -0
- package/scripts/smaht/adapters/tools_adapter.py +49 -0
- package/scripts/smaht/cheatsheet_store.py +236 -0
- package/scripts/smaht/context_package.py +386 -0
- package/scripts/smaht/knowledge_graph.py +395 -0
- package/scripts/smaht/propose_skills.py +856 -0
- package/scripts/smaht/test_cheatsheet_store.py +428 -0
- package/scripts/statusline.py +151 -0
- package/scripts/where_am_i.py +236 -0
- package/skills/agentic/agentic-patterns/SKILL.md +91 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-design.md +283 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-operational.md +216 -0
- package/skills/agentic/agentic-patterns/refs/pattern-catalog.md +329 -0
- package/skills/agentic/context-engineering/SKILL.md +142 -0
- package/skills/agentic/context-engineering/refs/caching-and-optimization.md +251 -0
- package/skills/agentic/context-engineering/refs/compression-techniques.md +171 -0
- package/skills/agentic/context-engineering/refs/cost-calculation-budget.md +250 -0
- package/skills/agentic/context-engineering/refs/cost-optimization-reporting.md +300 -0
- package/skills/agentic/context-engineering/refs/selective-loading.md +179 -0
- package/skills/agentic/frameworks/SKILL.md +191 -0
- package/skills/agentic/frameworks/refs/framework-profiles-1.md +209 -0
- package/skills/agentic/frameworks/refs/framework-profiles-2.md +214 -0
- package/skills/agentic/frameworks/refs/migration-patterns-paths.md +267 -0
- package/skills/agentic/frameworks/refs/migration-patterns-testing.md +208 -0
- package/skills/agentic/refs/audit.md +134 -0
- package/skills/agentic/refs/design.md +137 -0
- package/skills/agentic/refs/frameworks.md +102 -0
- package/skills/agentic/refs/review.md +160 -0
- package/skills/agentic/review-methodology/SKILL.md +186 -0
- package/skills/agentic/review-methodology/refs/deliverable-templates.md +246 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-quality-testing.md +277 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-reliability-safety.md +253 -0
- package/skills/agentic/trust-and-safety/SKILL.md +177 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-actions.md +246 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-input-output.md +238 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-resources.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-advanced.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-core.md +215 -0
- package/skills/archetype/SKILL.md +104 -0
- package/skills/archetype/refs/build.md +142 -0
- package/skills/archetype/refs/decide.md +117 -0
- package/skills/archetype/refs/explore.md +66 -0
- package/skills/archetype/refs/incident.md +149 -0
- package/skills/archetype/refs/migrate.md +170 -0
- package/skills/archetype/refs/modernize.md +187 -0
- package/skills/archetype/refs/review.md +145 -0
- package/skills/archetype/refs/ship.md +117 -0
- package/skills/archetype/refs/specify.md +108 -0
- package/skills/archetype/refs/triage.md +62 -0
- package/skills/classify/SKILL.md +157 -0
- package/skills/data/analysis/SKILL.md +125 -0
- package/skills/data/analysis/refs/templates.md +170 -0
- package/skills/data/data/SKILL.md +118 -0
- package/skills/data/data/refs/examples.md +235 -0
- package/skills/data/ml/SKILL.md +125 -0
- package/skills/data/ml/refs/templates.md +172 -0
- package/skills/data/pipeline/SKILL.md +136 -0
- package/skills/data/pipeline/refs/templates.md +175 -0
- package/skills/data/refs/analyze.md +123 -0
- package/skills/data/refs/data.md +95 -0
- package/skills/data/refs/ml.md +118 -0
- package/skills/data/refs/pipeline.md +92 -0
- package/skills/deliberate/SKILL.md +125 -0
- package/skills/deliberate/refs/architecture-lens.md +37 -0
- package/skills/deliberate/refs/assumptions-lens.md +43 -0
- package/skills/deliberate/refs/code-lens.md +38 -0
- package/skills/deliberate/refs/content-lens.md +37 -0
- package/skills/deliberate/refs/data-lens.md +43 -0
- package/skills/deliberate/refs/design-lens.md +37 -0
- package/skills/deliberate/refs/opportunity-patterns.md +107 -0
- package/skills/deliberate/refs/ops-lens.md +43 -0
- package/skills/deliberate/refs/product-lens.md +46 -0
- package/skills/deliberate/refs/rethink-framework.md +77 -0
- package/skills/deliberate/refs/security-lens.md +47 -0
- package/skills/deliberate/refs/testing-lens.md +45 -0
- package/skills/deliberate/refs/timing-lens.md +42 -0
- package/skills/engineering/architecture/SKILL.md +177 -0
- package/skills/engineering/architecture/refs/adr-template.md +268 -0
- package/skills/engineering/architecture/refs/architecture-template-deployment.md +176 -0
- package/skills/engineering/architecture/refs/architecture-template-design.md +300 -0
- package/skills/engineering/architecture/refs/examples-ecommerce-analytics.md +299 -0
- package/skills/engineering/architecture/refs/examples-saas-trading.md +291 -0
- package/skills/engineering/architecture/refs/patterns-advanced.md +238 -0
- package/skills/engineering/architecture/refs/patterns-structural.md +303 -0
- package/skills/engineering/backend/SKILL.md +102 -0
- package/skills/engineering/backend/refs/checklists.md +146 -0
- package/skills/engineering/backend/refs/patterns.md +181 -0
- package/skills/engineering/debugging/SKILL.md +99 -0
- package/skills/engineering/debugging/refs/patterns.md +248 -0
- package/skills/engineering/debugging/refs/process.md +203 -0
- package/skills/engineering/docs/SKILL.md +97 -0
- package/skills/engineering/engineering/SKILL.md +88 -0
- package/skills/engineering/engineering/refs/checklists.md +91 -0
- package/skills/engineering/engineering/refs/templates.md +71 -0
- package/skills/engineering/frontend/SKILL.md +92 -0
- package/skills/engineering/frontend/refs/checklists.md +96 -0
- package/skills/engineering/frontend/refs/patterns.md +167 -0
- package/skills/engineering/integration/SKILL.md +183 -0
- package/skills/engineering/integration/refs/error-handling-best-practices.md +139 -0
- package/skills/engineering/integration/refs/error-handling-error-classes-typescript.md +212 -0
- package/skills/engineering/integration/refs/error-handling-error-response-format.md +301 -0
- package/skills/engineering/integration/refs/error-handling-retry-strategies.md +190 -0
- package/skills/engineering/integration/refs/event-schemas-best-practices.md +225 -0
- package/skills/engineering/integration/refs/event-schemas-event-naming-conventions.md +303 -0
- package/skills/engineering/integration/refs/event-schemas-event-structure.md +288 -0
- package/skills/engineering/integration/refs/graphql-best-practices.md +138 -0
- package/skills/engineering/integration/refs/graphql-error-handling.md +198 -0
- package/skills/engineering/integration/refs/graphql-graphql-basics.md +197 -0
- package/skills/engineering/integration/refs/graphql-query-examples.md +226 -0
- package/skills/engineering/integration/refs/graphql-schema-design-patterns.md +280 -0
- package/skills/engineering/integration/refs/grpc-grpc-overview.md +246 -0
- package/skills/engineering/integration/refs/grpc-implementation-examples.md +269 -0
- package/skills/engineering/integration/refs/grpc-metadata-headers.md +304 -0
- package/skills/engineering/integration/refs/grpc-testing.md +49 -0
- package/skills/engineering/integration/refs/openapi-template.yaml +682 -0
- package/skills/engineering/integration/refs/resilience-best-practices.md +109 -0
- package/skills/engineering/integration/refs/resilience-circuit-breaker-pattern.md +302 -0
- package/skills/engineering/integration/refs/resilience-rate-limiting.md +245 -0
- package/skills/engineering/integration/refs/resilience-timeout-pattern.md +212 -0
- package/skills/engineering/integration/refs/rest-api-fundamentals.md +299 -0
- package/skills/engineering/integration/refs/rest-api-responses-security.md +302 -0
- package/skills/engineering/integration/refs/versioning-breaking-changes.md +237 -0
- package/skills/engineering/integration/refs/versioning-migration-strategies.md +257 -0
- package/skills/engineering/integration/refs/versioning-versioning-strategies.md +296 -0
- package/skills/engineering/large-scale-migration/SKILL.md +65 -0
- package/skills/engineering/patch/SKILL.md +184 -0
- package/skills/engineering/refs/arch.md +126 -0
- package/skills/engineering/refs/debug.md +142 -0
- package/skills/engineering/refs/docs.md +190 -0
- package/skills/engineering/refs/plan.md +109 -0
- package/skills/engineering/refs/review.md +138 -0
- package/skills/engineering/system-design/SKILL.md +154 -0
- package/skills/engineering/system-design/refs/anti-patterns-big-ball-of-mud.md +254 -0
- package/skills/engineering/system-design/refs/anti-patterns-circular-dependencies.md +241 -0
- package/skills/engineering/system-design/refs/anti-patterns-feature-envy.md +168 -0
- package/skills/engineering/system-design/refs/anti-patterns-god-object.md +247 -0
- package/skills/engineering/system-design/refs/component-template-lifecycle.md +210 -0
- package/skills/engineering/system-design/refs/component-template-maintenance.md +219 -0
- package/skills/engineering/system-design/refs/component-template-structure.md +276 -0
- package/skills/engineering/system-design/refs/dependencies-best-practices.md +260 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-principles.md +295 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-tracking.md +261 -0
- package/skills/engineering/system-design/refs/interface-template-lifecycle.md +211 -0
- package/skills/engineering/system-design/refs/interface-template-maintenance.md +212 -0
- package/skills/engineering/system-design/refs/interface-template-structure.md +269 -0
- package/skills/engineering/system-design/refs/patterns-dependency-injection.md +151 -0
- package/skills/engineering/system-design/refs/patterns-facade-pattern.md +257 -0
- package/skills/engineering/system-design/refs/patterns-layered-pattern.md +204 -0
- package/skills/engineering/system-design/refs/patterns-plugin-architecture.md +229 -0
- package/skills/engineering/unit-test-quality/SKILL.md +194 -0
- package/skills/ground/SKILL.md +130 -0
- package/skills/integration-discovery/SKILL.md +168 -0
- package/skills/integration-discovery/refs/capabilities.md +282 -0
- package/skills/integration-discovery/refs/cli-detection.md +295 -0
- package/skills/integration-discovery/refs/discovery-sources.md +233 -0
- package/skills/integration-discovery/refs/task-patterns.md +206 -0
- package/skills/jam/SKILL.md +45 -0
- package/skills/jam/refs/facilitation-patterns.md +125 -0
- package/skills/jam/refs/quick.md +64 -0
- package/skills/jam/refs/synthesis-patterns.md +114 -0
- package/skills/multi-model/SKILL.md +168 -0
- package/skills/multi-model/refs/aichat.md +119 -0
- package/skills/multi-model/refs/aider.md +100 -0
- package/skills/multi-model/refs/auditability.md +238 -0
- package/skills/multi-model/refs/codex.md +169 -0
- package/skills/multi-model/refs/context.md +306 -0
- package/skills/multi-model/refs/copilot.md +161 -0
- package/skills/multi-model/refs/examples.md +224 -0
- package/skills/multi-model/refs/gemini.md +185 -0
- package/skills/multi-model/refs/goose.md +112 -0
- package/skills/multi-model/refs/llm.md +124 -0
- package/skills/multi-model/refs/opencode.md +196 -0
- package/skills/multi-model/refs/orchestration.md +188 -0
- package/skills/multi-model/refs/pi.md +208 -0
- package/skills/persona/SKILL.md +144 -0
- package/skills/persona/refs/define.md +133 -0
- package/skills/platform/actions/refs/actions.md +114 -0
- package/skills/platform/audit/SKILL.md +174 -0
- package/skills/platform/audit/refs/audit.md +145 -0
- package/skills/platform/audit/refs/checklists-evidence-operations.md +144 -0
- package/skills/platform/audit/refs/checklists-gdpr-pci-evidence.md +213 -0
- package/skills/platform/audit/refs/checklists-soc2-hipaa.md +120 -0
- package/skills/platform/audit/refs/frameworks-gdpr-pci.md +225 -0
- package/skills/platform/audit/refs/frameworks-soc2-hipaa.md +228 -0
- package/skills/platform/compliance/SKILL.md +169 -0
- package/skills/platform/compliance/refs/checklists.md +303 -0
- package/skills/platform/compliance/refs/compliance.md +117 -0
- package/skills/platform/compliance/refs/frameworks.md +282 -0
- package/skills/platform/errors/SKILL.md +183 -0
- package/skills/platform/errors/refs/patterns.md +271 -0
- package/skills/platform/errors/refs/severity.md +264 -0
- package/skills/platform/gate-benchmark-rebaseline/SKILL.md +108 -0
- package/skills/platform/gh/refs/gh.md +105 -0
- package/skills/platform/gh-cli/SKILL.md +120 -0
- package/skills/platform/gh-cli/refs/automation.md +63 -0
- package/skills/platform/gh-cli/refs/gh-ops.md +103 -0
- package/skills/platform/gh-cli/refs/patterns.md +132 -0
- package/skills/platform/gh-cli/scripts/gh_ops.py +454 -0
- package/skills/platform/github-actions/SKILL.md +159 -0
- package/skills/platform/github-actions/refs/security.md +150 -0
- package/skills/platform/github-actions/refs/templates.md +230 -0
- package/skills/platform/github-actions/refs/troubleshooting.md +174 -0
- package/skills/platform/github-actions/scripts/workflow_gen.py +215 -0
- package/skills/platform/gitlab-ci/SKILL.md +140 -0
- package/skills/platform/gitlab-ci/refs/templates.md +223 -0
- package/skills/platform/gitlab-ci/refs/troubleshooting.md +213 -0
- package/skills/platform/glab-cli/SKILL.md +133 -0
- package/skills/platform/glab-cli/refs/ci-debugging.md +95 -0
- package/skills/platform/glab-cli/refs/glab-ops.md +78 -0
- package/skills/platform/glab-cli/refs/patterns.md +131 -0
- package/skills/platform/glab-cli/scripts/glab_ops.py +313 -0
- package/skills/platform/health/SKILL.md +177 -0
- package/skills/platform/health/refs/health.md +72 -0
- package/skills/platform/health/refs/sources.md +302 -0
- package/skills/platform/incident/refs/incident.md +136 -0
- package/skills/platform/infra/refs/infra.md +104 -0
- package/skills/platform/observability/SKILL.md +128 -0
- package/skills/platform/observability/refs/toolchain-discovery.md +243 -0
- package/skills/platform/prereq-doctor/SKILL.md +57 -0
- package/skills/platform/prereq-doctor/refs/tool-registry.md +58 -0
- package/skills/platform/traces/refs/traces.md +101 -0
- package/skills/product/acceptance-criteria/SKILL.md +193 -0
- package/skills/product/acceptance-criteria/refs/ac-anti-patterns.md +291 -0
- package/skills/product/acceptance-criteria/refs/ac-examples.md +277 -0
- package/skills/product/accessibility/SKILL.md +197 -0
- package/skills/product/accessibility/refs/aria-patterns-basics.md +250 -0
- package/skills/product/accessibility/refs/aria-patterns-dynamic.md +187 -0
- package/skills/product/accessibility/refs/aria-patterns-interactive.md +246 -0
- package/skills/product/accessibility/refs/audit-report-findings.md +248 -0
- package/skills/product/accessibility/refs/audit-report-results.md +276 -0
- package/skills/product/accessibility/refs/common-violations-html-css.md +270 -0
- package/skills/product/accessibility/refs/common-violations-interactive.md +303 -0
- package/skills/product/accessibility/refs/keyboard-testing-basics.md +222 -0
- package/skills/product/accessibility/refs/keyboard-testing-patterns.md +173 -0
- package/skills/product/accessibility/refs/screen-reader-testing-advanced.md +294 -0
- package/skills/product/accessibility/refs/screen-reader-testing-commands.md +255 -0
- package/skills/product/accessibility/refs/wcag-checklist.md +262 -0
- package/skills/product/analyze/SKILL.md +164 -0
- package/skills/product/analyze/refs/algorithms.md +238 -0
- package/skills/product/analyze/refs/sentiment-patterns.md +180 -0
- package/skills/product/imagery/SKILL.md +97 -0
- package/skills/product/imagery/alter/SKILL.md +105 -0
- package/skills/product/imagery/alter/refs/editing-reference.md +146 -0
- package/skills/product/imagery/create/SKILL.md +109 -0
- package/skills/product/imagery/create/refs/prompt-engineering.md +112 -0
- package/skills/product/imagery/create/refs/provider-reference.md +219 -0
- package/skills/product/imagery/review/SKILL.md +101 -0
- package/skills/product/imagery/review/refs/analysis-lenses.md +136 -0
- package/skills/product/imagery/review/refs/quality-gates.md +153 -0
- package/skills/product/imagery/scripts/analyze.sh +23 -0
- package/skills/product/imagery/scripts/edit.sh +14 -0
- package/skills/product/imagery/scripts/generate.sh +13 -0
- package/skills/product/imagery/scripts/provider.py +787 -0
- package/skills/product/listen/SKILL.md +175 -0
- package/skills/product/listen/refs/channels.md +220 -0
- package/skills/product/mockup/SKILL.md +106 -0
- package/skills/product/mockup/refs/templates.md +95 -0
- package/skills/product/refs/a11y.md +109 -0
- package/skills/product/refs/acceptance.md +79 -0
- package/skills/product/refs/align.md +67 -0
- package/skills/product/refs/analyze.md +77 -0
- package/skills/product/refs/elicit.md +67 -0
- package/skills/product/refs/listen.md +58 -0
- package/skills/product/refs/mockup.md +77 -0
- package/skills/product/refs/screenshot.md +59 -0
- package/skills/product/refs/strategy.md +77 -0
- package/skills/product/refs/synthesize.md +54 -0
- package/skills/product/refs/ux-review.md +68 -0
- package/skills/product/refs/ux.md +101 -0
- package/skills/product/requirements-analysis/SKILL.md +136 -0
- package/skills/product/requirements-analysis/refs/requirements-example-auth.md +264 -0
- package/skills/product/requirements-analysis/refs/requirements-example-export-integration.md +88 -0
- package/skills/product/requirements-analysis/refs/requirements-output-format-template.md +247 -0
- package/skills/product/requirements-analysis/refs/user-story-guide.md +296 -0
- package/skills/product/requirements-graph/SKILL.md +155 -0
- package/skills/product/requirements-graph/refs/examples.md +223 -0
- package/skills/product/requirements-graph/refs/schema.md +162 -0
- package/skills/product/requirements-migrate/SKILL.md +90 -0
- package/skills/product/requirements-navigate/SKILL.md +141 -0
- package/skills/product/requirements-navigate/refs/meta-templates.md +141 -0
- package/skills/product/screenshot/SKILL.md +147 -0
- package/skills/product/strategy/SKILL.md +198 -0
- package/skills/product/strategy/refs/frameworks.md +295 -0
- package/skills/product/strategy/refs/templates-competitive-market.md +299 -0
- package/skills/product/strategy/refs/templates-roi-value.md +207 -0
- package/skills/product/synthesize/SKILL.md +116 -0
- package/skills/product/synthesize/refs/journey-mapping.md +212 -0
- package/skills/product/synthesize/refs/prioritization.md +193 -0
- package/skills/product/ux-review/SKILL.md +164 -0
- package/skills/product/visual-direction/SKILL.md +137 -0
- package/skills/product/visual-review/SKILL.md +131 -0
- package/skills/runtime-exec/SKILL.md +138 -0
- package/skills/search/codebase-narrator/SKILL.md +131 -0
- package/skills/search/codebase-narrator/refs/output-template.md +100 -0
- package/skills/smaht/SKILL.md +72 -0
- package/skills/smaht/discovery/SKILL.md +65 -0
- package/skills/smaht/intent/SKILL.md +89 -0
- package/skills/smaht/propose-skills/SKILL.md +142 -0
- package/skills/smaht/refs/state.md +76 -0
- package/skills/swarm/SKILL.md +132 -0
- package/skills/swarm/refs/fan-out.md +104 -0
- package/skills/swarm/refs/independent-verification.md +86 -0
- package/skills/swarm/refs/receipts-and-evidence.md +69 -0
- package/skills/swarm/refs/recon-synthesis.md +87 -0
- package/skills/swarm/refs/ship-discipline.md +90 -0
- package/skills/wickedizer/SKILL.md +147 -0
- package/skills/wickedizer/refs/artifact-types.md +238 -0
- package/skills/wickedizer/refs/complexity.md +278 -0
- package/skills/wickedizer/refs/de-ai-pass.md +140 -0
- package/skills/wickedizer/refs/failure-modes.md +249 -0
- package/skills/wickedizer/refs/patterns.md +282 -0
- package/skills/wickedizer/refs/review.md +221 -0
- package/skills/wickedizer/refs/structure.md +282 -0
- package/skills/wickedizer/refs/voice-profile.md +125 -0
- package/skills/workflow/SKILL.md +132 -0
- package/skills/workflow/refs/archetype-adjustments.md +205 -0
- package/skills/workflow/refs/evidence.md +58 -0
- package/skills/workflow/refs/integration.md +130 -0
- package/skills/workflow/refs/risk-dimension-signals.md +260 -0
- package/skills/workflow/refs/scoring-rubric.md +49 -0
- package/skills/workflow/refs/signal-keywords.md +176 -0
- package/skills/workflow/refs/specialist-routing-rules.md +159 -0
- package/skills/worktrees/SKILL.md +87 -0
- package/skills/worktrees/refs/recipes.md +96 -0
|
@@ -0,0 +1,1214 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
guard_pipeline.py — Autonomous session-close guard evaluation (Issue #448).
|
|
4
|
+
|
|
5
|
+
Runs a tiered profile of surface-level verification checks at session close
|
|
6
|
+
(and optionally at build-phase approval). Does NOT hard-block — surfaces
|
|
7
|
+
findings to the session summary + wicked-bus. Fail-open everywhere.
|
|
8
|
+
|
|
9
|
+
Scope limits (important):
|
|
10
|
+
* All checks are intentionally cheap. This is NOT a substitute for
|
|
11
|
+
engineering:review, crew:gate, or a proper static-analysis run.
|
|
12
|
+
* R1-R6 scan uses regex/line-pattern heuristics only — high recall for
|
|
13
|
+
obvious tells, zero semantic understanding. Document this limitation
|
|
14
|
+
in any downstream report.
|
|
15
|
+
* Semantic-reviewer integration (#444) is optional and imported lazily.
|
|
16
|
+
If unavailable we emit a "semantic-review-unavailable" finding with
|
|
17
|
+
severity=info and continue.
|
|
18
|
+
|
|
19
|
+
The 5 checks:
|
|
20
|
+
1. bulletproof_scan — R1-R6 surface heuristics on changed files
|
|
21
|
+
2. debug_artifacts — print/console.log/pdb/breakpoint leftovers
|
|
22
|
+
3. adr_constraints — ADR MUST/MUST NOT phrases vs the diff
|
|
23
|
+
4. semantic_review — delegates to scripts.qe.semantic_review (#444)
|
|
24
|
+
5. skip_log — unresolved skip-reeval entries (audit_skip_log.py)
|
|
25
|
+
|
|
26
|
+
Usage (as a module):
|
|
27
|
+
from platform.guard_pipeline import run_pipeline
|
|
28
|
+
report = run_pipeline(profile_name="scalpel", cwd=Path.cwd())
|
|
29
|
+
|
|
30
|
+
Usage (as a CLI — for debugging):
|
|
31
|
+
python3 guard_pipeline.py run --profile scalpel
|
|
32
|
+
python3 guard_pipeline.py run --profile standard --project-dir ./some/proj
|
|
33
|
+
|
|
34
|
+
Frozen R1-R6 reference (copied from CLAUDE.md, single source of truth):
|
|
35
|
+
R1 — no dead code (commented-out blocks, unreferenced exports)
|
|
36
|
+
R2 — no bare panics (`raise Exception`, bare `panic!`, process.exit, System.exit)
|
|
37
|
+
R3 — no magic values (literal numbers in conditionals/returns without names)
|
|
38
|
+
R4 — no swallowed errors (`except: pass`, empty catch, `// swallow`)
|
|
39
|
+
R5 — no unbounded ops (while True w/o break, for without range, unbounded recursion hints)
|
|
40
|
+
R6 — no god functions (> 80 lines, > 8 parameters in a single def)
|
|
41
|
+
|
|
42
|
+
These are heuristics. False-positives are expected — the guard pipeline
|
|
43
|
+
surfaces them as findings, not blockers. Engineering review owns final calls.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
import re
|
|
51
|
+
import subprocess
|
|
52
|
+
import sys
|
|
53
|
+
import time
|
|
54
|
+
from dataclasses import asdict, dataclass, field
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Path setup — allow sibling imports (guard_profiles + scripts/_bus etc.)
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
_THIS_DIR = Path(__file__).resolve().parent
|
|
63
|
+
_SCRIPTS_ROOT = _THIS_DIR.parent
|
|
64
|
+
sys.path.insert(0, str(_SCRIPTS_ROOT))
|
|
65
|
+
sys.path.insert(0, str(_THIS_DIR))
|
|
66
|
+
|
|
67
|
+
from guard_profiles import ( # noqa: E402 (sys.path manipulation above)
|
|
68
|
+
GuardProfile,
|
|
69
|
+
SCALPEL,
|
|
70
|
+
auto_select,
|
|
71
|
+
get_profile,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Constants
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
PIPELINE_VERSION = "1.0"
|
|
80
|
+
|
|
81
|
+
# Severity levels — keep in sync with briefing renderer.
|
|
82
|
+
SEVERITY_BLOCK = "block" # would be a hard-block in a full gate (we still fail-open)
|
|
83
|
+
SEVERITY_WARN = "warn" # advisory
|
|
84
|
+
SEVERITY_INFO = "info" # informational (e.g. "scan skipped — no files")
|
|
85
|
+
|
|
86
|
+
# Per-check time budget fraction of the profile budget. Leaves headroom for
|
|
87
|
+
# emission and summary assembly.
|
|
88
|
+
_PER_CHECK_BUDGET_FRACTION = 0.8
|
|
89
|
+
|
|
90
|
+
# Files/dirs we always skip when scanning the diff.
|
|
91
|
+
_SKIP_PARTS = frozenset({
|
|
92
|
+
"node_modules", ".git", "__pycache__", ".venv", "venv", "dist", "build",
|
|
93
|
+
".tox", ".mypy_cache", ".ruff_cache", ".pytest_cache", ".claude",
|
|
94
|
+
"htmlcov", "coverage", ".next", ".nuxt", "target",
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
# Extensions in scope for bulletproof/debug scans.
|
|
98
|
+
_CODE_EXTENSIONS = frozenset({
|
|
99
|
+
".py", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
|
|
100
|
+
".rs", ".go", ".java", ".kt", ".rb", ".cs",
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
# Max lines we read per file — an unsanitized megafile shouldn't burn our budget.
|
|
104
|
+
_MAX_LINES_PER_FILE = 5000
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Data model
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class Finding:
|
|
113
|
+
"""A single guard-pipeline finding.
|
|
114
|
+
|
|
115
|
+
`rule_id` is the sub-rule (e.g. "R2", "adr-MUST-NOT"). `severity` is one
|
|
116
|
+
of the SEVERITY_* constants. `file` and `line` are optional — checks may
|
|
117
|
+
aggregate findings without a pinned location.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
check: str
|
|
121
|
+
rule_id: str
|
|
122
|
+
severity: str
|
|
123
|
+
message: str
|
|
124
|
+
file: Optional[str] = None
|
|
125
|
+
line: Optional[int] = None
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
128
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class CheckResult:
|
|
133
|
+
"""Result of a single check: findings + timing + status."""
|
|
134
|
+
|
|
135
|
+
name: str
|
|
136
|
+
status: str # "ok" | "skip" | "error"
|
|
137
|
+
findings: List[Finding] = field(default_factory=list)
|
|
138
|
+
duration_ms: int = 0
|
|
139
|
+
note: Optional[str] = None # short human reason (e.g. "no changed files")
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
142
|
+
return {
|
|
143
|
+
"name": self.name,
|
|
144
|
+
"status": self.status,
|
|
145
|
+
"duration_ms": self.duration_ms,
|
|
146
|
+
"note": self.note,
|
|
147
|
+
"findings": [f.to_dict() for f in self.findings],
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class PipelineReport:
|
|
153
|
+
"""Aggregated guard-pipeline output."""
|
|
154
|
+
|
|
155
|
+
pipeline_version: str
|
|
156
|
+
profile: str
|
|
157
|
+
budget_seconds: float
|
|
158
|
+
duration_ms: int
|
|
159
|
+
status: str # "ok" | "budget_exceeded" | "error"
|
|
160
|
+
checks: List[CheckResult] = field(default_factory=list)
|
|
161
|
+
findings_by_severity: Dict[str, int] = field(default_factory=dict)
|
|
162
|
+
total_findings: int = 0
|
|
163
|
+
|
|
164
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
165
|
+
return {
|
|
166
|
+
"pipeline_version": self.pipeline_version,
|
|
167
|
+
"profile": self.profile,
|
|
168
|
+
"budget_seconds": self.budget_seconds,
|
|
169
|
+
"duration_ms": self.duration_ms,
|
|
170
|
+
"status": self.status,
|
|
171
|
+
"total_findings": self.total_findings,
|
|
172
|
+
"findings_by_severity": self.findings_by_severity,
|
|
173
|
+
"checks": [c.to_dict() for c in self.checks],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# Diff helpers
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def _path_in_scope(path: str) -> bool:
|
|
182
|
+
"""Skip vendored/generated/irrelevant paths."""
|
|
183
|
+
parts = Path(path).parts
|
|
184
|
+
return not (set(parts) & _SKIP_PARTS)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _changed_files(cwd: Optional[Path] = None) -> List[str]:
|
|
188
|
+
"""Return changed files — staged + unstaged + diff vs HEAD~1. Deduped."""
|
|
189
|
+
cwdp = str(cwd) if cwd else None
|
|
190
|
+
files: List[str] = []
|
|
191
|
+
seen: set = set()
|
|
192
|
+
|
|
193
|
+
def _collect(cmd: List[str]) -> None:
|
|
194
|
+
try:
|
|
195
|
+
result = subprocess.run(cmd, cwd=cwdp, capture_output=True, text=True, timeout=3)
|
|
196
|
+
if result.returncode == 0:
|
|
197
|
+
for line in result.stdout.splitlines():
|
|
198
|
+
line = line.strip()
|
|
199
|
+
if not line or line in seen:
|
|
200
|
+
continue
|
|
201
|
+
if not _path_in_scope(line):
|
|
202
|
+
continue
|
|
203
|
+
# Resolve against cwd so downstream reads succeed
|
|
204
|
+
full = str((cwd or Path.cwd()) / line) if cwd else line
|
|
205
|
+
if os.path.isfile(full):
|
|
206
|
+
seen.add(line)
|
|
207
|
+
files.append(full)
|
|
208
|
+
except Exception:
|
|
209
|
+
pass # fail open — we just return whatever we have so far
|
|
210
|
+
|
|
211
|
+
_collect(["git", "diff", "--name-only", "HEAD"])
|
|
212
|
+
_collect(["git", "diff", "--name-only", "--cached"])
|
|
213
|
+
return files
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _read_lines(path: str) -> List[str]:
|
|
217
|
+
"""Read up to _MAX_LINES_PER_FILE lines. Empty on error."""
|
|
218
|
+
try:
|
|
219
|
+
text = Path(path).read_text(encoding="utf-8", errors="replace")
|
|
220
|
+
lines = text.splitlines()
|
|
221
|
+
return lines[:_MAX_LINES_PER_FILE]
|
|
222
|
+
except Exception:
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# Check 1: bulletproof_scan (R1-R6 surface heuristics)
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
# Each entry: (rule_id, regex, extensions, message, severity)
|
|
231
|
+
# Extensions=None means "any code file".
|
|
232
|
+
_BULLETPROOF_PATTERNS: List[Tuple[str, re.Pattern, Optional[frozenset], str, str]] = [
|
|
233
|
+
# R2 — bare panics
|
|
234
|
+
("R2", re.compile(r"\braise\s+Exception\s*\("), frozenset({".py"}),
|
|
235
|
+
"R2: bare `raise Exception(...)` — use a typed exception", SEVERITY_WARN),
|
|
236
|
+
("R2", re.compile(r"\bpanic!\s*\("), frozenset({".rs"}),
|
|
237
|
+
"R2: bare `panic!` — prefer Result/Error propagation", SEVERITY_WARN),
|
|
238
|
+
("R2", re.compile(r"\bprocess\.exit\s*\("),
|
|
239
|
+
frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}),
|
|
240
|
+
"R2: `process.exit(...)` in library code — throw instead", SEVERITY_WARN),
|
|
241
|
+
("R2", re.compile(r"\bSystem\.exit\s*\("), frozenset({".java", ".kt"}),
|
|
242
|
+
"R2: `System.exit(...)` — throw or return", SEVERITY_WARN),
|
|
243
|
+
|
|
244
|
+
# R4 — swallowed errors (single-line form; multiline form handled separately)
|
|
245
|
+
("R4", re.compile(r"except[^:]*:\s*pass\s*(#.*)?$"), frozenset({".py"}),
|
|
246
|
+
"R4: bare `except: pass` — swallowed exception", SEVERITY_WARN),
|
|
247
|
+
("R4", re.compile(r"catch\s*\([^)]*\)\s*\{\s*\}"),
|
|
248
|
+
frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".java", ".kt", ".cs"}),
|
|
249
|
+
"R4: empty catch block — swallowed exception", SEVERITY_WARN),
|
|
250
|
+
("R4", re.compile(r"//\s*swallow|//\s*ignore\s+err"), None,
|
|
251
|
+
"R4: explicit error-swallowing comment", SEVERITY_WARN),
|
|
252
|
+
|
|
253
|
+
# R5 — unbounded ops (structural hint, not semantic)
|
|
254
|
+
("R5", re.compile(r"\bwhile\s+True\s*:"), frozenset({".py"}),
|
|
255
|
+
"R5: `while True:` — confirm the loop has a bounded exit", SEVERITY_INFO),
|
|
256
|
+
("R5", re.compile(r"\bwhile\s*\(\s*true\s*\)\s*\{"),
|
|
257
|
+
frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".java", ".kt", ".cs"}),
|
|
258
|
+
"R5: `while (true)` — confirm the loop has a bounded exit", SEVERITY_INFO),
|
|
259
|
+
|
|
260
|
+
# R1 — dead code hint (commented-out blocks of 3+ consecutive lines)
|
|
261
|
+
# We do NOT try to detect via regex — handled in a separate pass below.
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
_EXCEPT_RE = re.compile(r"^(\s*)except\b[^:]*:\s*(#.*)?$")
|
|
266
|
+
_BODY_PASS_RE = re.compile(r"^\s*pass\s*(#.*)?$")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _detect_multiline_swallow(path: str, lines: List[str]) -> List[Finding]:
|
|
270
|
+
"""R4: flag multi-line `except ...:\n pass` patterns (Python)."""
|
|
271
|
+
findings: List[Finding] = []
|
|
272
|
+
if not path.endswith(".py"):
|
|
273
|
+
return findings
|
|
274
|
+
for i, line in enumerate(lines):
|
|
275
|
+
m = _EXCEPT_RE.match(line)
|
|
276
|
+
if not m:
|
|
277
|
+
continue
|
|
278
|
+
# Find the next non-blank, non-comment line
|
|
279
|
+
for j in range(i + 1, min(i + 5, len(lines))):
|
|
280
|
+
nxt = lines[j]
|
|
281
|
+
stripped = nxt.strip()
|
|
282
|
+
if not stripped or stripped.startswith("#"):
|
|
283
|
+
continue
|
|
284
|
+
if _BODY_PASS_RE.match(nxt):
|
|
285
|
+
findings.append(Finding(
|
|
286
|
+
check="bulletproof_scan", rule_id="R4", severity=SEVERITY_WARN,
|
|
287
|
+
message="R4: `except ...: pass` (multi-line) — swallowed exception",
|
|
288
|
+
file=path, line=i + 1,
|
|
289
|
+
))
|
|
290
|
+
break
|
|
291
|
+
return findings
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _count_python_god_functions(path: str, lines: List[str]) -> List[Finding]:
|
|
295
|
+
"""R6: flag Python defs with > 80 lines or > 8 params."""
|
|
296
|
+
findings: List[Finding] = []
|
|
297
|
+
def_re = re.compile(r"^(\s*)def\s+(\w+)\s*\(([^)]*)\)")
|
|
298
|
+
i = 0
|
|
299
|
+
while i < len(lines):
|
|
300
|
+
m = def_re.match(lines[i])
|
|
301
|
+
if not m:
|
|
302
|
+
i += 1
|
|
303
|
+
continue
|
|
304
|
+
indent = len(m.group(1))
|
|
305
|
+
name = m.group(2)
|
|
306
|
+
# Count params — crude but stdlib-only.
|
|
307
|
+
params_blob = m.group(3).strip()
|
|
308
|
+
param_count = 0
|
|
309
|
+
if params_blob:
|
|
310
|
+
# Ignore `self` / `cls` and default-value commas inside brackets.
|
|
311
|
+
depth = 0
|
|
312
|
+
current = ""
|
|
313
|
+
parts: List[str] = []
|
|
314
|
+
for ch in params_blob:
|
|
315
|
+
if ch in "([{":
|
|
316
|
+
depth += 1
|
|
317
|
+
elif ch in ")]}":
|
|
318
|
+
depth -= 1
|
|
319
|
+
if ch == "," and depth == 0:
|
|
320
|
+
parts.append(current.strip())
|
|
321
|
+
current = ""
|
|
322
|
+
else:
|
|
323
|
+
current += ch
|
|
324
|
+
if current.strip():
|
|
325
|
+
parts.append(current.strip())
|
|
326
|
+
parts = [p for p in parts if p and p not in ("self", "cls")]
|
|
327
|
+
param_count = len(parts)
|
|
328
|
+
if param_count > 8:
|
|
329
|
+
findings.append(Finding(
|
|
330
|
+
check="bulletproof_scan", rule_id="R6", severity=SEVERITY_WARN,
|
|
331
|
+
message=f"R6: function `{name}` has {param_count} parameters (> 8)",
|
|
332
|
+
file=path, line=i + 1,
|
|
333
|
+
))
|
|
334
|
+
# Find function end — next line at <= indent that isn't blank
|
|
335
|
+
body_start = i + 1
|
|
336
|
+
end = len(lines)
|
|
337
|
+
for j in range(body_start, len(lines)):
|
|
338
|
+
stripped = lines[j].rstrip()
|
|
339
|
+
if not stripped:
|
|
340
|
+
continue
|
|
341
|
+
leading = len(lines[j]) - len(lines[j].lstrip())
|
|
342
|
+
if leading <= indent:
|
|
343
|
+
end = j
|
|
344
|
+
break
|
|
345
|
+
body_len = end - i
|
|
346
|
+
if body_len > 80:
|
|
347
|
+
findings.append(Finding(
|
|
348
|
+
check="bulletproof_scan", rule_id="R6", severity=SEVERITY_WARN,
|
|
349
|
+
message=f"R6: function `{name}` is {body_len} lines long (> 80)",
|
|
350
|
+
file=path, line=i + 1,
|
|
351
|
+
))
|
|
352
|
+
i = max(end, i + 1)
|
|
353
|
+
return findings
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _count_commented_blocks(path: str, lines: List[str]) -> List[Finding]:
|
|
357
|
+
"""R1: flag runs of >= 3 consecutive commented-out code lines."""
|
|
358
|
+
findings: List[Finding] = []
|
|
359
|
+
ext = Path(path).suffix.lower()
|
|
360
|
+
comment_re: Optional[re.Pattern] = None
|
|
361
|
+
if ext == ".py":
|
|
362
|
+
# Python: commented line that looks like code (contains `=`, `(`, `:`)
|
|
363
|
+
comment_re = re.compile(r"^\s*#\s*[A-Za-z_].*[=(:]")
|
|
364
|
+
elif ext in {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
|
|
365
|
+
".rs", ".go", ".java", ".kt", ".cs"}:
|
|
366
|
+
comment_re = re.compile(r"^\s*//\s*[A-Za-z_].*[=(;{]")
|
|
367
|
+
if comment_re is None:
|
|
368
|
+
return findings
|
|
369
|
+
|
|
370
|
+
run_start: Optional[int] = None
|
|
371
|
+
run_len = 0
|
|
372
|
+
for idx, line in enumerate(lines):
|
|
373
|
+
if comment_re.match(line):
|
|
374
|
+
if run_start is None:
|
|
375
|
+
run_start = idx
|
|
376
|
+
run_len += 1
|
|
377
|
+
else:
|
|
378
|
+
if run_len >= 3 and run_start is not None:
|
|
379
|
+
findings.append(Finding(
|
|
380
|
+
check="bulletproof_scan", rule_id="R1", severity=SEVERITY_INFO,
|
|
381
|
+
message=f"R1: {run_len} consecutive commented-out code lines",
|
|
382
|
+
file=path, line=run_start + 1,
|
|
383
|
+
))
|
|
384
|
+
run_start = None
|
|
385
|
+
run_len = 0
|
|
386
|
+
if run_len >= 3 and run_start is not None:
|
|
387
|
+
findings.append(Finding(
|
|
388
|
+
check="bulletproof_scan", rule_id="R1", severity=SEVERITY_INFO,
|
|
389
|
+
message=f"R1: {run_len} consecutive commented-out code lines",
|
|
390
|
+
file=path, line=run_start + 1,
|
|
391
|
+
))
|
|
392
|
+
return findings
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
# R3 + R5 AST heuristics (Python only) — #462, Item 3
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
# R3 allow-list: numeric literals that are "boring idioms" we don't flag.
|
|
400
|
+
# 0, 1, -1 cover the vast majority of loop guards, sentinels, empty-checks,
|
|
401
|
+
# and index math where a named constant would be overkill.
|
|
402
|
+
_R3_ALLOWED_NUMBERS = frozenset({0, 1, -1})
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _detect_magic_values_r3(path: str, tree: "ast.AST") -> List[Finding]:
|
|
406
|
+
"""R3 — flag magic numeric literals in Compare nodes (Python AST).
|
|
407
|
+
|
|
408
|
+
Heuristic: walk every ast.Compare and flag right-hand-side comparators
|
|
409
|
+
whose value is a numeric literal NOT in {0, 1, -1}. Examples:
|
|
410
|
+
|
|
411
|
+
if x > 42: # flagged: 42 is magic
|
|
412
|
+
if n == 0: # NOT flagged: 0 is allow-listed
|
|
413
|
+
if x == -1: # NOT flagged: -1 is allow-listed
|
|
414
|
+
if count > THRESHOLD: # NOT flagged: Name, not Constant
|
|
415
|
+
|
|
416
|
+
Conservative on purpose — we expect false positives (e.g. HTTP status
|
|
417
|
+
codes 200/400/404 in test fixtures) and treat them as info/warn
|
|
418
|
+
surface, not a blocker. Engineering review owns final calls.
|
|
419
|
+
|
|
420
|
+
Performance: AST walk is O(nodes); on typical Python files this runs
|
|
421
|
+
in sub-millisecond time. Pre-parsed tree is passed in so the caller
|
|
422
|
+
can reuse it across R3/R5 without re-parsing.
|
|
423
|
+
"""
|
|
424
|
+
import ast # stdlib — local import keeps module-level cost unchanged
|
|
425
|
+
findings: List[Finding] = []
|
|
426
|
+
|
|
427
|
+
for node in ast.walk(tree):
|
|
428
|
+
if not isinstance(node, ast.Compare):
|
|
429
|
+
continue
|
|
430
|
+
# Each Compare can have multiple comparators (e.g. `a < b < c`);
|
|
431
|
+
# we scan each comparator, not just the first.
|
|
432
|
+
for comparator in node.comparators:
|
|
433
|
+
# ast.Constant covers int, float, bool. We want numerics
|
|
434
|
+
# only (exclude bools — they are technically ints but are
|
|
435
|
+
# never "magic").
|
|
436
|
+
if isinstance(comparator, ast.Constant):
|
|
437
|
+
value = comparator.value
|
|
438
|
+
if isinstance(value, bool):
|
|
439
|
+
continue
|
|
440
|
+
if isinstance(value, (int, float)) and value not in _R3_ALLOWED_NUMBERS:
|
|
441
|
+
line_no = getattr(comparator, "lineno", getattr(node, "lineno", 0))
|
|
442
|
+
findings.append(Finding(
|
|
443
|
+
check="bulletproof_scan",
|
|
444
|
+
rule_id="R3",
|
|
445
|
+
severity=SEVERITY_WARN,
|
|
446
|
+
message=f"R3: magic value {value} in conditional at {path}:{line_no}",
|
|
447
|
+
file=path,
|
|
448
|
+
line=line_no,
|
|
449
|
+
))
|
|
450
|
+
# Also handle ast.UnaryOp wrapping a Constant (e.g. `-42`).
|
|
451
|
+
# -1 is allow-listed above as a plain value; here we catch
|
|
452
|
+
# `-42`, `-100`, etc. that the parser emits as UnaryOp(USub).
|
|
453
|
+
elif isinstance(comparator, ast.UnaryOp) and isinstance(comparator.op, ast.USub):
|
|
454
|
+
operand = comparator.operand
|
|
455
|
+
if isinstance(operand, ast.Constant) and isinstance(operand.value, (int, float)) \
|
|
456
|
+
and not isinstance(operand.value, bool):
|
|
457
|
+
effective = -operand.value
|
|
458
|
+
if effective in _R3_ALLOWED_NUMBERS:
|
|
459
|
+
continue
|
|
460
|
+
line_no = getattr(comparator, "lineno", getattr(node, "lineno", 0))
|
|
461
|
+
findings.append(Finding(
|
|
462
|
+
check="bulletproof_scan",
|
|
463
|
+
rule_id="R3",
|
|
464
|
+
severity=SEVERITY_WARN,
|
|
465
|
+
message=f"R3: magic value {effective} in conditional at {path}:{line_no}",
|
|
466
|
+
file=path,
|
|
467
|
+
line=line_no,
|
|
468
|
+
))
|
|
469
|
+
return findings
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _func_has_guard_before_call(func: "ast.AST", func_name: str) -> bool:
|
|
473
|
+
"""Return True if the function body has an early-return / conditional-return
|
|
474
|
+
BEFORE any recursive self-call. Heuristic used by R5.
|
|
475
|
+
|
|
476
|
+
Walks top-level statements in the function body in order. A statement
|
|
477
|
+
counts as a "guard" if it is:
|
|
478
|
+
* a top-level `return` (bounded-exit early-out)
|
|
479
|
+
* an `If` whose body contains a `return` at any nested level
|
|
480
|
+
* a `raise` (exit path that prevents recursion from running)
|
|
481
|
+
|
|
482
|
+
Statements after the first recursive call do not count.
|
|
483
|
+
"""
|
|
484
|
+
import ast
|
|
485
|
+
|
|
486
|
+
def _contains_recursive_call(node: "ast.AST") -> bool:
|
|
487
|
+
for sub in ast.walk(node):
|
|
488
|
+
if isinstance(sub, ast.Call):
|
|
489
|
+
callee = sub.func
|
|
490
|
+
if isinstance(callee, ast.Name) and callee.id == func_name:
|
|
491
|
+
return True
|
|
492
|
+
# Also flag `self.func_name(...)` / `Class.func_name(...)`
|
|
493
|
+
if isinstance(callee, ast.Attribute) and callee.attr == func_name:
|
|
494
|
+
return True
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
def _contains_return(node: "ast.AST") -> bool:
|
|
498
|
+
for sub in ast.walk(node):
|
|
499
|
+
if isinstance(sub, (ast.Return, ast.Raise)):
|
|
500
|
+
return True
|
|
501
|
+
return False
|
|
502
|
+
|
|
503
|
+
for stmt in func.body: # type: ignore[attr-defined]
|
|
504
|
+
# If the statement itself contains the recursive call, we've run
|
|
505
|
+
# out of guards — stop looking.
|
|
506
|
+
if _contains_recursive_call(stmt):
|
|
507
|
+
return False
|
|
508
|
+
# A bare `return` or `raise` at the top level is a guard.
|
|
509
|
+
if isinstance(stmt, (ast.Return, ast.Raise)):
|
|
510
|
+
return True
|
|
511
|
+
# An `if` whose body contains a return/raise is a guard.
|
|
512
|
+
if isinstance(stmt, ast.If) and _contains_return(stmt):
|
|
513
|
+
return True
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _detect_unbounded_recursion_r5(path: str, tree: "ast.AST") -> List[Finding]:
|
|
518
|
+
"""R5 — flag functions that call themselves with no guard clause.
|
|
519
|
+
|
|
520
|
+
Heuristic: for each FunctionDef / AsyncFunctionDef, check whether the
|
|
521
|
+
function calls `<own_name>(...)` anywhere in its body AND the body
|
|
522
|
+
lacks any top-level return/raise/if-return guard BEFORE the first
|
|
523
|
+
recursive call. Examples:
|
|
524
|
+
|
|
525
|
+
def f(n): def f(n):
|
|
526
|
+
return f(n - 1) if n <= 0:
|
|
527
|
+
return 0
|
|
528
|
+
return f(n - 1)
|
|
529
|
+
# flagged # NOT flagged (guard clause)
|
|
530
|
+
|
|
531
|
+
Performance: O(nodes) AST walk; matches R3 cost. Conservative — we
|
|
532
|
+
only look for same-name calls (not mutual recursion). Documented
|
|
533
|
+
limitation: this is a structural hint, not a proof of unboundedness.
|
|
534
|
+
False positives possible when the guard is deep inside a try/except
|
|
535
|
+
or a for-else clause. Engineering review owns final calls.
|
|
536
|
+
"""
|
|
537
|
+
import ast
|
|
538
|
+
findings: List[Finding] = []
|
|
539
|
+
|
|
540
|
+
for node in ast.walk(tree):
|
|
541
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
542
|
+
continue
|
|
543
|
+
name = node.name
|
|
544
|
+
# Does this function call itself anywhere?
|
|
545
|
+
calls_self = False
|
|
546
|
+
for sub in ast.walk(node):
|
|
547
|
+
if isinstance(sub, ast.Call):
|
|
548
|
+
callee = sub.func
|
|
549
|
+
if isinstance(callee, ast.Name) and callee.id == name:
|
|
550
|
+
calls_self = True
|
|
551
|
+
break
|
|
552
|
+
if isinstance(callee, ast.Attribute) and callee.attr == name:
|
|
553
|
+
calls_self = True
|
|
554
|
+
break
|
|
555
|
+
if not calls_self:
|
|
556
|
+
continue
|
|
557
|
+
# Guard present? — early return / raise / conditional return BEFORE
|
|
558
|
+
# the recursive call.
|
|
559
|
+
if _func_has_guard_before_call(node, name):
|
|
560
|
+
continue
|
|
561
|
+
line_no = getattr(node, "lineno", 0)
|
|
562
|
+
findings.append(Finding(
|
|
563
|
+
check="bulletproof_scan",
|
|
564
|
+
rule_id="R5",
|
|
565
|
+
severity=SEVERITY_WARN,
|
|
566
|
+
message=f"R5: possibly unbounded recursion in {name} at {path}:{line_no}",
|
|
567
|
+
file=path,
|
|
568
|
+
line=line_no,
|
|
569
|
+
))
|
|
570
|
+
return findings
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _run_python_ast_heuristics(path: str, lines: List[str]) -> List[Finding]:
|
|
574
|
+
"""Parse Python source once, then run R3 + R5 over the shared tree.
|
|
575
|
+
|
|
576
|
+
Returns an empty list on parse error — we do not attempt to salvage
|
|
577
|
+
partial findings from a syntactically-broken file. This respects the
|
|
578
|
+
per-check budget: a single parse failure costs ms, not seconds.
|
|
579
|
+
"""
|
|
580
|
+
import ast
|
|
581
|
+
try:
|
|
582
|
+
source = "\n".join(lines)
|
|
583
|
+
tree = ast.parse(source)
|
|
584
|
+
except SyntaxError:
|
|
585
|
+
return []
|
|
586
|
+
except Exception:
|
|
587
|
+
return []
|
|
588
|
+
findings: List[Finding] = []
|
|
589
|
+
findings.extend(_detect_magic_values_r3(path, tree))
|
|
590
|
+
findings.extend(_detect_unbounded_recursion_r5(path, tree))
|
|
591
|
+
return findings
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def check_bulletproof_scan(
|
|
595
|
+
files: List[str],
|
|
596
|
+
*,
|
|
597
|
+
budget_seconds: float,
|
|
598
|
+
**_kwargs: Any,
|
|
599
|
+
) -> CheckResult:
|
|
600
|
+
"""Run R1-R6 surface heuristics on code files."""
|
|
601
|
+
t0 = time.monotonic()
|
|
602
|
+
result = CheckResult(name="bulletproof_scan", status="ok")
|
|
603
|
+
|
|
604
|
+
if not files:
|
|
605
|
+
result.status = "skip"
|
|
606
|
+
result.note = "no changed files"
|
|
607
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
608
|
+
return result
|
|
609
|
+
|
|
610
|
+
deadline = t0 + budget_seconds
|
|
611
|
+
for filepath in files:
|
|
612
|
+
if time.monotonic() > deadline:
|
|
613
|
+
result.note = "budget exceeded — partial scan"
|
|
614
|
+
break
|
|
615
|
+
ext = Path(filepath).suffix.lower()
|
|
616
|
+
if ext not in _CODE_EXTENSIONS:
|
|
617
|
+
continue
|
|
618
|
+
lines = _read_lines(filepath)
|
|
619
|
+
if not lines:
|
|
620
|
+
continue
|
|
621
|
+
|
|
622
|
+
for line_num, line in enumerate(lines, start=1):
|
|
623
|
+
for rule_id, pattern, exts, msg, severity in _BULLETPROOF_PATTERNS:
|
|
624
|
+
if exts is not None and ext not in exts:
|
|
625
|
+
continue
|
|
626
|
+
if pattern.search(line):
|
|
627
|
+
result.findings.append(Finding(
|
|
628
|
+
check="bulletproof_scan", rule_id=rule_id, severity=severity,
|
|
629
|
+
message=msg, file=filepath, line=line_num,
|
|
630
|
+
))
|
|
631
|
+
|
|
632
|
+
if ext == ".py":
|
|
633
|
+
result.findings.extend(_count_python_god_functions(filepath, lines))
|
|
634
|
+
result.findings.extend(_detect_multiline_swallow(filepath, lines))
|
|
635
|
+
# R3 + R5 AST heuristics (#462, Item 3). Single parse shared
|
|
636
|
+
# across both rules. Fails open on SyntaxError — a broken file
|
|
637
|
+
# contributes zero R3/R5 findings rather than a scan failure.
|
|
638
|
+
result.findings.extend(_run_python_ast_heuristics(filepath, lines))
|
|
639
|
+
result.findings.extend(_count_commented_blocks(filepath, lines))
|
|
640
|
+
|
|
641
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
642
|
+
return result
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
# ---------------------------------------------------------------------------
|
|
646
|
+
# Check 2: debug_artifacts
|
|
647
|
+
# ---------------------------------------------------------------------------
|
|
648
|
+
|
|
649
|
+
_DEBUG_PATTERNS: List[Tuple[str, re.Pattern, Optional[frozenset], str]] = [
|
|
650
|
+
("debug.console", re.compile(r"\bconsole\.log\s*\("),
|
|
651
|
+
frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}), "console.log"),
|
|
652
|
+
("debug.debugger", re.compile(r"\bdebugger\s*;?"),
|
|
653
|
+
frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}), "debugger"),
|
|
654
|
+
("debug.pdb", re.compile(r"\bpdb\.set_trace\s*\("),
|
|
655
|
+
frozenset({".py"}), "pdb.set_trace"),
|
|
656
|
+
("debug.breakpoint", re.compile(r"\bbreakpoint\s*\("),
|
|
657
|
+
frozenset({".py"}), "breakpoint()"),
|
|
658
|
+
("debug.print", re.compile(r"^\s*print\s*\("),
|
|
659
|
+
frozenset({".py"}), "print()"),
|
|
660
|
+
]
|
|
661
|
+
|
|
662
|
+
# Files we tolerate prints in (scripts, tests, examples)
|
|
663
|
+
_PRINT_ALLOWED_PATTERNS = (
|
|
664
|
+
"/tests/", "/test_", "/examples/", "/scripts/",
|
|
665
|
+
"/hooks/", "cli.py", "__main__.py",
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _print_allowed(path: str) -> bool:
|
|
670
|
+
norm = path.replace("\\", "/")
|
|
671
|
+
return any(pat in norm for pat in _PRINT_ALLOWED_PATTERNS)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def check_debug_artifacts(
|
|
675
|
+
files: List[str],
|
|
676
|
+
*,
|
|
677
|
+
budget_seconds: float,
|
|
678
|
+
**_kwargs: Any,
|
|
679
|
+
) -> CheckResult:
|
|
680
|
+
"""Scan changed files for debug leftovers."""
|
|
681
|
+
t0 = time.monotonic()
|
|
682
|
+
result = CheckResult(name="debug_artifacts", status="ok")
|
|
683
|
+
|
|
684
|
+
if not files:
|
|
685
|
+
result.status = "skip"
|
|
686
|
+
result.note = "no changed files"
|
|
687
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
688
|
+
return result
|
|
689
|
+
|
|
690
|
+
deadline = t0 + budget_seconds
|
|
691
|
+
for filepath in files:
|
|
692
|
+
if time.monotonic() > deadline:
|
|
693
|
+
result.note = "budget exceeded — partial scan"
|
|
694
|
+
break
|
|
695
|
+
ext = Path(filepath).suffix.lower()
|
|
696
|
+
if ext not in _CODE_EXTENSIONS:
|
|
697
|
+
continue
|
|
698
|
+
lines = _read_lines(filepath)
|
|
699
|
+
for line_num, line in enumerate(lines, start=1):
|
|
700
|
+
for rule_id, pattern, exts, label in _DEBUG_PATTERNS:
|
|
701
|
+
if exts is not None and ext not in exts:
|
|
702
|
+
continue
|
|
703
|
+
# Suppress `print()` in files where it's expected (tests, scripts, hooks).
|
|
704
|
+
if label == "print()" and _print_allowed(filepath):
|
|
705
|
+
continue
|
|
706
|
+
if pattern.search(line):
|
|
707
|
+
result.findings.append(Finding(
|
|
708
|
+
check="debug_artifacts", rule_id=rule_id, severity=SEVERITY_WARN,
|
|
709
|
+
message=f"{label} left in changed file",
|
|
710
|
+
file=filepath, line=line_num,
|
|
711
|
+
))
|
|
712
|
+
|
|
713
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
714
|
+
return result
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
# ---------------------------------------------------------------------------
|
|
718
|
+
# Check 3: adr_constraints
|
|
719
|
+
# ---------------------------------------------------------------------------
|
|
720
|
+
|
|
721
|
+
# Matches "MUST", "MUST NOT", "SHALL NOT" etc. in uppercase context — RFC-2119 tone
|
|
722
|
+
_ADR_MUST_RE = re.compile(r"\b(MUST NOT|SHALL NOT|MUST|SHALL)\b\s+(.{5,140}?)(?:[.!?]|$)")
|
|
723
|
+
|
|
724
|
+
_ADR_SEARCH_ROOTS = ("docs/adr", "adr", "architecture/decisions", "docs/decisions")
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _find_adr_files(cwd: Path) -> List[Path]:
|
|
728
|
+
"""Return ADR markdown files under well-known roots (capped for budget)."""
|
|
729
|
+
files: List[Path] = []
|
|
730
|
+
for root in _ADR_SEARCH_ROOTS:
|
|
731
|
+
rootp = cwd / root
|
|
732
|
+
if not rootp.is_dir():
|
|
733
|
+
continue
|
|
734
|
+
try:
|
|
735
|
+
for p in rootp.rglob("*.md"):
|
|
736
|
+
if not _path_in_scope(str(p)):
|
|
737
|
+
continue
|
|
738
|
+
files.append(p)
|
|
739
|
+
if len(files) >= 50:
|
|
740
|
+
return files
|
|
741
|
+
except Exception:
|
|
742
|
+
continue
|
|
743
|
+
return files
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _extract_constraints(adr_files: List[Path]) -> List[Tuple[Path, str, str]]:
|
|
747
|
+
"""Return [(adr_path, verb, phrase), ...] for MUST/SHALL phrases."""
|
|
748
|
+
constraints: List[Tuple[Path, str, str]] = []
|
|
749
|
+
for adr in adr_files:
|
|
750
|
+
try:
|
|
751
|
+
text = adr.read_text(encoding="utf-8", errors="replace")
|
|
752
|
+
except Exception:
|
|
753
|
+
continue
|
|
754
|
+
for match in _ADR_MUST_RE.finditer(text):
|
|
755
|
+
verb = match.group(1)
|
|
756
|
+
phrase = match.group(2).strip()
|
|
757
|
+
if 5 <= len(phrase) <= 140:
|
|
758
|
+
constraints.append((adr, verb, phrase))
|
|
759
|
+
if len(constraints) >= 200:
|
|
760
|
+
return constraints
|
|
761
|
+
return constraints
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def check_adr_constraints(
|
|
765
|
+
files: List[str],
|
|
766
|
+
*,
|
|
767
|
+
budget_seconds: float,
|
|
768
|
+
cwd: Optional[Path] = None,
|
|
769
|
+
**_kwargs: Any,
|
|
770
|
+
) -> CheckResult:
|
|
771
|
+
"""Scan changed files for MUST-NOT phrase matches from ADRs.
|
|
772
|
+
|
|
773
|
+
Best-effort: finds literal substring matches of MUST-NOT phrases inside
|
|
774
|
+
the diff. False-positives are expected — surfaced as info, not warn.
|
|
775
|
+
"""
|
|
776
|
+
t0 = time.monotonic()
|
|
777
|
+
result = CheckResult(name="adr_constraints", status="ok")
|
|
778
|
+
|
|
779
|
+
base = cwd or Path.cwd()
|
|
780
|
+
adr_files = _find_adr_files(base)
|
|
781
|
+
if not adr_files:
|
|
782
|
+
result.status = "skip"
|
|
783
|
+
result.note = "no ADRs found"
|
|
784
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
785
|
+
return result
|
|
786
|
+
|
|
787
|
+
constraints = _extract_constraints(adr_files)
|
|
788
|
+
must_not = [(adr, phrase) for (adr, verb, phrase) in constraints if "NOT" in verb]
|
|
789
|
+
if not must_not:
|
|
790
|
+
result.status = "skip"
|
|
791
|
+
result.note = f"{len(adr_files)} ADRs scanned, no MUST-NOT constraints"
|
|
792
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
793
|
+
return result
|
|
794
|
+
|
|
795
|
+
if not files:
|
|
796
|
+
result.status = "skip"
|
|
797
|
+
result.note = "no changed files"
|
|
798
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
799
|
+
return result
|
|
800
|
+
|
|
801
|
+
deadline = t0 + budget_seconds
|
|
802
|
+
for filepath in files:
|
|
803
|
+
if time.monotonic() > deadline:
|
|
804
|
+
result.note = "budget exceeded — partial scan"
|
|
805
|
+
break
|
|
806
|
+
lines = _read_lines(filepath)
|
|
807
|
+
if not lines:
|
|
808
|
+
continue
|
|
809
|
+
# Combine into one blob for substring search — small files only
|
|
810
|
+
text = "\n".join(lines)
|
|
811
|
+
for adr, phrase in must_not:
|
|
812
|
+
# Take first word of the phrase as a cheap prefilter; fall back to full match
|
|
813
|
+
head = phrase.split()[0] if phrase.split() else phrase
|
|
814
|
+
if head.lower() in text.lower() and phrase.lower()[:40] in text.lower():
|
|
815
|
+
result.findings.append(Finding(
|
|
816
|
+
check="adr_constraints",
|
|
817
|
+
rule_id="adr-MUST-NOT",
|
|
818
|
+
severity=SEVERITY_WARN,
|
|
819
|
+
message=f"Changed file contains text matching ADR constraint: '{phrase[:80]}' (from {adr.name})",
|
|
820
|
+
file=filepath,
|
|
821
|
+
))
|
|
822
|
+
|
|
823
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
824
|
+
return result
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
# ---------------------------------------------------------------------------
|
|
828
|
+
# Check 4: semantic_review (lazy import — depends on PR #444 / #455)
|
|
829
|
+
# ---------------------------------------------------------------------------
|
|
830
|
+
|
|
831
|
+
def _call_semantic_reviewer(project_dir: Path, complexity: int) -> Optional[Dict[str, Any]]:
|
|
832
|
+
"""Import + call the semantic reviewer lazily. None on unavailable."""
|
|
833
|
+
try:
|
|
834
|
+
# Imports from scripts/qe/semantic_review.py — shipping in PR #455
|
|
835
|
+
sys.path.insert(0, str(_SCRIPTS_ROOT / "qe"))
|
|
836
|
+
from semantic_review import run_semantic_review # type: ignore
|
|
837
|
+
except Exception:
|
|
838
|
+
return None
|
|
839
|
+
|
|
840
|
+
try:
|
|
841
|
+
result = run_semantic_review(project_dir, complexity)
|
|
842
|
+
if isinstance(result, dict):
|
|
843
|
+
return result
|
|
844
|
+
except Exception:
|
|
845
|
+
return None
|
|
846
|
+
return None
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def check_semantic_review(
|
|
850
|
+
files: List[str],
|
|
851
|
+
*,
|
|
852
|
+
budget_seconds: float,
|
|
853
|
+
project_dir: Optional[Path] = None,
|
|
854
|
+
complexity: int = 3,
|
|
855
|
+
**_kwargs: Any,
|
|
856
|
+
) -> CheckResult:
|
|
857
|
+
"""Delegate to scripts.qe.semantic_review (PR #444). Fail-open."""
|
|
858
|
+
t0 = time.monotonic()
|
|
859
|
+
result = CheckResult(name="semantic_review", status="ok")
|
|
860
|
+
|
|
861
|
+
target = project_dir or Path.cwd()
|
|
862
|
+
report = _call_semantic_reviewer(target, complexity)
|
|
863
|
+
if report is None:
|
|
864
|
+
result.status = "skip"
|
|
865
|
+
result.note = "semantic-review-unavailable"
|
|
866
|
+
result.findings.append(Finding(
|
|
867
|
+
check="semantic_review",
|
|
868
|
+
rule_id="semantic-review-unavailable",
|
|
869
|
+
severity=SEVERITY_INFO,
|
|
870
|
+
message="semantic reviewer (scripts/qe/semantic_review.py) unavailable — skipped",
|
|
871
|
+
))
|
|
872
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
873
|
+
return result
|
|
874
|
+
|
|
875
|
+
verdict = str(report.get("verdict", "APPROVE")).upper()
|
|
876
|
+
for raw in report.get("findings", []) or []:
|
|
877
|
+
if not isinstance(raw, dict):
|
|
878
|
+
continue
|
|
879
|
+
sev = SEVERITY_WARN
|
|
880
|
+
if verdict == "REJECT":
|
|
881
|
+
sev = SEVERITY_BLOCK
|
|
882
|
+
elif verdict == "APPROVE":
|
|
883
|
+
sev = SEVERITY_INFO
|
|
884
|
+
msg = str(raw.get("message") or raw.get("summary") or raw.get("title") or "")[:200]
|
|
885
|
+
if not msg:
|
|
886
|
+
continue
|
|
887
|
+
result.findings.append(Finding(
|
|
888
|
+
check="semantic_review",
|
|
889
|
+
rule_id=str(raw.get("rule_id") or raw.get("kind") or "spec-drift"),
|
|
890
|
+
severity=sev,
|
|
891
|
+
message=msg,
|
|
892
|
+
file=raw.get("file"),
|
|
893
|
+
line=raw.get("line") if isinstance(raw.get("line"), int) else None,
|
|
894
|
+
))
|
|
895
|
+
|
|
896
|
+
if not result.findings and verdict != "APPROVE":
|
|
897
|
+
# Reviewer issued a verdict but no itemized findings — surface one rollup.
|
|
898
|
+
result.findings.append(Finding(
|
|
899
|
+
check="semantic_review",
|
|
900
|
+
rule_id="verdict",
|
|
901
|
+
severity=SEVERITY_WARN if verdict == "CONDITIONAL" else SEVERITY_BLOCK,
|
|
902
|
+
message=f"Semantic review verdict: {verdict} ({report.get('summary', '')[:120]})",
|
|
903
|
+
))
|
|
904
|
+
|
|
905
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
906
|
+
return result
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
# ---------------------------------------------------------------------------
|
|
910
|
+
# Check 5: skip_log (unresolved skip-reeval entries)
|
|
911
|
+
# ---------------------------------------------------------------------------
|
|
912
|
+
|
|
913
|
+
def _call_audit_skip_log(project_dir: Path) -> List[Dict[str, Any]]:
|
|
914
|
+
"""Import + call audit_skip_log.scan. Empty list on error."""
|
|
915
|
+
try:
|
|
916
|
+
sys.path.insert(0, str(_SCRIPTS_ROOT / "crew"))
|
|
917
|
+
from audit_skip_log import scan # type: ignore
|
|
918
|
+
entries = scan(project_dir)
|
|
919
|
+
return entries if isinstance(entries, list) else []
|
|
920
|
+
except Exception:
|
|
921
|
+
return []
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def check_skip_log(
|
|
925
|
+
files: List[str],
|
|
926
|
+
*,
|
|
927
|
+
budget_seconds: float,
|
|
928
|
+
project_dir: Optional[Path] = None,
|
|
929
|
+
**_kwargs: Any,
|
|
930
|
+
) -> CheckResult:
|
|
931
|
+
"""Surface unresolved skip-reeval-log.json entries."""
|
|
932
|
+
t0 = time.monotonic()
|
|
933
|
+
result = CheckResult(name="skip_log", status="ok")
|
|
934
|
+
|
|
935
|
+
target = project_dir
|
|
936
|
+
if target is None or not target.is_dir():
|
|
937
|
+
result.status = "skip"
|
|
938
|
+
result.note = "no crew project directory"
|
|
939
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
940
|
+
return result
|
|
941
|
+
|
|
942
|
+
entries = _call_audit_skip_log(target)
|
|
943
|
+
if not entries:
|
|
944
|
+
result.note = "no unresolved skip entries"
|
|
945
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
946
|
+
return result
|
|
947
|
+
|
|
948
|
+
for entry in entries[:20]:
|
|
949
|
+
phase = entry.get("_source_phase", "?")
|
|
950
|
+
reason = entry.get("reason") or entry.get("note") or "unresolved skip"
|
|
951
|
+
result.findings.append(Finding(
|
|
952
|
+
check="skip_log",
|
|
953
|
+
rule_id="skip-unresolved",
|
|
954
|
+
severity=SEVERITY_WARN,
|
|
955
|
+
message=f"Unresolved skip in phase `{phase}`: {str(reason)[:120]}",
|
|
956
|
+
))
|
|
957
|
+
|
|
958
|
+
if len(entries) > 20:
|
|
959
|
+
result.findings.append(Finding(
|
|
960
|
+
check="skip_log",
|
|
961
|
+
rule_id="skip-unresolved",
|
|
962
|
+
severity=SEVERITY_INFO,
|
|
963
|
+
message=f"... and {len(entries) - 20} more unresolved skip entries",
|
|
964
|
+
))
|
|
965
|
+
|
|
966
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
967
|
+
return result
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
# ---------------------------------------------------------------------------
|
|
971
|
+
# Registry + runner
|
|
972
|
+
# ---------------------------------------------------------------------------
|
|
973
|
+
|
|
974
|
+
CHECK_REGISTRY: Dict[str, Callable[..., CheckResult]] = {
|
|
975
|
+
"bulletproof_scan": check_bulletproof_scan,
|
|
976
|
+
"debug_artifacts": check_debug_artifacts,
|
|
977
|
+
"adr_constraints": check_adr_constraints,
|
|
978
|
+
"semantic_review": check_semantic_review,
|
|
979
|
+
"skip_log": check_skip_log,
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def run_pipeline(
|
|
984
|
+
profile_name: Optional[str] = None,
|
|
985
|
+
*,
|
|
986
|
+
cwd: Optional[Path] = None,
|
|
987
|
+
project_dir: Optional[Path] = None,
|
|
988
|
+
complexity: int = 3,
|
|
989
|
+
build_phase_just_closed: bool = False,
|
|
990
|
+
files: Optional[List[str]] = None,
|
|
991
|
+
) -> PipelineReport:
|
|
992
|
+
"""Run the guard pipeline end-to-end. Never raises.
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
profile_name: "scalpel" | "standard" | "deep". Auto-selects when None.
|
|
996
|
+
cwd: Working directory for git probes and relative paths. Defaults to
|
|
997
|
+
the process CWD.
|
|
998
|
+
project_dir: Crew project directory (for skip_log check). Optional.
|
|
999
|
+
complexity: Complexity score passed to the semantic reviewer.
|
|
1000
|
+
build_phase_just_closed: Promotes scalpel → standard via auto_select.
|
|
1001
|
+
files: Optional explicit file list (useful for tests). When None,
|
|
1002
|
+
uses `git diff --name-only`.
|
|
1003
|
+
|
|
1004
|
+
Returns:
|
|
1005
|
+
A fully populated PipelineReport. `status` is "ok" unless the
|
|
1006
|
+
total-budget deadline was exceeded ("budget_exceeded") or an
|
|
1007
|
+
unrecoverable error happened ("error").
|
|
1008
|
+
"""
|
|
1009
|
+
base = cwd or Path.cwd()
|
|
1010
|
+
t0 = time.monotonic()
|
|
1011
|
+
|
|
1012
|
+
if profile_name:
|
|
1013
|
+
profile: GuardProfile = get_profile(profile_name)
|
|
1014
|
+
else:
|
|
1015
|
+
profile = auto_select(build_phase_just_closed=build_phase_just_closed, cwd=base)
|
|
1016
|
+
|
|
1017
|
+
# Hard deadline for the whole pipeline
|
|
1018
|
+
deadline = t0 + profile.budget_seconds
|
|
1019
|
+
|
|
1020
|
+
# Share one file-list across all diff-driven checks
|
|
1021
|
+
try:
|
|
1022
|
+
change_set = files if files is not None else _changed_files(base)
|
|
1023
|
+
except Exception:
|
|
1024
|
+
change_set = []
|
|
1025
|
+
|
|
1026
|
+
report = PipelineReport(
|
|
1027
|
+
pipeline_version=PIPELINE_VERSION,
|
|
1028
|
+
profile=profile.name,
|
|
1029
|
+
budget_seconds=profile.budget_seconds,
|
|
1030
|
+
duration_ms=0,
|
|
1031
|
+
status="ok",
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
per_check_budget = max(
|
|
1035
|
+
0.1,
|
|
1036
|
+
profile.budget_seconds * _PER_CHECK_BUDGET_FRACTION / max(1, len(profile.checks)),
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
for name in profile.checks:
|
|
1040
|
+
if time.monotonic() > deadline:
|
|
1041
|
+
report.status = "budget_exceeded"
|
|
1042
|
+
report.checks.append(CheckResult(
|
|
1043
|
+
name=name, status="skip", note="pipeline budget exceeded",
|
|
1044
|
+
))
|
|
1045
|
+
continue
|
|
1046
|
+
|
|
1047
|
+
fn = CHECK_REGISTRY.get(name)
|
|
1048
|
+
if fn is None:
|
|
1049
|
+
report.checks.append(CheckResult(
|
|
1050
|
+
name=name, status="error", note=f"unknown check: {name}",
|
|
1051
|
+
))
|
|
1052
|
+
continue
|
|
1053
|
+
|
|
1054
|
+
try:
|
|
1055
|
+
check_result = fn(
|
|
1056
|
+
change_set,
|
|
1057
|
+
budget_seconds=per_check_budget,
|
|
1058
|
+
cwd=base,
|
|
1059
|
+
project_dir=project_dir,
|
|
1060
|
+
complexity=complexity,
|
|
1061
|
+
)
|
|
1062
|
+
except Exception as exc: # fail-open — one check error doesn't kill the pipeline
|
|
1063
|
+
check_result = CheckResult(
|
|
1064
|
+
name=name, status="error", note=f"{type(exc).__name__}: {str(exc)[:120]}",
|
|
1065
|
+
)
|
|
1066
|
+
report.checks.append(check_result)
|
|
1067
|
+
|
|
1068
|
+
# Aggregate findings
|
|
1069
|
+
by_sev: Dict[str, int] = {SEVERITY_BLOCK: 0, SEVERITY_WARN: 0, SEVERITY_INFO: 0}
|
|
1070
|
+
total = 0
|
|
1071
|
+
for c in report.checks:
|
|
1072
|
+
for f in c.findings:
|
|
1073
|
+
total += 1
|
|
1074
|
+
by_sev[f.severity] = by_sev.get(f.severity, 0) + 1
|
|
1075
|
+
report.findings_by_severity = by_sev
|
|
1076
|
+
report.total_findings = total
|
|
1077
|
+
report.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
1078
|
+
return report
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
# ---------------------------------------------------------------------------
|
|
1082
|
+
# Briefing integration — write findings to a session-scoped file
|
|
1083
|
+
# ---------------------------------------------------------------------------
|
|
1084
|
+
|
|
1085
|
+
def _session_guard_dir() -> Path:
|
|
1086
|
+
import tempfile
|
|
1087
|
+
session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
|
|
1088
|
+
safe = session_id.replace("/", "_").replace("\\", "_").replace("..", "_")
|
|
1089
|
+
return Path(tempfile.gettempdir()) / "wicked-guard" / safe
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def write_briefing_file(report: PipelineReport) -> Optional[Path]:
|
|
1093
|
+
"""Write the report where bootstrap.py can pick it up next session.
|
|
1094
|
+
|
|
1095
|
+
Uses ${TMPDIR}/wicked-guard/{session_id}/findings.json. Fail-open — returns
|
|
1096
|
+
None on error.
|
|
1097
|
+
"""
|
|
1098
|
+
try:
|
|
1099
|
+
out_dir = _session_guard_dir()
|
|
1100
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
1101
|
+
path = out_dir / "findings.json"
|
|
1102
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
1103
|
+
json.dump(report.to_dict(), f, indent=2)
|
|
1104
|
+
return path
|
|
1105
|
+
except Exception:
|
|
1106
|
+
return None
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def render_summary(report: PipelineReport, *, max_lines: int = 8) -> str:
|
|
1110
|
+
"""Render a compact, user-facing summary suitable for the session message."""
|
|
1111
|
+
if report.total_findings == 0:
|
|
1112
|
+
return f"[Guard] {report.profile} pipeline ran ({report.duration_ms}ms) — no findings"
|
|
1113
|
+
|
|
1114
|
+
header = (
|
|
1115
|
+
f"[Guard] {report.profile} pipeline surfaced {report.total_findings} finding(s) "
|
|
1116
|
+
f"({report.duration_ms}ms) — block={report.findings_by_severity.get(SEVERITY_BLOCK, 0)} "
|
|
1117
|
+
f"warn={report.findings_by_severity.get(SEVERITY_WARN, 0)} "
|
|
1118
|
+
f"info={report.findings_by_severity.get(SEVERITY_INFO, 0)}. "
|
|
1119
|
+
"Review next-session briefing for details (not a hard block)."
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
# Show top findings by severity (block first, then warn)
|
|
1123
|
+
priority = {SEVERITY_BLOCK: 0, SEVERITY_WARN: 1, SEVERITY_INFO: 2}
|
|
1124
|
+
all_findings: List[Finding] = []
|
|
1125
|
+
for c in report.checks:
|
|
1126
|
+
all_findings.extend(c.findings)
|
|
1127
|
+
all_findings.sort(key=lambda f: priority.get(f.severity, 99))
|
|
1128
|
+
|
|
1129
|
+
lines = [header]
|
|
1130
|
+
for finding in all_findings[:max_lines]:
|
|
1131
|
+
loc = f" @ {finding.file}:{finding.line}" if finding.file and finding.line else (
|
|
1132
|
+
f" @ {finding.file}" if finding.file else ""
|
|
1133
|
+
)
|
|
1134
|
+
lines.append(f" - [{finding.severity}] {finding.rule_id}: {finding.message}{loc}")
|
|
1135
|
+
if len(all_findings) > max_lines:
|
|
1136
|
+
lines.append(f" ... and {len(all_findings) - max_lines} more")
|
|
1137
|
+
return "\n".join(lines)
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
# ---------------------------------------------------------------------------
|
|
1141
|
+
# Bus emission
|
|
1142
|
+
# ---------------------------------------------------------------------------
|
|
1143
|
+
|
|
1144
|
+
def emit_findings_event(report: PipelineReport) -> None:
|
|
1145
|
+
"""Emit a wicked.guard.findings event. Fail-open.
|
|
1146
|
+
|
|
1147
|
+
Uses scripts/_bus.py::emit_event when available and the event is in the
|
|
1148
|
+
BUS_EVENT_MAP. When not, writes a JSONL line to a session-scoped
|
|
1149
|
+
fallback file so we still have a trail (matches the #443 pattern).
|
|
1150
|
+
"""
|
|
1151
|
+
payload = {
|
|
1152
|
+
"profile": report.profile,
|
|
1153
|
+
"duration_ms": report.duration_ms,
|
|
1154
|
+
"status": report.status,
|
|
1155
|
+
"total_findings": report.total_findings,
|
|
1156
|
+
"findings_by_severity": report.findings_by_severity,
|
|
1157
|
+
# Do NOT ship raw findings (paths/snippets) to the bus — deny-list rules.
|
|
1158
|
+
# Downstream subscribers can load the briefing file by session_id.
|
|
1159
|
+
"session_id": os.environ.get("CLAUDE_SESSION_ID", "default"),
|
|
1160
|
+
}
|
|
1161
|
+
try:
|
|
1162
|
+
sys.path.insert(0, str(_SCRIPTS_ROOT))
|
|
1163
|
+
from _bus import emit_event, BUS_EVENT_MAP # type: ignore
|
|
1164
|
+
if "wicked.guard.findings" in BUS_EVENT_MAP:
|
|
1165
|
+
emit_event("wicked.guard.findings", payload)
|
|
1166
|
+
return
|
|
1167
|
+
except Exception:
|
|
1168
|
+
pass # fail open — fall through to JSONL fallback below
|
|
1169
|
+
|
|
1170
|
+
# Fallback: append a JSONL line so we still have a local trail.
|
|
1171
|
+
try:
|
|
1172
|
+
out_dir = _session_guard_dir()
|
|
1173
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
1174
|
+
with open(out_dir / "bus-fallback.jsonl", "a", encoding="utf-8") as f:
|
|
1175
|
+
f.write(json.dumps({"event": "wicked.guard.findings", "payload": payload}) + "\n")
|
|
1176
|
+
except Exception:
|
|
1177
|
+
pass # fail open — telemetry should never block the caller
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
# ---------------------------------------------------------------------------
|
|
1181
|
+
# CLI — for debugging only
|
|
1182
|
+
# ---------------------------------------------------------------------------
|
|
1183
|
+
|
|
1184
|
+
def _main_cli(argv: List[str]) -> int:
|
|
1185
|
+
import argparse
|
|
1186
|
+
|
|
1187
|
+
ap = argparse.ArgumentParser(description="Run the guard pipeline")
|
|
1188
|
+
ap.add_argument("command", choices=["run"])
|
|
1189
|
+
ap.add_argument("--profile", default=None, help="scalpel|standard|deep")
|
|
1190
|
+
ap.add_argument("--project-dir", default=None)
|
|
1191
|
+
ap.add_argument("--complexity", type=int, default=3)
|
|
1192
|
+
ap.add_argument("--emit-bus", action="store_true")
|
|
1193
|
+
ap.add_argument("--write-briefing", action="store_true")
|
|
1194
|
+
args = ap.parse_args(argv)
|
|
1195
|
+
|
|
1196
|
+
project_dir = Path(args.project_dir) if args.project_dir else None
|
|
1197
|
+
report = run_pipeline(
|
|
1198
|
+
profile_name=args.profile,
|
|
1199
|
+
project_dir=project_dir,
|
|
1200
|
+
complexity=args.complexity,
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
sys.stdout.write(json.dumps(report.to_dict(), indent=2))
|
|
1204
|
+
sys.stdout.write("\n")
|
|
1205
|
+
|
|
1206
|
+
if args.emit_bus:
|
|
1207
|
+
emit_findings_event(report)
|
|
1208
|
+
if args.write_briefing:
|
|
1209
|
+
write_briefing_file(report)
|
|
1210
|
+
return 0
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
if __name__ == "__main__":
|
|
1214
|
+
sys.exit(_main_cli(sys.argv[1:]))
|