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,739 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
drift.py — quality-telemetry drift detection for wicked-garden.
|
|
4
|
+
|
|
5
|
+
Closes Issue #443: distinguish noise (common-cause) from signal (special-cause)
|
|
6
|
+
on the session-level quality timeline maintained by telemetry.py.
|
|
7
|
+
|
|
8
|
+
Algorithms
|
|
9
|
+
----------
|
|
10
|
+
1. Moving average (N=4 baseline window). Flags ``drop_pct`` when the current
|
|
11
|
+
session's metric drops >15% below the baseline mean — the minimal viable
|
|
12
|
+
alarm specified in the issue.
|
|
13
|
+
2. EWMA slope over the last ``ewma_window`` sessions with alpha=0.3. A
|
|
14
|
+
monotonically negative slope across the smoothed points is reported as a
|
|
15
|
+
trending degradation signal distinct from a single-point drop.
|
|
16
|
+
3. 3σ Western Electric zone-A rule (baseline mean ± 3 * baseline_stddev).
|
|
17
|
+
Points outside 3σ are tagged ``special-cause``. Points outside 2σ are
|
|
18
|
+
reported as ``warn`` (zone B). Everything else is ``common-cause`` noise.
|
|
19
|
+
4. Western Electric runs rules (issues #459, #719) beyond zone A:
|
|
20
|
+
- ``2_of_3_zone_b``: 2 of the last 3 points are beyond 2σ on the bad side
|
|
21
|
+
(below-baseline). Fires ``special-cause`` even when the latest point is
|
|
22
|
+
inside zone A.
|
|
23
|
+
- ``4_of_5_zone_c``: 4 of the last 5 points beyond 1σ on the bad side
|
|
24
|
+
(issue #719). Catches sustained drift one zone closer to the mean.
|
|
25
|
+
- ``8_consecutive_one_side``: 8 consecutive points all on the bad side of
|
|
26
|
+
the mean (issue #719). Catches subtle but persistent bias.
|
|
27
|
+
- ``trending_down``: 6 consecutive points each lower than the one before
|
|
28
|
+
(monotonic degradation). Fires ``special-cause``.
|
|
29
|
+
- ``trending_up``: symmetric monotonic climb. Reported but never flips
|
|
30
|
+
``drift`` — rising metrics on a "higher is better" axis are good news.
|
|
31
|
+
These runs rules are additive — they OR with the zone-A sigma check.
|
|
32
|
+
|
|
33
|
+
Classification output
|
|
34
|
+
---------------------
|
|
35
|
+
{
|
|
36
|
+
"metric": "gate_pass_rate",
|
|
37
|
+
"latest": 0.55,
|
|
38
|
+
"baseline": {"mean": 0.88, "stddev": 0.06, "window": 4, "samples": [...]},
|
|
39
|
+
"drop_pct": 0.375, # 37.5% below baseline mean
|
|
40
|
+
"ewma_slope": -0.018, # negative slope = worsening
|
|
41
|
+
"zone": "special-cause" | "warn" | "common-cause" | "insufficient",
|
|
42
|
+
"drift": True, # final decision: alarm should fire
|
|
43
|
+
"reasons": ["drop_pct 37.5% >= 15%", "outside 3σ"],
|
|
44
|
+
"session_count": 5,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Guardrails
|
|
48
|
+
----------
|
|
49
|
+
- Stdlib-only. Fail-open. No hard dependency on wicked-bus.
|
|
50
|
+
- Minimum-sample rule: require >= 5 sessions (issue acceptance criterion) before
|
|
51
|
+
classifying anything as drift. Below that, return ``zone: "insufficient"``.
|
|
52
|
+
- "No new gate in response to common-cause variation" — we expose a helper
|
|
53
|
+
``is_actionable`` that returns True only for special-cause drift, so the
|
|
54
|
+
caller (crew/status) can refuse to escalate on noise.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import json
|
|
60
|
+
import math
|
|
61
|
+
import os
|
|
62
|
+
import sys
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
from typing import Any, Dict, List, Optional
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Path setup — co-located with telemetry.py under scripts/delivery/
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
_SCRIPTS_ROOT = Path(__file__).resolve().parents[1]
|
|
71
|
+
if str(_SCRIPTS_ROOT) not in sys.path:
|
|
72
|
+
sys.path.insert(0, str(_SCRIPTS_ROOT))
|
|
73
|
+
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
74
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
75
|
+
|
|
76
|
+
# Defaults — kept as module constants so callers can tune via kwargs.
|
|
77
|
+
DEFAULT_BASELINE_WINDOW = 4 # sessions preceding the latest one
|
|
78
|
+
DEFAULT_EWMA_WINDOW = 6
|
|
79
|
+
DEFAULT_EWMA_ALPHA = 0.3
|
|
80
|
+
DEFAULT_DROP_PCT_THRESHOLD = 0.15 # issue acceptance: 15% below 4-session baseline
|
|
81
|
+
DEFAULT_SPECIAL_CAUSE_SIGMA = 3.0 # Western Electric zone A
|
|
82
|
+
DEFAULT_WARN_SIGMA = 2.0 # zone B
|
|
83
|
+
DEFAULT_ZONE_C_SIGMA = 1.0 # zone C (issue #719)
|
|
84
|
+
MIN_SESSIONS_FOR_DRIFT = 5 # issue #459 acceptance
|
|
85
|
+
# Issue #719 acceptance: at least 8 samples before *any* WE rule may fire.
|
|
86
|
+
WARMUP_MIN_SAMPLES = 8
|
|
87
|
+
|
|
88
|
+
# Western Electric runs-rule thresholds (issues #459, #719).
|
|
89
|
+
# 2-of-3: "2 of the most recent 3 points beyond 2σ on the same side."
|
|
90
|
+
WE_2OF3_WINDOW = 3
|
|
91
|
+
WE_2OF3_COUNT = 2
|
|
92
|
+
# 4-of-5: "4 of the most recent 5 points beyond 1σ on the same side."
|
|
93
|
+
WE_4OF5_WINDOW = 5
|
|
94
|
+
WE_4OF5_COUNT = 4
|
|
95
|
+
# Run-of-8: "8 consecutive points on the same side of the centerline."
|
|
96
|
+
WE_RUN_OF_8_WINDOW = 8
|
|
97
|
+
# Trending: 6 consecutive monotonically increasing / decreasing points.
|
|
98
|
+
WE_TRENDING_WINDOW = 6
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Low-level statistics (stdlib only, deterministic)
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def _mean(xs: List[float]) -> float:
|
|
106
|
+
return sum(xs) / len(xs) if xs else 0.0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _stddev(xs: List[float]) -> float:
|
|
110
|
+
"""Population standard deviation. Returns 0 on fewer than 2 points."""
|
|
111
|
+
if len(xs) < 2:
|
|
112
|
+
return 0.0
|
|
113
|
+
mu = _mean(xs)
|
|
114
|
+
return math.sqrt(sum((x - mu) ** 2 for x in xs) / len(xs))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _ewma(xs: List[float], alpha: float = DEFAULT_EWMA_ALPHA) -> List[float]:
|
|
118
|
+
"""Return the EWMA series. Empty input → empty output."""
|
|
119
|
+
out: List[float] = []
|
|
120
|
+
prev: Optional[float] = None
|
|
121
|
+
for x in xs:
|
|
122
|
+
if prev is None:
|
|
123
|
+
prev = x
|
|
124
|
+
else:
|
|
125
|
+
prev = alpha * x + (1 - alpha) * prev
|
|
126
|
+
out.append(prev)
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _slope(xs: List[float]) -> float:
|
|
131
|
+
"""Simple linear regression slope with x = [0, 1, 2, ...]."""
|
|
132
|
+
n = len(xs)
|
|
133
|
+
if n < 2:
|
|
134
|
+
return 0.0
|
|
135
|
+
x = list(range(n))
|
|
136
|
+
mx = _mean([float(i) for i in x])
|
|
137
|
+
my = _mean(xs)
|
|
138
|
+
num = sum((xi - mx) * (yi - my) for xi, yi in zip(x, xs))
|
|
139
|
+
den = sum((xi - mx) ** 2 for xi in x)
|
|
140
|
+
if den == 0:
|
|
141
|
+
return 0.0
|
|
142
|
+
return num / den
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Metric extraction from a timeline
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def _pull_metric(records: List[Dict[str, Any]], metric: str) -> List[float]:
|
|
150
|
+
"""Extract a numeric metric series from the timeline, skipping missing."""
|
|
151
|
+
out: List[float] = []
|
|
152
|
+
for r in records:
|
|
153
|
+
m = r.get("metrics") or {}
|
|
154
|
+
v = m.get(metric)
|
|
155
|
+
if isinstance(v, (int, float)):
|
|
156
|
+
out.append(float(v))
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Western Electric runs rules (issue #459)
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def _we_2_of_3_zone_b(
|
|
165
|
+
series: List[float],
|
|
166
|
+
mean: float,
|
|
167
|
+
stddev: float,
|
|
168
|
+
*,
|
|
169
|
+
warn_sigma: float = DEFAULT_WARN_SIGMA,
|
|
170
|
+
) -> bool:
|
|
171
|
+
"""WE rule: 2 of the last 3 points beyond 2σ below the baseline mean.
|
|
172
|
+
|
|
173
|
+
Requires at least 3 points and a non-zero baseline stddev. We only flag
|
|
174
|
+
the "bad" (below-baseline) side — metrics like gate_pass_rate are
|
|
175
|
+
higher-is-better. Callers with lower-is-better metrics should invert
|
|
176
|
+
the series before passing it in.
|
|
177
|
+
"""
|
|
178
|
+
if stddev <= 0:
|
|
179
|
+
return False
|
|
180
|
+
if len(series) < WE_2OF3_WINDOW:
|
|
181
|
+
return False
|
|
182
|
+
recent = series[-WE_2OF3_WINDOW:]
|
|
183
|
+
# Point counts as "beyond 2σ on the bad side" when it sits at least
|
|
184
|
+
# warn_sigma standard deviations below the baseline mean.
|
|
185
|
+
threshold = mean - warn_sigma * stddev
|
|
186
|
+
breaches = sum(1 for x in recent if x < threshold)
|
|
187
|
+
return breaches >= WE_2OF3_COUNT
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _we_4_of_5_zone_c(
|
|
191
|
+
series: List[float],
|
|
192
|
+
mean: float,
|
|
193
|
+
stddev: float,
|
|
194
|
+
*,
|
|
195
|
+
side: str = "below",
|
|
196
|
+
zone_c_sigma: float = DEFAULT_ZONE_C_SIGMA,
|
|
197
|
+
) -> bool:
|
|
198
|
+
"""WE rule (#719): 4 of the last 5 points beyond 1σ on one side of the baseline.
|
|
199
|
+
|
|
200
|
+
``side`` mirrors ``_we_8_consecutive_one_side``: "below" (default — the
|
|
201
|
+
bad side for higher-is-better metrics) or "above". Requires non-zero
|
|
202
|
+
baseline stddev and at least ``WE_4OF5_WINDOW`` observations.
|
|
203
|
+
"""
|
|
204
|
+
if stddev <= 0:
|
|
205
|
+
return False
|
|
206
|
+
if len(series) < WE_4OF5_WINDOW:
|
|
207
|
+
return False
|
|
208
|
+
recent = series[-WE_4OF5_WINDOW:]
|
|
209
|
+
if side == "above":
|
|
210
|
+
threshold = mean + zone_c_sigma * stddev
|
|
211
|
+
breaches = sum(1 for x in recent if x > threshold)
|
|
212
|
+
else:
|
|
213
|
+
threshold = mean - zone_c_sigma * stddev
|
|
214
|
+
breaches = sum(1 for x in recent if x < threshold)
|
|
215
|
+
return breaches >= WE_4OF5_COUNT
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _we_8_consecutive_one_side(
|
|
219
|
+
series: List[float],
|
|
220
|
+
mean: float,
|
|
221
|
+
*,
|
|
222
|
+
side: str = "below",
|
|
223
|
+
) -> bool:
|
|
224
|
+
"""WE rule (#719): 8 consecutive points on one side of the centerline.
|
|
225
|
+
|
|
226
|
+
``side="below"`` is the bad side for higher-is-better metrics.
|
|
227
|
+
Equality with the mean does not count — SPC theory requires strict
|
|
228
|
+
one-sided runs to indicate a process shift.
|
|
229
|
+
"""
|
|
230
|
+
if len(series) < WE_RUN_OF_8_WINDOW:
|
|
231
|
+
return False
|
|
232
|
+
window = series[-WE_RUN_OF_8_WINDOW:]
|
|
233
|
+
if side == "below":
|
|
234
|
+
return all(x < mean for x in window)
|
|
235
|
+
if side == "above":
|
|
236
|
+
return all(x > mean for x in window)
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _we_trending(series: List[float], direction: str = "down") -> bool:
|
|
241
|
+
"""WE rule: 6 consecutive points moving monotonically.
|
|
242
|
+
|
|
243
|
+
``direction`` is "down" (each point strictly less than the previous) or
|
|
244
|
+
"up" (strictly greater). Flat runs (equal values) do NOT count — SPC
|
|
245
|
+
theory requires strict monotonicity for a trend signal.
|
|
246
|
+
"""
|
|
247
|
+
if len(series) < WE_TRENDING_WINDOW:
|
|
248
|
+
return False
|
|
249
|
+
window = series[-WE_TRENDING_WINDOW:]
|
|
250
|
+
if direction == "down":
|
|
251
|
+
return all(window[i] < window[i - 1] for i in range(1, len(window)))
|
|
252
|
+
if direction == "up":
|
|
253
|
+
return all(window[i] > window[i - 1] for i in range(1, len(window)))
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# Public API
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def classify(
|
|
262
|
+
records: List[Dict[str, Any]],
|
|
263
|
+
metric: str = "gate_pass_rate",
|
|
264
|
+
*,
|
|
265
|
+
baseline_window: int = DEFAULT_BASELINE_WINDOW,
|
|
266
|
+
ewma_window: int = DEFAULT_EWMA_WINDOW,
|
|
267
|
+
drop_pct_threshold: float = DEFAULT_DROP_PCT_THRESHOLD,
|
|
268
|
+
special_cause_sigma: float = DEFAULT_SPECIAL_CAUSE_SIGMA,
|
|
269
|
+
warn_sigma: float = DEFAULT_WARN_SIGMA,
|
|
270
|
+
min_sessions: int = MIN_SESSIONS_FOR_DRIFT,
|
|
271
|
+
) -> Dict[str, Any]:
|
|
272
|
+
"""Classify the latest point against its baseline.
|
|
273
|
+
|
|
274
|
+
Returns a dict with fields documented in the module docstring. Always
|
|
275
|
+
returns a dict — never raises. Missing metric → ``zone: insufficient``.
|
|
276
|
+
"""
|
|
277
|
+
series = _pull_metric(records, metric)
|
|
278
|
+
session_count = len(series)
|
|
279
|
+
|
|
280
|
+
result: Dict[str, Any] = {
|
|
281
|
+
"metric": metric,
|
|
282
|
+
"session_count": session_count,
|
|
283
|
+
"latest": series[-1] if series else None,
|
|
284
|
+
"baseline": None,
|
|
285
|
+
"drop_pct": None,
|
|
286
|
+
"ewma_slope": None,
|
|
287
|
+
"zone": "insufficient",
|
|
288
|
+
"drift": False,
|
|
289
|
+
"reasons": [],
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if session_count < min_sessions:
|
|
293
|
+
result["reasons"].append(
|
|
294
|
+
f"need >= {min_sessions} sessions, have {session_count}"
|
|
295
|
+
)
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
# Baseline: the N sessions immediately preceding the latest.
|
|
299
|
+
baseline = series[-(baseline_window + 1):-1] if baseline_window < session_count else series[:-1]
|
|
300
|
+
if not baseline:
|
|
301
|
+
result["reasons"].append("no baseline samples available")
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
mu = _mean(baseline)
|
|
305
|
+
sigma = _stddev(baseline)
|
|
306
|
+
latest = series[-1]
|
|
307
|
+
result["baseline"] = {
|
|
308
|
+
"mean": round(mu, 6),
|
|
309
|
+
"stddev": round(sigma, 6),
|
|
310
|
+
"window": len(baseline),
|
|
311
|
+
"samples": [round(x, 6) for x in baseline],
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Moving-average deviation (issue acceptance: >15% drop).
|
|
315
|
+
if mu > 0:
|
|
316
|
+
drop_pct = (mu - latest) / mu # positive ⇒ latest is below baseline
|
|
317
|
+
elif mu == 0 and latest < 0:
|
|
318
|
+
drop_pct = 1.0
|
|
319
|
+
else:
|
|
320
|
+
drop_pct = 0.0
|
|
321
|
+
result["drop_pct"] = round(drop_pct, 6)
|
|
322
|
+
|
|
323
|
+
# 3σ classification.
|
|
324
|
+
if sigma > 0:
|
|
325
|
+
z = (mu - latest) / sigma
|
|
326
|
+
else:
|
|
327
|
+
# Flat baseline: any delta is instantly "special-cause" in SPC theory,
|
|
328
|
+
# but we soften: require drop_pct threshold *and* a non-zero delta.
|
|
329
|
+
z = math.inf if latest < mu else 0.0
|
|
330
|
+
|
|
331
|
+
# EWMA slope over the last ewma_window points.
|
|
332
|
+
window_series = series[-ewma_window:] if len(series) >= ewma_window else series
|
|
333
|
+
ewma_series = _ewma(window_series)
|
|
334
|
+
slope = _slope(ewma_series)
|
|
335
|
+
result["ewma_slope"] = round(slope, 6)
|
|
336
|
+
|
|
337
|
+
# Zone classification (zone-A sigma test).
|
|
338
|
+
# Surface structured fields so _classify_rule doesn't have to substring-match
|
|
339
|
+
# the human-readable reasons text (Copilot review on PR #730).
|
|
340
|
+
result["outside_special_cause_sigma"] = z >= special_cause_sigma
|
|
341
|
+
result["special_cause_sigma_threshold"] = special_cause_sigma
|
|
342
|
+
if z >= special_cause_sigma:
|
|
343
|
+
zone = "special-cause"
|
|
344
|
+
result["reasons"].append(f"outside {special_cause_sigma:.0f}σ (z={z:.2f})")
|
|
345
|
+
elif z >= warn_sigma:
|
|
346
|
+
zone = "warn"
|
|
347
|
+
result["reasons"].append(f"outside {warn_sigma:.0f}σ (z={z:.2f})")
|
|
348
|
+
else:
|
|
349
|
+
zone = "common-cause"
|
|
350
|
+
|
|
351
|
+
# Western Electric runs rules (issues #459, #719) — operate on the full
|
|
352
|
+
# observed series using the baseline mean/stddev as the reference frame.
|
|
353
|
+
# Issue #719 acceptance: at least WARMUP_MIN_SAMPLES observations before
|
|
354
|
+
# *any* WE rule may fire, regardless of which rule is configured. Below
|
|
355
|
+
# warmup we still classify zone-A from the latest z-score, but suppress
|
|
356
|
+
# WE flags entirely. ``insufficient_warmup`` lets callers tell "below
|
|
357
|
+
# warmup" apart from "warm but quiet".
|
|
358
|
+
we_rules: List[str] = []
|
|
359
|
+
insufficient_warmup = session_count < WARMUP_MIN_SAMPLES
|
|
360
|
+
result["insufficient_warmup"] = insufficient_warmup
|
|
361
|
+
result["warmup_min_samples"] = WARMUP_MIN_SAMPLES
|
|
362
|
+
|
|
363
|
+
if not insufficient_warmup:
|
|
364
|
+
if _we_2_of_3_zone_b(series, mu, sigma, warn_sigma=warn_sigma):
|
|
365
|
+
we_rules.append("2_of_3_zone_b")
|
|
366
|
+
result["reasons"].append(
|
|
367
|
+
f"WE 2-of-3: 2+ of last {WE_2OF3_WINDOW} points beyond "
|
|
368
|
+
f"{warn_sigma:.0f}σ below baseline"
|
|
369
|
+
)
|
|
370
|
+
if _we_4_of_5_zone_c(series, mu, sigma):
|
|
371
|
+
we_rules.append("4_of_5_zone_c")
|
|
372
|
+
result["reasons"].append(
|
|
373
|
+
f"WE 4-of-5: 4+ of last {WE_4OF5_WINDOW} points beyond "
|
|
374
|
+
f"{DEFAULT_ZONE_C_SIGMA:.0f}σ below baseline"
|
|
375
|
+
)
|
|
376
|
+
if _we_8_consecutive_one_side(series, mu, side="below"):
|
|
377
|
+
we_rules.append("8_consecutive_one_side")
|
|
378
|
+
result["reasons"].append(
|
|
379
|
+
f"WE run-of-8: {WE_RUN_OF_8_WINDOW} consecutive points below baseline mean"
|
|
380
|
+
)
|
|
381
|
+
if _we_trending(series, direction="down"):
|
|
382
|
+
we_rules.append("trending_down")
|
|
383
|
+
result["reasons"].append(
|
|
384
|
+
f"WE trending-down: {WE_TRENDING_WINDOW} consecutive decreasing points"
|
|
385
|
+
)
|
|
386
|
+
if _we_trending(series, direction="up"):
|
|
387
|
+
we_rules.append("trending_up")
|
|
388
|
+
result["reasons"].append(
|
|
389
|
+
f"WE trending-up: {WE_TRENDING_WINDOW} consecutive increasing points"
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
result["reasons"].append(
|
|
393
|
+
f"warmup gate: need {WARMUP_MIN_SAMPLES} samples for WE rules, "
|
|
394
|
+
f"have {session_count}"
|
|
395
|
+
)
|
|
396
|
+
result["we_rules"] = we_rules
|
|
397
|
+
|
|
398
|
+
# Any "bad-side" WE runs rule escalates zone to special-cause.
|
|
399
|
+
# trending_up is informational only (higher-is-better metrics).
|
|
400
|
+
bad_side_rules = {"2_of_3_zone_b", "4_of_5_zone_c", "8_consecutive_one_side", "trending_down"}
|
|
401
|
+
if any(r in bad_side_rules for r in we_rules):
|
|
402
|
+
zone = "special-cause"
|
|
403
|
+
|
|
404
|
+
# Drift alarm: >=15% drop below baseline OR special-cause zone.
|
|
405
|
+
# Issue #719 acceptance: gate all flags behind WARMUP_MIN_SAMPLES so we
|
|
406
|
+
# don't fire on a freshly-onboarded project's first noisy week. The
|
|
407
|
+
# existing ``min_sessions`` (default 5) is the *classification* floor; the
|
|
408
|
+
# warmup gate is the *flag* floor.
|
|
409
|
+
drift = False
|
|
410
|
+
if drop_pct >= drop_pct_threshold:
|
|
411
|
+
result["reasons"].append(
|
|
412
|
+
f"drop_pct {drop_pct * 100:.1f}% >= {drop_pct_threshold * 100:.0f}%"
|
|
413
|
+
)
|
|
414
|
+
if not insufficient_warmup:
|
|
415
|
+
drift = True
|
|
416
|
+
if zone == "special-cause" and not insufficient_warmup:
|
|
417
|
+
drift = True
|
|
418
|
+
|
|
419
|
+
# Trending degradation: EWMA slope meaningfully negative.
|
|
420
|
+
if slope < -0.01 and len(ewma_series) >= 3:
|
|
421
|
+
result["reasons"].append(f"ewma_slope {slope:.4f} trending down")
|
|
422
|
+
# Trending slope alone does not flip drift=True unless combined with
|
|
423
|
+
# a drop or special-cause — slope is advisory context.
|
|
424
|
+
|
|
425
|
+
result["zone"] = zone
|
|
426
|
+
result["drift"] = drift
|
|
427
|
+
return result
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def is_actionable(classification: Dict[str, Any]) -> bool:
|
|
431
|
+
"""Should the caller escalate (e.g. add a gate, alert a user)?
|
|
432
|
+
|
|
433
|
+
Issue acceptance: "No new process gate added in response to common-cause
|
|
434
|
+
variation." Actionable = special-cause OR drop_pct breach — both
|
|
435
|
+
unambiguous signal, not noise.
|
|
436
|
+
"""
|
|
437
|
+
if not classification:
|
|
438
|
+
return False
|
|
439
|
+
if classification.get("zone") == "special-cause":
|
|
440
|
+
return True
|
|
441
|
+
drop = classification.get("drop_pct") or 0
|
|
442
|
+
if drop >= DEFAULT_DROP_PCT_THRESHOLD:
|
|
443
|
+
return True
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def summarize(classification: Dict[str, Any]) -> str:
|
|
448
|
+
"""Render a one-line human summary for status output."""
|
|
449
|
+
if not classification:
|
|
450
|
+
return "no telemetry data"
|
|
451
|
+
zone = classification.get("zone", "unknown")
|
|
452
|
+
metric = classification.get("metric", "metric")
|
|
453
|
+
latest = classification.get("latest")
|
|
454
|
+
base = classification.get("baseline") or {}
|
|
455
|
+
mu = base.get("mean")
|
|
456
|
+
drop = classification.get("drop_pct")
|
|
457
|
+
latest_s = "n/a" if latest is None else f"{latest:.3f}"
|
|
458
|
+
mu_s = "n/a" if mu is None else f"{mu:.3f}"
|
|
459
|
+
drop_s = "n/a" if drop is None else f"{drop * 100:+.1f}%"
|
|
460
|
+
if zone == "insufficient":
|
|
461
|
+
return f"{metric}: insufficient data ({classification.get('session_count', 0)} sessions)"
|
|
462
|
+
label = {
|
|
463
|
+
"special-cause": "SIGNAL",
|
|
464
|
+
"warn": "watch",
|
|
465
|
+
"common-cause": "noise",
|
|
466
|
+
}.get(zone, zone)
|
|
467
|
+
return f"{metric}: latest={latest_s} baseline={mu_s} delta={drop_s} [{label}]"
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
# Bus emission (fail-open)
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
def emit_drift_event(
|
|
475
|
+
project: str,
|
|
476
|
+
classification: Dict[str, Any],
|
|
477
|
+
) -> bool:
|
|
478
|
+
"""Emit a wicked-bus drift event if the classification is actionable.
|
|
479
|
+
|
|
480
|
+
Returns True if emission was attempted (bus may have been unavailable),
|
|
481
|
+
False if the classification didn't warrant emission.
|
|
482
|
+
Never raises.
|
|
483
|
+
"""
|
|
484
|
+
try:
|
|
485
|
+
if not is_actionable(classification):
|
|
486
|
+
return False
|
|
487
|
+
# Import lazily so drift.py remains importable without the bus shim.
|
|
488
|
+
from _bus import emit_event # type: ignore
|
|
489
|
+
payload = {
|
|
490
|
+
"project": project,
|
|
491
|
+
"metric": classification.get("metric"),
|
|
492
|
+
"zone": classification.get("zone"),
|
|
493
|
+
"latest": classification.get("latest"),
|
|
494
|
+
"baseline_mean": (classification.get("baseline") or {}).get("mean"),
|
|
495
|
+
"baseline_stddev": (classification.get("baseline") or {}).get("stddev"),
|
|
496
|
+
"drop_pct": classification.get("drop_pct"),
|
|
497
|
+
"ewma_slope": classification.get("ewma_slope"),
|
|
498
|
+
"reasons": classification.get("reasons"),
|
|
499
|
+
"session_count": classification.get("session_count"),
|
|
500
|
+
}
|
|
501
|
+
emit_event("wicked.quality.drift_detected", payload)
|
|
502
|
+
return True
|
|
503
|
+
except Exception as exc:
|
|
504
|
+
# Fail-open — bus absence must never break telemetry.
|
|
505
|
+
print(f"[wicked-garden] drift emit error: {exc}", file=sys.stderr)
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# ---------------------------------------------------------------------------
|
|
510
|
+
# SPC flag emission via DomainStore (issue #719)
|
|
511
|
+
# ---------------------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
# Critical = zone-A (3σ) breach or 8-in-a-row run; warn = lesser WE rules
|
|
514
|
+
# or simple drop_pct trigger. Severity drives downstream alert routing.
|
|
515
|
+
_CRITICAL_RULES = {"1_outside_3sigma", "8_consecutive_one_side"}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _classify_rule(classification: Dict[str, Any]) -> List[str]:
|
|
519
|
+
"""Return all rule identifiers that fired for this classification.
|
|
520
|
+
|
|
521
|
+
A single classification can fire multiple rules — emit one flag per rule
|
|
522
|
+
so downstream consumers can deduplicate by (metric, rule) instead of
|
|
523
|
+
forcing the priority decision here.
|
|
524
|
+
"""
|
|
525
|
+
rules = list(classification.get("we_rules") or [])
|
|
526
|
+
# Synthesize a rule id for the zone-A z-score breach using the structured
|
|
527
|
+
# field set by classify() — substring-matching the reasons text was brittle
|
|
528
|
+
# against tuned ``special_cause_sigma`` (e.g. "z=4.2" matches "3" by accident).
|
|
529
|
+
if classification.get("outside_special_cause_sigma"):
|
|
530
|
+
if "1_outside_3sigma" not in rules:
|
|
531
|
+
rules.append("1_outside_3sigma")
|
|
532
|
+
drop = classification.get("drop_pct") or 0.0
|
|
533
|
+
if drop >= DEFAULT_DROP_PCT_THRESHOLD and not rules:
|
|
534
|
+
rules.append("drop_pct_threshold")
|
|
535
|
+
# Strip informational-only rules from emission set.
|
|
536
|
+
return [r for r in rules if r != "trending_up"]
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def emit_spc_flag(
|
|
540
|
+
project: str,
|
|
541
|
+
classification: Dict[str, Any],
|
|
542
|
+
*,
|
|
543
|
+
metric: Optional[str] = None,
|
|
544
|
+
) -> List[Dict[str, Any]]:
|
|
545
|
+
"""Persist one ``delivery:spc:flag`` record per fired rule.
|
|
546
|
+
|
|
547
|
+
Returns the list of payloads written (empty when nothing fired or warmup
|
|
548
|
+
blocks emission). Never raises — DomainStore failures are logged to stderr.
|
|
549
|
+
"""
|
|
550
|
+
if classification.get("insufficient_warmup"):
|
|
551
|
+
return []
|
|
552
|
+
if not classification.get("drift"):
|
|
553
|
+
return []
|
|
554
|
+
rules = _classify_rule(classification)
|
|
555
|
+
if not rules:
|
|
556
|
+
return []
|
|
557
|
+
|
|
558
|
+
metric_name = metric or classification.get("metric") or "unknown"
|
|
559
|
+
base = classification.get("baseline") or {}
|
|
560
|
+
samples = base.get("samples") or []
|
|
561
|
+
sample_window = {
|
|
562
|
+
"start": samples[0] if samples else None,
|
|
563
|
+
"end": classification.get("latest"),
|
|
564
|
+
"n": classification.get("session_count"),
|
|
565
|
+
}
|
|
566
|
+
written: List[Dict[str, Any]] = []
|
|
567
|
+
try:
|
|
568
|
+
from _domain_store import DomainStore # type: ignore
|
|
569
|
+
ds = DomainStore("delivery", hook_mode=True)
|
|
570
|
+
except Exception as exc: # pragma: no cover — DomainStore is plugin core
|
|
571
|
+
print(f"[wicked-garden] spc flag store error: {exc}", file=sys.stderr)
|
|
572
|
+
return []
|
|
573
|
+
|
|
574
|
+
for rule in rules:
|
|
575
|
+
payload = {
|
|
576
|
+
"project": project,
|
|
577
|
+
"metric": metric_name,
|
|
578
|
+
"rule": rule,
|
|
579
|
+
"severity": "critical" if rule in _CRITICAL_RULES else "warn",
|
|
580
|
+
"sample_window": sample_window,
|
|
581
|
+
"current_value": classification.get("latest"),
|
|
582
|
+
"baseline_mean": base.get("mean"),
|
|
583
|
+
"baseline_stddev": base.get("stddev"),
|
|
584
|
+
"drop_pct": classification.get("drop_pct"),
|
|
585
|
+
"reasons": classification.get("reasons"),
|
|
586
|
+
"recorded_at": _utc_iso_now(),
|
|
587
|
+
}
|
|
588
|
+
try:
|
|
589
|
+
rec = ds.create("spc", payload)
|
|
590
|
+
if rec is not None:
|
|
591
|
+
written.append(rec)
|
|
592
|
+
except Exception as exc: # pragma: no cover
|
|
593
|
+
print(f"[wicked-garden] spc flag write error: {exc}", file=sys.stderr)
|
|
594
|
+
return written
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _utc_iso_now() -> str:
|
|
598
|
+
"""Local helper — drift.py is otherwise time-free, so import datetime here."""
|
|
599
|
+
from datetime import datetime, timezone
|
|
600
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def list_recent_flags(
|
|
604
|
+
project: str,
|
|
605
|
+
*,
|
|
606
|
+
limit: int = 20,
|
|
607
|
+
) -> List[Dict[str, Any]]:
|
|
608
|
+
"""Return recent SPC flags for a project, newest first. Fail-open."""
|
|
609
|
+
try:
|
|
610
|
+
from _domain_store import DomainStore # type: ignore
|
|
611
|
+
ds = DomainStore("delivery", hook_mode=True)
|
|
612
|
+
flags = ds.list("spc", project=project) or []
|
|
613
|
+
except Exception:
|
|
614
|
+
return []
|
|
615
|
+
flags.sort(key=lambda r: r.get("recorded_at") or "", reverse=True)
|
|
616
|
+
return flags[:limit] if limit and limit > 0 else flags
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# ---------------------------------------------------------------------------
|
|
620
|
+
# CLI
|
|
621
|
+
# ---------------------------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
def _cli_classify(args: List[str]) -> int:
|
|
624
|
+
if not args:
|
|
625
|
+
print(json.dumps({"ok": False, "reason": "project required"}))
|
|
626
|
+
return 0
|
|
627
|
+
project = args[0]
|
|
628
|
+
metric = args[1] if len(args) > 1 else "gate_pass_rate"
|
|
629
|
+
try:
|
|
630
|
+
from delivery.telemetry import read_timeline # type: ignore
|
|
631
|
+
except Exception:
|
|
632
|
+
# When invoked via direct path, the delivery package import may not
|
|
633
|
+
# be on sys.path. Fall back to relative import.
|
|
634
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
635
|
+
from telemetry import read_timeline # type: ignore
|
|
636
|
+
records = read_timeline(project)
|
|
637
|
+
cls = classify(records, metric)
|
|
638
|
+
print(json.dumps({
|
|
639
|
+
"ok": True,
|
|
640
|
+
"project": project,
|
|
641
|
+
"metric": metric,
|
|
642
|
+
"classification": cls,
|
|
643
|
+
"summary": summarize(cls),
|
|
644
|
+
"actionable": is_actionable(cls),
|
|
645
|
+
}, indent=2))
|
|
646
|
+
return 0
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _cli_emit(args: List[str]) -> int:
|
|
650
|
+
"""Classify + conditionally emit bus event. Used by hooks."""
|
|
651
|
+
if not args:
|
|
652
|
+
print(json.dumps({"ok": False, "reason": "project required"}))
|
|
653
|
+
return 0
|
|
654
|
+
project = args[0]
|
|
655
|
+
metric = args[1] if len(args) > 1 else "gate_pass_rate"
|
|
656
|
+
try:
|
|
657
|
+
from delivery.telemetry import read_timeline # type: ignore
|
|
658
|
+
except Exception:
|
|
659
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
660
|
+
from telemetry import read_timeline # type: ignore
|
|
661
|
+
cls = classify(read_timeline(project), metric)
|
|
662
|
+
emitted = emit_drift_event(project, cls)
|
|
663
|
+
print(json.dumps({
|
|
664
|
+
"ok": True,
|
|
665
|
+
"emitted": emitted,
|
|
666
|
+
"actionable": is_actionable(cls),
|
|
667
|
+
"classification": cls,
|
|
668
|
+
}))
|
|
669
|
+
return 0
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _cli_flag(args: List[str]) -> int:
|
|
673
|
+
"""Classify and persist any SPC flags via DomainStore. Used by /delivery:health."""
|
|
674
|
+
if not args:
|
|
675
|
+
print(json.dumps({"ok": False, "reason": "project required"}))
|
|
676
|
+
return 0
|
|
677
|
+
project = args[0]
|
|
678
|
+
metric = args[1] if len(args) > 1 else "gate_pass_rate"
|
|
679
|
+
try:
|
|
680
|
+
from delivery.telemetry import read_timeline # type: ignore
|
|
681
|
+
except Exception:
|
|
682
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
683
|
+
from telemetry import read_timeline # type: ignore
|
|
684
|
+
cls = classify(read_timeline(project), metric)
|
|
685
|
+
flags = emit_spc_flag(project, cls, metric=metric)
|
|
686
|
+
print(json.dumps({
|
|
687
|
+
"ok": True,
|
|
688
|
+
"project": project,
|
|
689
|
+
"metric": metric,
|
|
690
|
+
"classification": cls,
|
|
691
|
+
"flags_written": flags,
|
|
692
|
+
}))
|
|
693
|
+
return 0
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _cli_list_flags(args: List[str]) -> int:
|
|
697
|
+
if not args:
|
|
698
|
+
print(json.dumps({"ok": False, "reason": "project required"}))
|
|
699
|
+
return 0
|
|
700
|
+
project = args[0]
|
|
701
|
+
limit = 20
|
|
702
|
+
if len(args) > 1:
|
|
703
|
+
try:
|
|
704
|
+
limit = int(args[1])
|
|
705
|
+
except ValueError:
|
|
706
|
+
limit = 20
|
|
707
|
+
flags = list_recent_flags(project, limit=limit)
|
|
708
|
+
print(json.dumps({"ok": True, "project": project, "flags": flags}))
|
|
709
|
+
return 0
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
713
|
+
argv = list(argv if argv is not None else sys.argv[1:])
|
|
714
|
+
if not argv or argv[0] in ("-h", "--help", "help"):
|
|
715
|
+
print(
|
|
716
|
+
"Usage:\n"
|
|
717
|
+
" drift.py classify <project> [metric]\n"
|
|
718
|
+
" drift.py emit <project> [metric]\n"
|
|
719
|
+
" drift.py flag <project> [metric]\n"
|
|
720
|
+
" drift.py list-flags <project> [limit]\n",
|
|
721
|
+
file=sys.stderr,
|
|
722
|
+
)
|
|
723
|
+
return 0
|
|
724
|
+
cmd = argv[0]
|
|
725
|
+
rest = argv[1:]
|
|
726
|
+
if cmd == "classify":
|
|
727
|
+
return _cli_classify(rest)
|
|
728
|
+
if cmd == "emit":
|
|
729
|
+
return _cli_emit(rest)
|
|
730
|
+
if cmd == "flag":
|
|
731
|
+
return _cli_flag(rest)
|
|
732
|
+
if cmd == "list-flags":
|
|
733
|
+
return _cli_list_flags(rest)
|
|
734
|
+
print(json.dumps({"ok": False, "reason": f"unknown command: {cmd}"}))
|
|
735
|
+
return 0
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
if __name__ == "__main__":
|
|
739
|
+
sys.exit(main())
|