autonomous-coding-toolkit 1.0.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/marketplace.json +22 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +21 -0
- package/Makefile +21 -0
- package/README.md +140 -0
- package/SECURITY.md +28 -0
- package/agents/bash-expert.md +113 -0
- package/agents/dependency-auditor.md +138 -0
- package/agents/integration-tester.md +120 -0
- package/agents/lesson-scanner.md +149 -0
- package/agents/python-expert.md +179 -0
- package/agents/service-monitor.md +141 -0
- package/agents/shell-expert.md +147 -0
- package/benchmarks/runner.sh +147 -0
- package/benchmarks/tasks/01-rest-endpoint/rubric.sh +29 -0
- package/benchmarks/tasks/01-rest-endpoint/task.md +17 -0
- package/benchmarks/tasks/02-refactor-module/task.md +8 -0
- package/benchmarks/tasks/03-fix-integration-bug/task.md +8 -0
- package/benchmarks/tasks/04-add-test-coverage/task.md +8 -0
- package/benchmarks/tasks/05-multi-file-feature/task.md +8 -0
- package/bin/act.js +238 -0
- package/commands/autocode.md +6 -0
- package/commands/cancel-ralph.md +18 -0
- package/commands/code-factory.md +53 -0
- package/commands/create-prd.md +55 -0
- package/commands/ralph-loop.md +18 -0
- package/commands/run-plan.md +117 -0
- package/commands/submit-lesson.md +122 -0
- package/docs/ARCHITECTURE.md +630 -0
- package/docs/CONTRIBUTING.md +125 -0
- package/docs/lessons/0001-bare-exception-swallowing.md +34 -0
- package/docs/lessons/0002-async-def-without-await.md +28 -0
- package/docs/lessons/0003-create-task-without-callback.md +28 -0
- package/docs/lessons/0004-hardcoded-test-counts.md +28 -0
- package/docs/lessons/0005-sqlite-without-closing.md +33 -0
- package/docs/lessons/0006-venv-pip-path.md +27 -0
- package/docs/lessons/0007-runner-state-self-rejection.md +35 -0
- package/docs/lessons/0008-quality-gate-blind-spot.md +33 -0
- package/docs/lessons/0009-parser-overcount-empty-batches.md +36 -0
- package/docs/lessons/0010-local-outside-function-bash.md +33 -0
- package/docs/lessons/0011-batch-tests-for-unimplemented-code.md +36 -0
- package/docs/lessons/0012-api-markdown-unescaped-chars.md +33 -0
- package/docs/lessons/0013-export-prefix-env-parsing.md +33 -0
- package/docs/lessons/0014-decorator-registry-import-side-effect.md +43 -0
- package/docs/lessons/0015-frontend-backend-schema-drift.md +43 -0
- package/docs/lessons/0016-event-driven-cold-start-seeding.md +44 -0
- package/docs/lessons/0017-copy-paste-logic-diverges.md +43 -0
- package/docs/lessons/0018-layer-passes-pipeline-broken.md +45 -0
- package/docs/lessons/0019-systemd-envfile-ignores-export.md +41 -0
- package/docs/lessons/0020-persist-state-incrementally.md +44 -0
- package/docs/lessons/0021-dual-axis-testing.md +48 -0
- package/docs/lessons/0022-jsx-factory-shadowing.md +43 -0
- package/docs/lessons/0023-static-analysis-spiral.md +51 -0
- package/docs/lessons/0024-shared-pipeline-implementation.md +55 -0
- package/docs/lessons/0025-defense-in-depth-all-entry-points.md +65 -0
- package/docs/lessons/0026-linter-no-rules-false-enforcement.md +54 -0
- package/docs/lessons/0027-jsx-silent-prop-drop.md +64 -0
- package/docs/lessons/0028-no-infrastructure-in-client-code.md +49 -0
- package/docs/lessons/0029-never-write-secrets-to-files.md +61 -0
- package/docs/lessons/0030-cache-merge-not-replace.md +62 -0
- package/docs/lessons/0031-verify-units-at-boundaries.md +66 -0
- package/docs/lessons/0032-module-lifecycle-subscribe-unsubscribe.md +89 -0
- package/docs/lessons/0033-async-iteration-mutable-snapshot.md +72 -0
- package/docs/lessons/0034-caller-missing-await-silent-discard.md +65 -0
- package/docs/lessons/0035-duplicate-registration-silent-overwrite.md +85 -0
- package/docs/lessons/0036-websocket-dirty-disconnect.md +33 -0
- package/docs/lessons/0037-parallel-agents-worktree-corruption.md +31 -0
- package/docs/lessons/0038-subscribe-no-stored-ref.md +36 -0
- package/docs/lessons/0039-fallback-or-default-hides-bugs.md +34 -0
- package/docs/lessons/0040-event-firehose-filter-first.md +36 -0
- package/docs/lessons/0041-ambiguous-base-dir-path-nesting.md +32 -0
- package/docs/lessons/0042-spec-compliance-insufficient.md +36 -0
- package/docs/lessons/0043-exact-count-extensible-collections.md +32 -0
- package/docs/lessons/0044-relative-file-deps-worktree.md +39 -0
- package/docs/lessons/0045-iterative-design-improvement.md +33 -0
- package/docs/lessons/0046-plan-assertion-math-bugs.md +38 -0
- package/docs/lessons/0047-pytest-single-threaded-default.md +37 -0
- package/docs/lessons/0048-integration-wiring-batch.md +40 -0
- package/docs/lessons/0049-ab-verification.md +41 -0
- package/docs/lessons/0050-editing-sourced-files-during-execution.md +33 -0
- package/docs/lessons/0051-infrastructure-fixes-cant-self-heal.md +30 -0
- package/docs/lessons/0052-uncommitted-changes-poison-quality-gates.md +31 -0
- package/docs/lessons/0053-jq-compact-flag-inconsistency.md +31 -0
- package/docs/lessons/0054-parser-matches-inside-code-blocks.md +30 -0
- package/docs/lessons/0055-agents-compensate-for-garbled-prompts.md +31 -0
- package/docs/lessons/0056-grep-count-exit-code-on-zero.md +42 -0
- package/docs/lessons/0057-new-artifacts-break-git-clean-gates.md +42 -0
- package/docs/lessons/0058-dead-config-keys-never-consumed.md +49 -0
- package/docs/lessons/0059-contract-test-shared-structures.md +53 -0
- package/docs/lessons/0060-set-e-silent-death-in-runners.md +53 -0
- package/docs/lessons/0061-context-injection-dirty-state.md +50 -0
- package/docs/lessons/0062-sibling-bug-neighborhood-scan.md +29 -0
- package/docs/lessons/0063-one-flag-two-lifetimes.md +31 -0
- package/docs/lessons/0064-test-passes-wrong-reason.md +31 -0
- package/docs/lessons/0065-pipefail-grep-count-double-output.md +39 -0
- package/docs/lessons/0066-local-keyword-outside-function.md +37 -0
- package/docs/lessons/0067-stdin-hang-non-interactive-shell.md +36 -0
- package/docs/lessons/0068-agent-builds-wrong-thing-correctly.md +31 -0
- package/docs/lessons/0069-plan-quality-dominates-execution.md +30 -0
- package/docs/lessons/0070-spec-echo-back-prevents-drift.md +31 -0
- package/docs/lessons/0071-positive-instructions-outperform-negative.md +30 -0
- package/docs/lessons/0072-lost-in-the-middle-context-placement.md +30 -0
- package/docs/lessons/0073-unscoped-lessons-cause-false-positives.md +30 -0
- package/docs/lessons/0074-stale-context-injection-wrong-batch.md +32 -0
- package/docs/lessons/0075-research-artifacts-must-persist.md +32 -0
- package/docs/lessons/0076-wrong-decomposition-contaminates-downstream.md +30 -0
- package/docs/lessons/0077-cherry-pick-merges-need-manual-resolution.md +30 -0
- package/docs/lessons/0078-static-review-without-live-test.md +30 -0
- package/docs/lessons/0079-integration-wiring-batch-required.md +32 -0
- package/docs/lessons/FRAMEWORK.md +161 -0
- package/docs/lessons/SUMMARY.md +201 -0
- package/docs/lessons/TEMPLATE.md +85 -0
- package/docs/plans/2026-02-21-code-factory-v2-design.md +204 -0
- package/docs/plans/2026-02-21-code-factory-v2-implementation-plan.md +2189 -0
- package/docs/plans/2026-02-21-code-factory-v2-phase4-design.md +537 -0
- package/docs/plans/2026-02-21-code-factory-v2-phase4-implementation-plan.md +2012 -0
- package/docs/plans/2026-02-21-hardening-pass-design.md +108 -0
- package/docs/plans/2026-02-21-hardening-pass-plan.md +1378 -0
- package/docs/plans/2026-02-21-mab-research-report.md +406 -0
- package/docs/plans/2026-02-21-marketplace-restructure-design.md +240 -0
- package/docs/plans/2026-02-21-marketplace-restructure-plan.md +832 -0
- package/docs/plans/2026-02-21-phase4-completion-plan.md +697 -0
- package/docs/plans/2026-02-21-validator-suite-design.md +148 -0
- package/docs/plans/2026-02-21-validator-suite-plan.md +540 -0
- package/docs/plans/2026-02-22-mab-research-round2.md +556 -0
- package/docs/plans/2026-02-22-mab-run-design.md +462 -0
- package/docs/plans/2026-02-22-mab-run-plan.md +2046 -0
- package/docs/plans/2026-02-22-operations-design-methodology-research.md +681 -0
- package/docs/plans/2026-02-22-research-agent-failure-taxonomy.md +532 -0
- package/docs/plans/2026-02-22-research-code-guideline-policies.md +886 -0
- package/docs/plans/2026-02-22-research-codebase-audit-refactoring.md +908 -0
- package/docs/plans/2026-02-22-research-coding-standards-documentation.md +541 -0
- package/docs/plans/2026-02-22-research-competitive-landscape.md +687 -0
- package/docs/plans/2026-02-22-research-comprehensive-testing.md +1076 -0
- package/docs/plans/2026-02-22-research-context-utilization.md +459 -0
- package/docs/plans/2026-02-22-research-cost-quality-tradeoff.md +548 -0
- package/docs/plans/2026-02-22-research-lesson-transferability.md +508 -0
- package/docs/plans/2026-02-22-research-multi-agent-coordination.md +312 -0
- package/docs/plans/2026-02-22-research-phase-integration.md +602 -0
- package/docs/plans/2026-02-22-research-plan-quality.md +428 -0
- package/docs/plans/2026-02-22-research-prompt-engineering.md +558 -0
- package/docs/plans/2026-02-22-research-unconventional-perspectives.md +528 -0
- package/docs/plans/2026-02-22-research-user-adoption.md +638 -0
- package/docs/plans/2026-02-22-research-verification-effectiveness.md +433 -0
- package/docs/plans/2026-02-23-agent-suite-design.md +299 -0
- package/docs/plans/2026-02-23-agent-suite-plan.md +578 -0
- package/docs/plans/2026-02-23-phase3-cost-infrastructure-design.md +148 -0
- package/docs/plans/2026-02-23-phase3-cost-infrastructure-plan.md +1062 -0
- package/docs/plans/2026-02-23-research-bash-expert-agent.md +543 -0
- package/docs/plans/2026-02-23-research-dependency-auditor-agent.md +564 -0
- package/docs/plans/2026-02-23-research-improving-existing-agents.md +503 -0
- package/docs/plans/2026-02-23-research-integration-tester-agent.md +454 -0
- package/docs/plans/2026-02-23-research-python-expert-agent.md +429 -0
- package/docs/plans/2026-02-23-research-service-monitor-agent.md +425 -0
- package/docs/plans/2026-02-23-research-shell-expert-agent.md +533 -0
- package/docs/plans/2026-02-23-roadmap-to-completion.md +530 -0
- package/docs/plans/2026-02-24-headless-module-split-design.md +98 -0
- package/docs/plans/2026-02-24-headless-module-split.md +443 -0
- package/docs/plans/2026-02-24-lesson-scope-metadata-design.md +228 -0
- package/docs/plans/2026-02-24-lesson-scope-metadata-plan.md +968 -0
- package/docs/plans/2026-02-24-npm-packaging-design.md +841 -0
- package/docs/plans/2026-02-24-npm-packaging-plan.md +1965 -0
- package/docs/plans/audit-findings.md +186 -0
- package/docs/telegram-notification-format.md +98 -0
- package/examples/example-plan.md +51 -0
- package/examples/example-prd.json +72 -0
- package/examples/example-roadmap.md +33 -0
- package/examples/quickstart-plan.md +63 -0
- package/hooks/hooks.json +26 -0
- package/hooks/setup-symlinks.sh +48 -0
- package/hooks/stop-hook.sh +135 -0
- package/package.json +47 -0
- package/policies/bash.md +71 -0
- package/policies/python.md +71 -0
- package/policies/testing.md +61 -0
- package/policies/universal.md +60 -0
- package/scripts/analyze-report.sh +97 -0
- package/scripts/architecture-map.sh +145 -0
- package/scripts/auto-compound.sh +273 -0
- package/scripts/batch-audit.sh +42 -0
- package/scripts/batch-test.sh +101 -0
- package/scripts/entropy-audit.sh +221 -0
- package/scripts/failure-digest.sh +51 -0
- package/scripts/generate-ast-rules.sh +96 -0
- package/scripts/init.sh +112 -0
- package/scripts/lesson-check.sh +428 -0
- package/scripts/lib/common.sh +61 -0
- package/scripts/lib/cost-tracking.sh +153 -0
- package/scripts/lib/ollama.sh +60 -0
- package/scripts/lib/progress-writer.sh +128 -0
- package/scripts/lib/run-plan-context.sh +215 -0
- package/scripts/lib/run-plan-echo-back.sh +231 -0
- package/scripts/lib/run-plan-headless.sh +396 -0
- package/scripts/lib/run-plan-notify.sh +57 -0
- package/scripts/lib/run-plan-parser.sh +81 -0
- package/scripts/lib/run-plan-prompt.sh +215 -0
- package/scripts/lib/run-plan-quality-gate.sh +132 -0
- package/scripts/lib/run-plan-routing.sh +315 -0
- package/scripts/lib/run-plan-sampling.sh +170 -0
- package/scripts/lib/run-plan-scoring.sh +146 -0
- package/scripts/lib/run-plan-state.sh +142 -0
- package/scripts/lib/run-plan-team.sh +199 -0
- package/scripts/lib/telegram.sh +54 -0
- package/scripts/lib/thompson-sampling.sh +176 -0
- package/scripts/license-check.sh +74 -0
- package/scripts/mab-run.sh +575 -0
- package/scripts/module-size-check.sh +146 -0
- package/scripts/patterns/async-no-await.yml +5 -0
- package/scripts/patterns/bare-except.yml +6 -0
- package/scripts/patterns/empty-catch.yml +6 -0
- package/scripts/patterns/hardcoded-localhost.yml +9 -0
- package/scripts/patterns/retry-loop-no-backoff.yml +12 -0
- package/scripts/pipeline-status.sh +197 -0
- package/scripts/policy-check.sh +226 -0
- package/scripts/prior-art-search.sh +133 -0
- package/scripts/promote-mab-lessons.sh +126 -0
- package/scripts/prompts/agent-a-superpowers.md +29 -0
- package/scripts/prompts/agent-b-ralph.md +29 -0
- package/scripts/prompts/judge-agent.md +61 -0
- package/scripts/prompts/planner-agent.md +44 -0
- package/scripts/pull-community-lessons.sh +90 -0
- package/scripts/quality-gate.sh +266 -0
- package/scripts/research-gate.sh +90 -0
- package/scripts/run-plan.sh +329 -0
- package/scripts/scope-infer.sh +159 -0
- package/scripts/setup-ralph-loop.sh +155 -0
- package/scripts/telemetry.sh +230 -0
- package/scripts/tests/run-all-tests.sh +52 -0
- package/scripts/tests/test-act-cli.sh +46 -0
- package/scripts/tests/test-agents-md.sh +87 -0
- package/scripts/tests/test-analyze-report.sh +114 -0
- package/scripts/tests/test-architecture-map.sh +89 -0
- package/scripts/tests/test-auto-compound.sh +169 -0
- package/scripts/tests/test-batch-test.sh +65 -0
- package/scripts/tests/test-benchmark-runner.sh +25 -0
- package/scripts/tests/test-common.sh +168 -0
- package/scripts/tests/test-cost-tracking.sh +158 -0
- package/scripts/tests/test-echo-back.sh +180 -0
- package/scripts/tests/test-entropy-audit.sh +146 -0
- package/scripts/tests/test-failure-digest.sh +66 -0
- package/scripts/tests/test-generate-ast-rules.sh +145 -0
- package/scripts/tests/test-helpers.sh +82 -0
- package/scripts/tests/test-init.sh +47 -0
- package/scripts/tests/test-lesson-check.sh +278 -0
- package/scripts/tests/test-lesson-local.sh +55 -0
- package/scripts/tests/test-license-check.sh +109 -0
- package/scripts/tests/test-mab-run.sh +182 -0
- package/scripts/tests/test-ollama-lib.sh +49 -0
- package/scripts/tests/test-ollama.sh +60 -0
- package/scripts/tests/test-pipeline-status.sh +198 -0
- package/scripts/tests/test-policy-check.sh +124 -0
- package/scripts/tests/test-prior-art-search.sh +96 -0
- package/scripts/tests/test-progress-writer.sh +140 -0
- package/scripts/tests/test-promote-mab-lessons.sh +110 -0
- package/scripts/tests/test-pull-community-lessons.sh +149 -0
- package/scripts/tests/test-quality-gate.sh +241 -0
- package/scripts/tests/test-research-gate.sh +132 -0
- package/scripts/tests/test-run-plan-cli.sh +86 -0
- package/scripts/tests/test-run-plan-context.sh +305 -0
- package/scripts/tests/test-run-plan-e2e.sh +153 -0
- package/scripts/tests/test-run-plan-headless.sh +424 -0
- package/scripts/tests/test-run-plan-notify.sh +124 -0
- package/scripts/tests/test-run-plan-parser.sh +217 -0
- package/scripts/tests/test-run-plan-prompt.sh +254 -0
- package/scripts/tests/test-run-plan-quality-gate.sh +222 -0
- package/scripts/tests/test-run-plan-routing.sh +178 -0
- package/scripts/tests/test-run-plan-scoring.sh +148 -0
- package/scripts/tests/test-run-plan-state.sh +261 -0
- package/scripts/tests/test-run-plan-team.sh +157 -0
- package/scripts/tests/test-scope-infer.sh +150 -0
- package/scripts/tests/test-setup-ralph-loop.sh +63 -0
- package/scripts/tests/test-telegram-env.sh +38 -0
- package/scripts/tests/test-telegram.sh +121 -0
- package/scripts/tests/test-telemetry.sh +46 -0
- package/scripts/tests/test-thompson-sampling.sh +139 -0
- package/scripts/tests/test-validate-all.sh +60 -0
- package/scripts/tests/test-validate-commands.sh +89 -0
- package/scripts/tests/test-validate-hooks.sh +98 -0
- package/scripts/tests/test-validate-lessons.sh +150 -0
- package/scripts/tests/test-validate-plan-quality.sh +235 -0
- package/scripts/tests/test-validate-plans.sh +187 -0
- package/scripts/tests/test-validate-plugin.sh +106 -0
- package/scripts/tests/test-validate-prd.sh +184 -0
- package/scripts/tests/test-validate-skills.sh +134 -0
- package/scripts/validate-all.sh +57 -0
- package/scripts/validate-commands.sh +67 -0
- package/scripts/validate-hooks.sh +89 -0
- package/scripts/validate-lessons.sh +98 -0
- package/scripts/validate-plan-quality.sh +369 -0
- package/scripts/validate-plans.sh +120 -0
- package/scripts/validate-plugin.sh +86 -0
- package/scripts/validate-policies.sh +42 -0
- package/scripts/validate-prd.sh +118 -0
- package/scripts/validate-skills.sh +96 -0
- package/skills/autocode/SKILL.md +285 -0
- package/skills/autocode/ab-verification.md +51 -0
- package/skills/autocode/code-quality-standards.md +37 -0
- package/skills/autocode/competitive-mode.md +364 -0
- package/skills/brainstorming/SKILL.md +97 -0
- package/skills/capture-lesson/SKILL.md +187 -0
- package/skills/check-lessons/SKILL.md +116 -0
- package/skills/dispatching-parallel-agents/SKILL.md +110 -0
- package/skills/executing-plans/SKILL.md +85 -0
- package/skills/finishing-a-development-branch/SKILL.md +201 -0
- package/skills/receiving-code-review/SKILL.md +72 -0
- package/skills/requesting-code-review/SKILL.md +59 -0
- package/skills/requesting-code-review/code-reviewer.md +82 -0
- package/skills/research/SKILL.md +145 -0
- package/skills/roadmap/SKILL.md +115 -0
- package/skills/subagent-driven-development/SKILL.md +98 -0
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +18 -0
- package/skills/subagent-driven-development/implementer-prompt.md +73 -0
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +57 -0
- package/skills/systematic-debugging/SKILL.md +134 -0
- package/skills/systematic-debugging/condition-based-waiting.md +64 -0
- package/skills/systematic-debugging/defense-in-depth.md +32 -0
- package/skills/systematic-debugging/root-cause-tracing.md +55 -0
- package/skills/test-driven-development/SKILL.md +167 -0
- package/skills/using-git-worktrees/SKILL.md +219 -0
- package/skills/using-superpowers/SKILL.md +54 -0
- package/skills/verification-before-completion/SKILL.md +140 -0
- package/skills/verify/SKILL.md +82 -0
- package/skills/writing-plans/SKILL.md +128 -0
- package/skills/writing-skills/SKILL.md +93 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 29
|
|
3
|
+
title: "Never write secret values into committed files"
|
|
4
|
+
severity: blocker
|
|
5
|
+
languages: [all]
|
|
6
|
+
scope: [universal]
|
|
7
|
+
category: silent-failures
|
|
8
|
+
pattern:
|
|
9
|
+
type: syntactic
|
|
10
|
+
regex: "(api_key|token|password|secret)\\s*=\\s*['\"][^'\"]{8,}"
|
|
11
|
+
description: "Actual secret values hardcoded in source files"
|
|
12
|
+
fix: "Reference secrets by env var name only; in tests use mock values; enforce with pre-commit hooks"
|
|
13
|
+
example:
|
|
14
|
+
bad: |
|
|
15
|
+
# config.py
|
|
16
|
+
API_KEY = 'sk-1234567890abcdef'
|
|
17
|
+
DATABASE_PASSWORD = 'prodPassword123'
|
|
18
|
+
|
|
19
|
+
# Committed to repo, exposed to anyone with access
|
|
20
|
+
good: |
|
|
21
|
+
# config.py
|
|
22
|
+
import os
|
|
23
|
+
API_KEY = os.environ.get('API_KEY', '')
|
|
24
|
+
DATABASE_PASSWORD = os.environ.get('DATABASE_PASSWORD', '')
|
|
25
|
+
|
|
26
|
+
# .env (never committed, sourced by deployment)
|
|
27
|
+
API_KEY=sk-1234567890abcdef
|
|
28
|
+
DATABASE_PASSWORD=prodPassword123
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Observation
|
|
32
|
+
|
|
33
|
+
Secrets hardcoded in source files are committed to version control, exposing them to anyone with repo access. Even after deletion, secrets remain in git history forever.
|
|
34
|
+
|
|
35
|
+
## Insight
|
|
36
|
+
|
|
37
|
+
Source code is assumed to be shareable (it's version-controlled, reviewed, archived). Secrets are the opposite (must be kept private, rotated, compartmentalized). Mixing them violates the principle of least privilege and creates an irreversible exposure risk.
|
|
38
|
+
|
|
39
|
+
## Lesson
|
|
40
|
+
|
|
41
|
+
**Never write secret values (passwords, API keys, tokens) into any file that gets committed:**
|
|
42
|
+
|
|
43
|
+
1. **Configuration files**: Use environment variables with `os.environ.get()` (Python) or `process.env` (Node.js)
|
|
44
|
+
2. **Tests**: Use mocks, fixtures, or test fixtures; never real credentials
|
|
45
|
+
3. **.env files**: Create locally, gitignore them, source them at runtime
|
|
46
|
+
4. **Pre-commit hooks**: Add linters (gitleaks, detect-secrets) to reject commits containing secrets
|
|
47
|
+
|
|
48
|
+
If a secret is committed:
|
|
49
|
+
|
|
50
|
+
1. Rotate it immediately (invalidate the exposed credential)
|
|
51
|
+
2. Scrub it from history (git filter-branch, BFG repo cleaner)
|
|
52
|
+
3. Document the incident
|
|
53
|
+
|
|
54
|
+
Recommended workflow:
|
|
55
|
+
|
|
56
|
+
- `config.py` reads from `os.environ`
|
|
57
|
+
- `.env` (gitignored) contains local values
|
|
58
|
+
- CI/deployment sets env vars via secrets manager (Vault, AWS Secrets, etc.)
|
|
59
|
+
- Tests use fixtures/mocks, no real credentials
|
|
60
|
+
|
|
61
|
+
Verify by inspecting the last 50 commits: `git log --all -S 'sk-' | head -50` should find zero matches.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 30
|
|
3
|
+
title: "Cache/registry updates must merge, never replace"
|
|
4
|
+
severity: should-fix
|
|
5
|
+
languages: [python, javascript, all]
|
|
6
|
+
scope: [universal]
|
|
7
|
+
category: integration-boundaries
|
|
8
|
+
pattern:
|
|
9
|
+
type: semantic
|
|
10
|
+
description: "Cache update replaces entire cache instead of merging, losing entries from other modules"
|
|
11
|
+
fix: "Use cache.update(new_entries) not cache = new_entries"
|
|
12
|
+
example:
|
|
13
|
+
bad: |
|
|
14
|
+
# Module A
|
|
15
|
+
cache = {'user:1': 'Alice'}
|
|
16
|
+
|
|
17
|
+
# Module B updates cache
|
|
18
|
+
def refresh_posts():
|
|
19
|
+
posts = fetch_posts()
|
|
20
|
+
cache = {'post:1': 'Hello'} # Replaces entire cache!
|
|
21
|
+
# user:1 lost
|
|
22
|
+
|
|
23
|
+
# Now cache only has posts, users are gone
|
|
24
|
+
good: |
|
|
25
|
+
# Shared cache object
|
|
26
|
+
cache = {}
|
|
27
|
+
|
|
28
|
+
# Module B updates cache (merge, don't replace)
|
|
29
|
+
def refresh_posts():
|
|
30
|
+
posts = fetch_posts()
|
|
31
|
+
cache.update({f'post:{p.id}': p for p in posts})
|
|
32
|
+
# All previous entries preserved
|
|
33
|
+
|
|
34
|
+
# Cache has both users and posts
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Observation
|
|
38
|
+
|
|
39
|
+
When multiple modules access a shared cache, replacing it from one module loses entries written by others. A refresh operation in Module B that replaces the cache erases data from Module A that's still valid.
|
|
40
|
+
|
|
41
|
+
## Insight
|
|
42
|
+
|
|
43
|
+
Cache semantics depend on ownership. If the cache is owned by one module, that module can replace it. If it's shared, updates must be additive or selective. Replacement (assignment) assumes sole ownership; without it, you lose data from other modules.
|
|
44
|
+
|
|
45
|
+
## Lesson
|
|
46
|
+
|
|
47
|
+
When updating a shared cache or registry:
|
|
48
|
+
|
|
49
|
+
1. **Merge, don't replace**: Use `cache.update()` (Python dict), `Object.assign()` (JavaScript), or similar. Never reassign the cache variable.
|
|
50
|
+
2. **Selective update**: If you need to replace specific keys, use `cache.pop(key)` then `cache[key] = value`, or `cache.update({key: value})`.
|
|
51
|
+
3. **TTL/expiry**: For caches with stale data, use timestamps or TTLs instead of wholesale replacement. Stale entries expire; fresh entries remain.
|
|
52
|
+
4. **Ownership**: Document which module owns the cache. If multiple modules write to it, document the contract: "All posts keys start with `post:`, all user keys start with `user:`. Modules only touch their namespace."
|
|
53
|
+
|
|
54
|
+
Example of selective update:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Only replace posts, keep other entries
|
|
58
|
+
posts_dict = {f'post:{p.id}': p for p in new_posts}
|
|
59
|
+
cache.update(posts_dict) # user:1 and user:2 still there
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Test this by simulating concurrent updates and verifying data from both modules persists.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 31
|
|
3
|
+
title: "Verify units at every boundary (0-1 vs 0-100)"
|
|
4
|
+
severity: should-fix
|
|
5
|
+
languages: [all]
|
|
6
|
+
scope: [universal]
|
|
7
|
+
category: integration-boundaries
|
|
8
|
+
pattern:
|
|
9
|
+
type: semantic
|
|
10
|
+
description: "Data crosses boundary with implicit unit change (proportion vs percentage)"
|
|
11
|
+
fix: "Verify units at every boundary; add unit to variable names (accuracy_pct, ratio_0_1)"
|
|
12
|
+
example:
|
|
13
|
+
bad: |
|
|
14
|
+
# Model outputs probability (0-1)
|
|
15
|
+
def predict(input):
|
|
16
|
+
return model.predict(input) # Returns 0.85 (85% confidence)
|
|
17
|
+
|
|
18
|
+
# UI assumes percentage (0-100)
|
|
19
|
+
confidence = predict(data)
|
|
20
|
+
ui.show_progress_bar(confidence) # Shows 0.85% instead of 85%!
|
|
21
|
+
good: |
|
|
22
|
+
# Clear units in names and documentation
|
|
23
|
+
def predict(input):
|
|
24
|
+
return model.predict(input) # Returns probability_ratio_0_1
|
|
25
|
+
|
|
26
|
+
# UI explicitly converts
|
|
27
|
+
probability_ratio_0_1 = predict(data)
|
|
28
|
+
confidence_pct = probability_ratio_0_1 * 100
|
|
29
|
+
ui.show_progress_bar(confidence_pct) # Shows 85%
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Observation
|
|
33
|
+
|
|
34
|
+
Data flows between systems with different unit conventions: probabilities (0-1), percentages (0-100), milliseconds vs seconds, ppm vs ppb. A boundary crossing without explicit conversion silently produces wrong results with no error.
|
|
35
|
+
|
|
36
|
+
## Insight
|
|
37
|
+
|
|
38
|
+
Unit mismatches are silent failures because both sides are syntactically valid — a number is a number. The bug isn't a crash, it's a wrong result. A 0.85 probability rendered as 0.85% is off by two orders of magnitude but the code runs without error.
|
|
39
|
+
|
|
40
|
+
## Lesson
|
|
41
|
+
|
|
42
|
+
At every data boundary (API, database, service-to-service), document and verify units:
|
|
43
|
+
|
|
44
|
+
1. **Variable names include units**: `accuracy_pct`, `ratio_0_1`, `duration_ms`, `temp_celsius`
|
|
45
|
+
2. **API contracts specify units**: "response returns confidence as float 0-1, not percentage"
|
|
46
|
+
3. **Conversion explicit**: `pct = ratio_0_1 * 100` is clear; `pct = ratio_0_1` is not
|
|
47
|
+
4. **Tests verify conversion**: Test that a 0.5 probability produces a 50% display value
|
|
48
|
+
|
|
49
|
+
Example contract in docs or code:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
GET /model/predict
|
|
53
|
+
Response: { "probability_ratio_0_1": 0.85 }
|
|
54
|
+
The probability is returned as a ratio (0-1), NOT a percentage.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Add unit verification tests:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
result = predict(data)
|
|
61
|
+
assert 0 <= result <= 1, f"Expected probability 0-1, got {result}"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For databases, use migration notes: "analytics.confidence column changed from integer (0-100) to float (0-1) in v2.1."
|
|
65
|
+
|
|
66
|
+
Verify by running data through all boundaries and spot-checking units at each step.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 32
|
|
3
|
+
title: "Module lifecycle: subscribe after init gate, unsubscribe on shutdown"
|
|
4
|
+
severity: should-fix
|
|
5
|
+
languages: [python, javascript]
|
|
6
|
+
scope: [universal]
|
|
7
|
+
category: resource-lifecycle
|
|
8
|
+
pattern:
|
|
9
|
+
type: semantic
|
|
10
|
+
description: "Component subscribes to events in constructor but never unsubscribes on shutdown"
|
|
11
|
+
fix: "Subscribe in initialize() after startup gate, store callback ref on self, unsubscribe in shutdown()"
|
|
12
|
+
example:
|
|
13
|
+
bad: |
|
|
14
|
+
class EventListener:
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.event_bus = get_event_bus()
|
|
17
|
+
self.event_bus.subscribe('user_login', self.on_login)
|
|
18
|
+
# subscribe in __init__, no unsubscribe
|
|
19
|
+
self.callback_ref = None # Lost reference
|
|
20
|
+
|
|
21
|
+
def on_login(self, event):
|
|
22
|
+
print(f"User {event.user} logged in")
|
|
23
|
+
|
|
24
|
+
# No shutdown method, so callback never unsubscribed
|
|
25
|
+
good: |
|
|
26
|
+
class EventListener:
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.event_bus = None
|
|
29
|
+
self.callback_ref = None
|
|
30
|
+
|
|
31
|
+
async def initialize(self):
|
|
32
|
+
self.event_bus = await get_event_bus()
|
|
33
|
+
self.callback_ref = self.on_login
|
|
34
|
+
self.event_bus.subscribe('user_login', self.callback_ref)
|
|
35
|
+
|
|
36
|
+
def on_login(self, event):
|
|
37
|
+
print(f"User {event.user} logged in")
|
|
38
|
+
|
|
39
|
+
async def shutdown(self):
|
|
40
|
+
if self.callback_ref:
|
|
41
|
+
self.event_bus.unsubscribe('user_login', self.callback_ref)
|
|
42
|
+
self.callback_ref = None
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Observation
|
|
46
|
+
|
|
47
|
+
Components subscribe to events in constructors but rarely unsubscribe. On shutdown, stale callbacks remain registered and continue firing, creating memory leaks and ghost events.
|
|
48
|
+
|
|
49
|
+
## Insight
|
|
50
|
+
|
|
51
|
+
Constructors are for initialization; cleanup is for shutdown. Mixing them violates the resource lifecycle principle. A callback registered in `__init__` may outlive the component because nothing explicitly removes it.
|
|
52
|
+
|
|
53
|
+
## Lesson
|
|
54
|
+
|
|
55
|
+
Follow this subscription lifecycle:
|
|
56
|
+
|
|
57
|
+
1. **Separate init/shutdown**: Never subscribe in `__init__`. Use an explicit `initialize()` method.
|
|
58
|
+
2. **Store callback reference**: Keep a reference to the callback on `self` so you can unsubscribe later.
|
|
59
|
+
3. **Unsubscribe on shutdown**: In `shutdown()`, unsubscribe and set callback to None.
|
|
60
|
+
|
|
61
|
+
Pattern:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
class Component:
|
|
65
|
+
def __init__(self):
|
|
66
|
+
self.event_bus = None
|
|
67
|
+
self.on_event_callback = None
|
|
68
|
+
|
|
69
|
+
async def initialize(self):
|
|
70
|
+
self.event_bus = await get_event_bus()
|
|
71
|
+
self.on_event_callback = self.on_event # Store ref
|
|
72
|
+
self.event_bus.subscribe('event_type', self.on_event_callback)
|
|
73
|
+
|
|
74
|
+
def on_event(self, event):
|
|
75
|
+
# Handle event
|
|
76
|
+
|
|
77
|
+
async def shutdown(self):
|
|
78
|
+
if self.on_event_callback:
|
|
79
|
+
self.event_bus.unsubscribe('event_type', self.on_event_callback)
|
|
80
|
+
self.on_event_callback = None
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Test this by:
|
|
84
|
+
1. Create component
|
|
85
|
+
2. Verify callback is registered (count subscribers)
|
|
86
|
+
3. Shutdown component
|
|
87
|
+
4. Fire event, verify callback doesn't fire (or count unchanged)
|
|
88
|
+
|
|
89
|
+
This pattern is critical for long-running services where components are created and destroyed repeatedly.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 33
|
|
3
|
+
title: "Async iteration over mutable collections needs snapshot"
|
|
4
|
+
severity: blocker
|
|
5
|
+
languages: [python]
|
|
6
|
+
scope: [language:python]
|
|
7
|
+
category: async-traps
|
|
8
|
+
pattern:
|
|
9
|
+
type: syntactic
|
|
10
|
+
regex: "for .+ in self\\..+:"
|
|
11
|
+
description: "Iterating over instance attribute in async function without snapshot"
|
|
12
|
+
fix: "Snapshot before iterating: for item in list(my_set):"
|
|
13
|
+
example:
|
|
14
|
+
bad: |
|
|
15
|
+
class EventDispatcher:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.subscribers = set()
|
|
18
|
+
|
|
19
|
+
async def dispatch(self, event):
|
|
20
|
+
# Iterating over mutable set in async context
|
|
21
|
+
for subscriber in self.subscribers: # Can raise "Set changed during iteration"
|
|
22
|
+
await subscriber.handle(event)
|
|
23
|
+
|
|
24
|
+
async def unsubscribe(self, subscriber):
|
|
25
|
+
self.subscribers.discard(subscriber)
|
|
26
|
+
good: |
|
|
27
|
+
class EventDispatcher:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.subscribers = set()
|
|
30
|
+
|
|
31
|
+
async def dispatch(self, event):
|
|
32
|
+
# Snapshot before iterating
|
|
33
|
+
subscribers_copy = list(self.subscribers)
|
|
34
|
+
for subscriber in subscribers_copy:
|
|
35
|
+
await subscriber.handle(event)
|
|
36
|
+
|
|
37
|
+
async def unsubscribe(self, subscriber):
|
|
38
|
+
self.subscribers.discard(subscriber)
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Observation
|
|
42
|
+
|
|
43
|
+
In async contexts, iterating over a mutable collection (set, dict) that can be modified by concurrent code raises `RuntimeError: Set changed during iteration`. Synchronous iteration is safe because the event loop is blocked; async iteration is not.
|
|
44
|
+
|
|
45
|
+
## Insight
|
|
46
|
+
|
|
47
|
+
Async/await allows other tasks to run between iterations. If another task modifies the collection you're iterating over, Python raises an error. The instinct is to ignore this risk in single-threaded async code, but multiple tasks can run in the same thread.
|
|
48
|
+
|
|
49
|
+
## Lesson
|
|
50
|
+
|
|
51
|
+
When iterating over a collection in an async function:
|
|
52
|
+
|
|
53
|
+
1. **Always snapshot first**: `for item in list(collection)` creates a snapshot immune to concurrent modification
|
|
54
|
+
2. **Copy the right way**:
|
|
55
|
+
- Sets: `list(my_set)`
|
|
56
|
+
- Dicts: `dict(my_dict)` or `list(my_dict.items())`
|
|
57
|
+
- Lists: `my_list.copy()` or `list(my_list)`
|
|
58
|
+
3. **Verify the pattern**: Grep for `for .+ in self\\.` in async functions and check for snapshots
|
|
59
|
+
|
|
60
|
+
Pattern:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
async def broadcast(self):
|
|
64
|
+
# Snapshot before any await
|
|
65
|
+
handlers_copy = list(self.handlers)
|
|
66
|
+
for handler in handlers_copy:
|
|
67
|
+
await handler.process()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Test by subscribing/unsubscribing in a concurrent task while dispatching, and verify no RuntimeError is raised.
|
|
71
|
+
|
|
72
|
+
This is Python-specific. JavaScript's for-of and async iteration have different semantics, but the same principle applies: if concurrent code modifies the collection, snapshot first.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 34
|
|
3
|
+
title: "Caller-side missing await silently discards work"
|
|
4
|
+
severity: blocker
|
|
5
|
+
languages: [python, javascript]
|
|
6
|
+
scope: [universal]
|
|
7
|
+
category: async-traps
|
|
8
|
+
pattern:
|
|
9
|
+
type: semantic
|
|
10
|
+
description: "Async function called without await, coroutine created but never executed"
|
|
11
|
+
fix: "Always await async calls; use create_task() with done_callback for fire-and-forget"
|
|
12
|
+
example:
|
|
13
|
+
bad: |
|
|
14
|
+
async def save_to_database(data):
|
|
15
|
+
await db.save(data)
|
|
16
|
+
print("Saved!")
|
|
17
|
+
|
|
18
|
+
async def main():
|
|
19
|
+
save_to_database(data) # Missing await!
|
|
20
|
+
# Function never executed, "Saved!" never prints
|
|
21
|
+
print("Done") # Prints immediately, before save completes
|
|
22
|
+
|
|
23
|
+
# Result: data may never be saved
|
|
24
|
+
good: |
|
|
25
|
+
async def main():
|
|
26
|
+
# Option 1: await (blocking)
|
|
27
|
+
await save_to_database(data)
|
|
28
|
+
|
|
29
|
+
# Option 2: fire-and-forget with task
|
|
30
|
+
task = asyncio.create_task(save_to_database(data))
|
|
31
|
+
task.add_done_callback(handle_save_error)
|
|
32
|
+
|
|
33
|
+
print("Done")
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Observation
|
|
37
|
+
|
|
38
|
+
Calling an async function without `await` creates a coroutine object but doesn't execute it. The work is discarded, often silently. In Python, the event loop may warn "coroutine was never awaited"; in JavaScript, it's silent.
|
|
39
|
+
|
|
40
|
+
## Insight
|
|
41
|
+
|
|
42
|
+
Async functions are lazy — they return a coroutine/promise that must be awaited to execute. Missing `await` is a type error (object of wrong type is created), but Python's runtime allows it. This is a language design quirk: async functions look like regular functions but require explicit awaiting.
|
|
43
|
+
|
|
44
|
+
## Lesson
|
|
45
|
+
|
|
46
|
+
**Always await async calls:**
|
|
47
|
+
|
|
48
|
+
1. **Default: await**: `await save_to_database(data)`
|
|
49
|
+
2. **Fire-and-forget**: If you don't want to block, use `asyncio.create_task()` with error handling:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
task = asyncio.create_task(save_to_database(data))
|
|
53
|
+
task.add_done_callback(lambda t: t.result() if t.exception() is None else None)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
3. **Never just call**: `save_to_database(data)` is a bug
|
|
57
|
+
|
|
58
|
+
Linting:
|
|
59
|
+
|
|
60
|
+
- Python: Use `pylint` with `no-unused-variable` or linters that detect unawaited coroutines
|
|
61
|
+
- JavaScript: Use TypeScript or ESLint with `no-floating-promises` rule
|
|
62
|
+
|
|
63
|
+
Test: Verify that a fire-and-forget task completes before the program exits. Use a counter or log to verify the callback was called.
|
|
64
|
+
|
|
65
|
+
This is critical in production: losing async work silently is a data loss bug.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 35
|
|
3
|
+
title: "Duplicate registration IDs cause silent overwrite"
|
|
4
|
+
severity: should-fix
|
|
5
|
+
languages: [python, javascript, all]
|
|
6
|
+
scope: [universal]
|
|
7
|
+
category: silent-failures
|
|
8
|
+
pattern:
|
|
9
|
+
type: semantic
|
|
10
|
+
description: "Multiple components register with the same ID, last one silently overwrites earlier ones"
|
|
11
|
+
fix: "Check for existing registration before inserting; log warning or raise on duplicate"
|
|
12
|
+
example:
|
|
13
|
+
bad: |
|
|
14
|
+
class HandlerRegistry:
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.handlers = {}
|
|
17
|
+
|
|
18
|
+
def register(self, handler_id, handler):
|
|
19
|
+
self.handlers[handler_id] = handler # Silently overwrites
|
|
20
|
+
|
|
21
|
+
registry = HandlerRegistry()
|
|
22
|
+
registry.register('payment', PaymentHandler())
|
|
23
|
+
registry.register('payment', StripeHandler()) # Oops, overwrites first one
|
|
24
|
+
# Now only Stripe handler is registered, PaymentHandler is lost
|
|
25
|
+
|
|
26
|
+
registry.handle('payment', data) # Only StripeHandler runs
|
|
27
|
+
good: |
|
|
28
|
+
class HandlerRegistry:
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.handlers = {}
|
|
31
|
+
|
|
32
|
+
def register(self, handler_id, handler):
|
|
33
|
+
if handler_id in self.handlers:
|
|
34
|
+
raise ValueError(f"Handler '{handler_id}' already registered")
|
|
35
|
+
self.handlers[handler_id] = handler
|
|
36
|
+
|
|
37
|
+
registry = HandlerRegistry()
|
|
38
|
+
registry.register('payment', PaymentHandler())
|
|
39
|
+
registry.register('payment', StripeHandler()) # Raises ValueError
|
|
40
|
+
# Bug caught immediately
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Observation
|
|
44
|
+
|
|
45
|
+
Registries that accept duplicate IDs silently overwrite earlier registrations. A second module registering with the same ID erases the first module's handler, and there's no error.
|
|
46
|
+
|
|
47
|
+
## Insight
|
|
48
|
+
|
|
49
|
+
Registries are designed to prevent collisions: each ID maps to one handler. Without collision detection, the register operation becomes a silent update, and duplicate IDs are indistinguishable from intentional overwrites.
|
|
50
|
+
|
|
51
|
+
## Lesson
|
|
52
|
+
|
|
53
|
+
When building a registration system (event handlers, plugins, middleware):
|
|
54
|
+
|
|
55
|
+
1. **Check for duplicates**: Before inserting, verify the ID isn't already registered.
|
|
56
|
+
2. **Three options on duplicate**:
|
|
57
|
+
- **Reject** (strict): Raise an exception. Fails fast, prevents bugs.
|
|
58
|
+
- **Warn** (permissive): Log a warning, then overwrite. Allows dynamic reconfiguration but can mask bugs.
|
|
59
|
+
- **Append** (list-based): If multiple handlers per ID are valid, use a list instead of a dict.
|
|
60
|
+
|
|
61
|
+
Pattern:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
def register(self, handler_id, handler):
|
|
65
|
+
if handler_id in self.handlers:
|
|
66
|
+
raise ValueError(f"Duplicate registration: '{handler_id}'")
|
|
67
|
+
self.handlers[handler_id] = handler
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or with warning:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
def register(self, handler_id, handler):
|
|
74
|
+
if handler_id in self.handlers:
|
|
75
|
+
logger.warning(f"Overwriting handler '{handler_id}'")
|
|
76
|
+
self.handlers[handler_id] = handler
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Test by:
|
|
80
|
+
1. Register handler A with ID 'foo'
|
|
81
|
+
2. Register handler B with ID 'foo'
|
|
82
|
+
3. Verify exception is raised OR warning is logged
|
|
83
|
+
4. Verify the correct handler is in the registry afterward
|
|
84
|
+
|
|
85
|
+
Choose **reject** (strict) by default unless dynamic reconfiguration is explicitly required.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 0036
|
|
3
|
+
title: "WebSocket dirty disconnects raise RuntimeError, not close"
|
|
4
|
+
severity: should-fix
|
|
5
|
+
languages: [python]
|
|
6
|
+
scope: [language:python]
|
|
7
|
+
category: resource-lifecycle
|
|
8
|
+
pattern:
|
|
9
|
+
type: semantic
|
|
10
|
+
description: "WebSocket send after client disconnects without close frame raises RuntimeError instead of WebSocketDisconnect"
|
|
11
|
+
fix: "Wrap all WebSocket sends in try/except RuntimeError and clean up the connection"
|
|
12
|
+
example:
|
|
13
|
+
bad: |
|
|
14
|
+
async def broadcast(self, message):
|
|
15
|
+
for ws in self.connections:
|
|
16
|
+
await ws.send_json({"msg": message})
|
|
17
|
+
good: |
|
|
18
|
+
async def broadcast(self, message):
|
|
19
|
+
for ws in self.connections:
|
|
20
|
+
try:
|
|
21
|
+
await ws.send_json({"msg": message})
|
|
22
|
+
except RuntimeError:
|
|
23
|
+
self.connections.remove(ws)
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Observation
|
|
27
|
+
WebSocket connections that are terminated by the client without a proper close frame (e.g., mobile network loss, browser tab close) raise `RuntimeError` on the next `send()` call, not `WebSocketDisconnect`. This exception type varies by websocket library implementation and client disconnection method.
|
|
28
|
+
|
|
29
|
+
## Insight
|
|
30
|
+
Developers expect WebSocket sends to raise `WebSocketDisconnect` on all disconnection types, so they only catch that exception. Dirty disconnects bypass the close handshake, triggering RuntimeError instead. This causes unhandled exceptions in broadcast loops.
|
|
31
|
+
|
|
32
|
+
## Lesson
|
|
33
|
+
Always wrap WebSocket sends in `try/except RuntimeError` in addition to connection-close handlers. Store connection state on `self`, remove failed connections immediately, and log the disconnection for visibility. Test with mobile network loss simulation, not just clean closes.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 0037
|
|
3
|
+
title: "Parallel agents sharing worktree corrupt staging area"
|
|
4
|
+
severity: blocker
|
|
5
|
+
languages: [all]
|
|
6
|
+
scope: [universal]
|
|
7
|
+
category: integration-boundaries
|
|
8
|
+
pattern:
|
|
9
|
+
type: semantic
|
|
10
|
+
description: "Multiple agents or CI jobs commit to the same git worktree, corrupting the staging area"
|
|
11
|
+
fix: "Each parallel agent gets its own git worktree; never share a worktree between concurrent processes"
|
|
12
|
+
example:
|
|
13
|
+
bad: |
|
|
14
|
+
# Both agents write to same repo
|
|
15
|
+
Agent A: git add file1.py && git commit -m "feat: A"
|
|
16
|
+
Agent B: git add file2.py && git commit -m "feat: B" # corrupts A's staging
|
|
17
|
+
good: |
|
|
18
|
+
# Each agent has isolated worktree
|
|
19
|
+
git worktree add agent-a-branch
|
|
20
|
+
git worktree add agent-b-branch
|
|
21
|
+
# Agents work in separate directories
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Observation
|
|
25
|
+
When multiple agents or CI processes write to the same git repository directory concurrently, they interfere with each other's staging area, index locks, and commit state. This results in "fatal: cannot lock ref" errors, lost commits, or commits with wrong file combinations.
|
|
26
|
+
|
|
27
|
+
## Insight
|
|
28
|
+
Git's index is a single file (`.git/index`) shared across all operations in a repository. The staging area is not thread-safe by design. Concurrent writes to this file corrupt it.
|
|
29
|
+
|
|
30
|
+
## Lesson
|
|
31
|
+
Never share a git worktree between concurrent processes. Use `git worktree add` to create isolated working directories for each parallel agent. Each worktree has its own index and staging area. Verify worktrees are cleaned up and removed after use.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 0038
|
|
3
|
+
title: "Subscribe without stored ref = cannot unsubscribe"
|
|
4
|
+
severity: should-fix
|
|
5
|
+
languages: [python, javascript]
|
|
6
|
+
scope: [universal]
|
|
7
|
+
category: resource-lifecycle
|
|
8
|
+
pattern:
|
|
9
|
+
type: syntactic
|
|
10
|
+
regex: "\.subscribe\(lambda|\.subscribe\(\s*\("
|
|
11
|
+
description: "Event subscription with anonymous lambda cannot be unsubscribed later"
|
|
12
|
+
fix: "Store callback on self before subscribing; unsubscribe with stored ref in shutdown"
|
|
13
|
+
example:
|
|
14
|
+
bad: |
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.emitter.subscribe(lambda event: self.on_event(event))
|
|
17
|
+
|
|
18
|
+
def shutdown(self):
|
|
19
|
+
# No way to unsubscribe -- callback ref lost
|
|
20
|
+
good: |
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._callback = lambda event: self.on_event(event)
|
|
23
|
+
self.emitter.subscribe(self._callback)
|
|
24
|
+
|
|
25
|
+
def shutdown(self):
|
|
26
|
+
self.emitter.unsubscribe(self._callback)
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Observation
|
|
30
|
+
Event subscriptions created with inline lambdas or anonymous functions cannot be unsubscribed later because the callback reference is not stored. In shutdown or cleanup code, there's no way to reference the callback to remove it.
|
|
31
|
+
|
|
32
|
+
## Insight
|
|
33
|
+
The subscriber pattern returns a reference to the callback if you need to unsubscribe later. When the callback is created inline and not stored, that reference is lost immediately after subscription.
|
|
34
|
+
|
|
35
|
+
## Lesson
|
|
36
|
+
Always store event callbacks on `self` before subscribing. Unsubscribe using the stored reference in `shutdown()` or cleanup methods. Test that subscriptions are properly cleaned up and no callbacks fire after shutdown. This prevents memory leaks and stale event handlers.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 0039
|
|
3
|
+
title: "Fallback `or default()` hides initialization bugs"
|
|
4
|
+
severity: should-fix
|
|
5
|
+
languages: [python]
|
|
6
|
+
scope: [language:python]
|
|
7
|
+
category: silent-failures
|
|
8
|
+
pattern:
|
|
9
|
+
type: semantic
|
|
10
|
+
description: "Expression like `self._resource or Resource()` creates new resource every access when _resource was never initialized"
|
|
11
|
+
fix: "Replace with guard return + warning: if not self._resource: logger.warning('not initialized'); return"
|
|
12
|
+
example:
|
|
13
|
+
bad: |
|
|
14
|
+
def get_value(self):
|
|
15
|
+
# If _resource never initialized, creates new one silently
|
|
16
|
+
return (self._resource or Resource()).value
|
|
17
|
+
|
|
18
|
+
# Bug: each call creates a new Resource if never initialized
|
|
19
|
+
good: |
|
|
20
|
+
def get_value(self):
|
|
21
|
+
if not self._resource:
|
|
22
|
+
logger.warning("Resource not initialized")
|
|
23
|
+
return None
|
|
24
|
+
return self._resource.value
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Observation
|
|
28
|
+
Using `or` as a fallback to create a default object (`self._resource or Resource()`) masks initialization bugs. The code never fails; it silently creates a new object on every access, leading to duplicate work, lost state, and difficult-to-trace behavior.
|
|
29
|
+
|
|
30
|
+
## Insight
|
|
31
|
+
Fallback patterns hide the bug rather than fail fast. The developer doesn't know initialization was skipped because the code "works." State stored in the first Resource is lost on the next access, causing subtle state inconsistencies.
|
|
32
|
+
|
|
33
|
+
## Lesson
|
|
34
|
+
Replace fallback patterns with explicit guard checks. Log a warning if the resource is not initialized, then return early or raise an exception. This makes initialization bugs fail fast and visible. Test initialization paths explicitly to ensure resources are initialized before first use.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 0040
|
|
3
|
+
title: "Process all events when 5% are relevant -- filter first"
|
|
4
|
+
severity: should-fix
|
|
5
|
+
languages: [all]
|
|
6
|
+
scope: [domain:ha-aria]
|
|
7
|
+
category: performance
|
|
8
|
+
pattern:
|
|
9
|
+
type: semantic
|
|
10
|
+
description: "Event handler processes every event when only a small fraction are relevant"
|
|
11
|
+
fix: "Filter by domain/type/source at the top of the handler before any expensive operations"
|
|
12
|
+
example:
|
|
13
|
+
bad: |
|
|
14
|
+
def on_event(self, event):
|
|
15
|
+
# Processes every event, even irrelevant ones
|
|
16
|
+
parsed = expensive_parse(event)
|
|
17
|
+
if parsed.domain != "target_domain":
|
|
18
|
+
return
|
|
19
|
+
self.handle_target(parsed)
|
|
20
|
+
good: |
|
|
21
|
+
def on_event(self, event):
|
|
22
|
+
if event.domain != "target_domain":
|
|
23
|
+
return
|
|
24
|
+
# Only expensive parse for relevant events
|
|
25
|
+
parsed = expensive_parse(event)
|
|
26
|
+
self.handle_target(parsed)
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Observation
|
|
30
|
+
Event handlers receive high-volume event streams (e.g., Home Assistant state_changed, MQTT topic subscriptions). Filtering happens after expensive operations (parsing, decoding, database lookups) instead of before, wasting CPU on irrelevant events.
|
|
31
|
+
|
|
32
|
+
## Insight
|
|
33
|
+
Early filtering is a free optimization. Checking a simple field (`event.type`, `event.domain`) takes nanoseconds. Do this first, return immediately for irrelevant events, then proceed with expensive operations only for matching events.
|
|
34
|
+
|
|
35
|
+
## Lesson
|
|
36
|
+
Filter events at the very top of the handler using simple field checks before any expensive operations. Structure the filter to reject irrelevant events as quickly as possible. If filtering becomes complex, move it to a decorator or middleware layer. Test with high event volume (1000s/sec) to verify performance doesn't degrade.
|