@tyroneross/build-loop 0.36.0 → 0.43.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/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/build-loop-surface.mdc +12 -11
- package/.cursor/rules/skill-index.mdc +33 -0
- package/AGENTS.md +213 -34
- package/README.md +99 -31
- package/agents/advisor.md +4 -4
- package/agents/alignment-checker.md +2 -2
- package/agents/architecture-scout.md +4 -4
- package/agents/build-orchestrator.md +38 -36
- package/agents/database-assessor.md +11 -5
- package/agents/design-contract-specialist.md +8 -8
- package/agents/fact-checker.md +13 -3
- package/agents/fix-critique.md +2 -2
- package/agents/independent-auditor.md +60 -7
- package/agents/leak-scanner.md +82 -0
- package/agents/overfitting-reviewer.md +2 -2
- package/agents/plan-critic.md +1 -1
- package/agents/promotion-reviewer.md +5 -5
- package/agents/retrospective-synthesizer.md +138 -35
- package/agents/scope-auditor.md +82 -11
- package/agents/security-reviewer.md +56 -2
- package/agents/self-improvement-architect.md +17 -3
- package/agents/transcript-pattern-miner.md +5 -5
- package/agents/ui-validator.md +1 -1
- package/bin/build-loop-debugger.js +143 -0
- package/bin/build-loop-install.js +1 -4
- package/bin/build-loop-load-probe.js +345 -0
- package/codex-skills/build-loop/SKILL.md +28 -6
- package/commands/feedback.md +37 -0
- package/dist/src/interactive-verifier.d.ts +1 -14
- package/dist/src/interactive-verifier.d.ts.map +1 -1
- package/dist/src/interactive-verifier.js +6 -113
- package/dist/src/interactive-verifier.js.map +1 -1
- package/dist/src/quality.d.ts +5 -0
- package/dist/src/quality.d.ts.map +1 -0
- package/dist/src/quality.js +81 -0
- package/dist/src/quality.js.map +1 -0
- package/dist/src/storage.d.ts.map +1 -1
- package/dist/src/storage.js +37 -3
- package/dist/src/storage.js.map +1 -1
- package/docs/agent-surface-policy.md +35 -31
- package/docs/memory-setup.md +19 -0
- package/hooks/git/pre-push +65 -4
- package/hooks/hooks.json +95 -38
- package/hooks/pre-commit +20 -1
- package/hooks/pre-edit-rally-point.sh +10 -3
- package/hooks/session-start-codex-hook-trust.sh +30 -0
- package/hooks/session-start-git-hooks.sh +3 -1
- package/hooks/session-start-rally-point.sh +52 -4
- package/hooks/session-start-worktree-gc.sh +47 -94
- package/hooks/stop-transcript-sweep.sh +173 -0
- package/hooks/test_closeout.sh +14 -2
- package/package.json +8 -7
- package/scripts/README.md +1 -1
- package/scripts/_paths.py +65 -0
- package/scripts/groundwork_exchange.py +1012 -0
- package/scripts/install_memory.py +33 -1
- package/scripts/lessons_index/ingest.py +13 -2
- package/scripts/lessons_index/query.py +36 -13
- package/scripts/memory_context/__init__.py +108 -14
- package/scripts/memory_graph/__init__.py +5 -1
- package/scripts/project_resolver.py +42 -36
- package/scripts/sync_plugin_cache.py +37 -2
- package/skills/agent-rally-point/SKILL.md +46 -0
- package/skills/api-registry-bridge/SKILL.md +1 -1
- package/skills/architecture/dead/SKILL.md +1 -1
- package/skills/architecture/impact/SKILL.md +1 -1
- package/skills/architecture/review/SKILL.md +1 -1
- package/skills/architecture/rules/SKILL.md +3 -3
- package/skills/architecture/scan/SKILL.md +1 -1
- package/skills/architecture/trace/SKILL.md +1 -1
- package/skills/attribution-standard/SKILL.md +6 -6
- package/skills/auto-decision-capture/SKILL.md +31 -2
- package/skills/auto-finding-capture/SKILL.md +28 -1
- package/skills/build-loop/SKILL.md +131 -23
- package/skills/build-loop/fallbacks.md +16 -21
- package/skills/build-loop/phases/ui-validation.md +2 -2
- package/skills/build-loop/references/advisor-dispatch-ladder.md +1 -1
- package/skills/build-loop/references/apple-native-planning.md +1 -1
- package/skills/build-loop/references/autonomous-and-per-commit-modes.md +11 -5
- package/skills/build-loop/references/autonomy-dashboard.md +115 -0
- package/skills/build-loop/references/capability-routing.md +24 -2
- package/skills/build-loop/references/coordination.md +24 -6
- package/skills/build-loop/references/experiment-results-template.md +15 -3
- package/skills/build-loop/references/leadership.md +1 -1
- package/skills/build-loop/references/memory.md +14 -3
- package/skills/build-loop/references/modular-systems-pack.md +8 -0
- package/skills/build-loop/references/output-style.md +86 -0
- package/skills/build-loop/references/phase-1-assess.md +102 -2
- package/skills/build-loop/references/phase-2-plan.md +9 -1
- package/skills/build-loop/references/phase-3-execute.md +5 -2
- package/skills/build-loop/references/phase-4-review.md +85 -8
- package/skills/build-loop/references/phase-5-iterate.md +76 -8
- package/skills/build-loop/references/phase-6-learn.md +10 -17
- package/skills/build-loop/references/privileged-request-broker.md +254 -0
- package/skills/build-loop/references/resource-aware-execution.md +183 -0
- package/skills/build-loop/references/self-recursive-dev.md +2 -2
- package/skills/build-loop/references/status-output-format.md +207 -0
- package/skills/build-loop/references/verify-dispatch.md +56 -2
- package/skills/building-with-deepagents/SKILL.md +1 -1
- package/skills/claim-scope/SKILL.md +185 -0
- package/skills/color-engine/SKILL.md +103 -0
- package/skills/color-engine/_core.py +464 -0
- package/skills/color-engine/color_engine.py +175 -0
- package/skills/cost-rca/SKILL.md +61 -0
- package/skills/data-plane-worktrees/SKILL.md +139 -0
- package/skills/data-plane-worktrees/agents/openai.yaml +4 -0
- package/skills/database-practice/SKILL.md +200 -0
- package/skills/database-practice/references/diagnostic-queries.sql +126 -0
- package/skills/database-practice/references/vector-and-graph-tuning.md +208 -0
- package/skills/database-practice/scripts/db_table_map.py +1244 -0
- package/skills/database-practice/scripts/test_db_table_map.py +514 -0
- package/skills/debug-loop/SKILL.md +36 -6
- package/skills/debugging-memory/SKILL.md +32 -430
- package/skills/debugging-memory/references/pattern-extraction.md +4 -4
- package/skills/debugging-memory/references/search.md +32 -120
- package/skills/debugging-memory/references/store.md +32 -126
- package/skills/debugging-memory/references/subagent-integration.md +1 -1
- package/skills/decision-queue/SKILL.md +251 -0
- package/skills/decision-queue/assets/template.html +1242 -0
- package/skills/decision-queue/references/example-large-queue-batching.md +164 -0
- package/skills/decision-queue/scripts/regen_template_constants.py +160 -0
- package/skills/defenseclaw-bridge/SKILL.md +2 -2
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +2 -9
- package/skills/drain-proposals/SKILL.md +53 -0
- package/skills/focused-loop-builder/SKILL.md +31 -0
- package/skills/focused-loop-builder/references/spec-format.md +27 -0
- package/skills/handoff/SKILL.md +169 -8
- package/skills/ibr-bridge/SKILL.md +4 -1
- package/skills/knowledge/SKILL.md +26 -14
- package/skills/knowledge/references/review-mode.md +2 -3
- package/skills/knowledge/templates/madr-minimal.md +1 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/model-bakeoff/SKILL.md +48 -10
- package/skills/model-tiering/SKILL.md +92 -31
- package/skills/native-ax-driver/SKILL.md +38 -5
- package/skills/native-ax-driver/scripts/native_driver.py +278 -22
- package/skills/native-ax-driver/scripts/test_native_driver.py +227 -0
- package/skills/optimize/SKILL.md +1 -1
- package/skills/plugin-builder/SKILL.md +48 -1
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +3 -4
- package/skills/plugin-builder/references/distribution.md +13 -2
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +2 -2
- package/skills/plugin-tests/SKILL.md +2 -2
- package/skills/recursive-retrospective/SKILL.md +1 -1
- package/skills/repo-closeout/SKILL.md +17 -0
- package/skills/repo-closeout/agents/openai.yaml +4 -0
- package/skills/repo-maintenance/SKILL.md +179 -0
- package/skills/repo-maintenance/agents/openai.yaml +4 -0
- package/skills/repo-maintenance/references/pre-public-hygiene.md +134 -0
- package/skills/repo-maintenance/references/repository-taxonomy.md +161 -0
- package/skills/repo-maintenance/references/safety-protocol.md +106 -0
- package/skills/repo-maintenance/references/stack-profiles.md +138 -0
- package/skills/repo-maintenance/scripts/audit_repo_maintenance.py +1198 -0
- package/skills/repo-maintenance/scripts/test_audit_repo_maintenance.py +506 -0
- package/skills/repository-intelligence/SKILL.md +189 -0
- package/skills/repository-intelligence/agents/openai.yaml +4 -0
- package/skills/repository-intelligence/references/assessment-rubric.md +88 -0
- package/skills/repository-intelligence/scripts/repository_inventory.py +347 -0
- package/skills/research/SKILL.md +12 -2
- package/skills/root-cause-analysis/SKILL.md +1 -1
- package/skills/runtime-parity-verification/SKILL.md +36 -1
- package/skills/security-methodology/SKILL.md +23 -10
- package/skills/security-methodology/references/agentic-handoff-templates.md +220 -0
- package/skills/security-methodology/references/cross-source-matrix.md +1 -1
- package/skills/security-methodology/references/owasp-agentic-top-10.md +1 -1
- package/skills/security-scan/SKILL.md +55 -15
- package/skills/self-improve/SKILL.md +70 -50
- package/skills/silent-assumptions/SKILL.md +341 -0
- package/skills/silent-assumptions/references/elicitation-detectors.md +342 -0
- package/skills/spec-writing/SKILL.md +128 -24
- package/skills/spec-writing/scripts/check_checklist.py +114 -15
- package/skills/ui-design/SKILL.md +6 -4
- package/skills/ui-design/references/color-engine.md +132 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +8 -8
- package/skills/ui-design/references/ui-guidance-sources.md +1 -1
- package/skills/ui-design/references/universal-design-principles.alt.md +2 -2
- package/plugin-artifacts/codex/.codex-plugin/plugin.json +0 -41
- package/plugin-artifacts/codex/AGENTS.md +0 -560
- package/plugin-artifacts/codex/BUILD-ARTIFACT.md +0 -5
- package/plugin-artifacts/codex/LICENSE +0 -202
- package/plugin-artifacts/codex/README.md +0 -313
- package/plugin-artifacts/codex/assets/build-loop-plugin-icon.png +0 -0
- package/plugin-artifacts/codex/docs/agent-surface-policy.md +0 -63
- package/plugin-artifacts/codex/references/advisor-dispatch-ladder.md +0 -62
- package/plugin-artifacts/codex/references/agent-role-taxonomy.md +0 -135
- package/plugin-artifacts/codex/references/autonomous-and-per-commit-modes.md +0 -161
- package/plugin-artifacts/codex/references/autonomy-config.md +0 -231
- package/plugin-artifacts/codex/references/backlog-system.md +0 -285
- package/plugin-artifacts/codex/references/capability-routing.md +0 -231
- package/plugin-artifacts/codex/references/codex-subagents.md +0 -106
- package/plugin-artifacts/codex/references/coordination-file-template.md +0 -181
- package/plugin-artifacts/codex/references/coordination-rules.md +0 -552
- package/plugin-artifacts/codex/references/dogfood-reload-checkpoint.md +0 -112
- package/plugin-artifacts/codex/references/halt-and-ask-protocol.md +0 -102
- package/plugin-artifacts/codex/references/implementer-envelope-schema.md +0 -302
- package/plugin-artifacts/codex/references/intent-capability-pack.md +0 -257
- package/plugin-artifacts/codex/references/intent-exploration-prompts.md +0 -96
- package/plugin-artifacts/codex/references/leadership.md +0 -72
- package/plugin-artifacts/codex/references/memory-systems.md +0 -261
- package/plugin-artifacts/codex/references/memory.md +0 -313
- package/plugin-artifacts/codex/references/model-tier-mapping.md +0 -296
- package/plugin-artifacts/codex/references/modular-systems-pack.md +0 -96
- package/plugin-artifacts/codex/references/phase-1-assess.md +0 -249
- package/plugin-artifacts/codex/references/phase-2-plan.md +0 -86
- package/plugin-artifacts/codex/references/phase-3-execute.md +0 -49
- package/plugin-artifacts/codex/references/phase-4-review.md +0 -341
- package/plugin-artifacts/codex/references/phase-5-iterate.md +0 -72
- package/plugin-artifacts/codex/references/phase-6-learn.md +0 -58
- package/plugin-artifacts/codex/references/recent-design-structures.md +0 -274
- package/plugin-artifacts/codex/references/research-trigger-policy.md +0 -140
- package/plugin-artifacts/codex/references/runtime-smoke-triggers.md +0 -42
- package/plugin-artifacts/codex/references/self-review.md +0 -234
- package/plugin-artifacts/codex/references/single-writer-commit-protocol.md +0 -90
- package/plugin-artifacts/codex/references/task-capture-policy.md +0 -68
- package/plugin-artifacts/codex/references/ui-io-contract.md +0 -116
- package/plugin-artifacts/codex/references/ui-spotcheck-protocol.md +0 -65
- package/plugin-artifacts/codex/references/verify-dispatch.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/SKILL.md +0 -381
- package/plugin-artifacts/codex/skills/build-loop/detect-plugins.mjs +0 -82
- package/plugin-artifacts/codex/skills/build-loop/eval-guide.md +0 -65
- package/plugin-artifacts/codex/skills/build-loop/fallbacks.md +0 -549
- package/plugin-artifacts/codex/skills/build-loop/phases/fact-check.md +0 -42
- package/plugin-artifacts/codex/skills/build-loop/phases/ui-validation.md +0 -267
- package/plugin-artifacts/codex/skills/build-loop/references/advisor-dispatch-ladder.md +0 -62
- package/plugin-artifacts/codex/skills/build-loop/references/apple-native-planning.md +0 -439
- package/plugin-artifacts/codex/skills/build-loop/references/autonomous-and-per-commit-modes.md +0 -161
- package/plugin-artifacts/codex/skills/build-loop/references/capability-routing.md +0 -231
- package/plugin-artifacts/codex/skills/build-loop/references/codex-subagents.md +0 -106
- package/plugin-artifacts/codex/skills/build-loop/references/coordination.md +0 -161
- package/plugin-artifacts/codex/skills/build-loop/references/correction-aware-capture.md +0 -177
- package/plugin-artifacts/codex/skills/build-loop/references/experiment-results-template.md +0 -101
- package/plugin-artifacts/codex/skills/build-loop/references/independent-auditor.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/intent-capability-pack.md +0 -257
- package/plugin-artifacts/codex/skills/build-loop/references/intent-exploration-prompts.md +0 -96
- package/plugin-artifacts/codex/skills/build-loop/references/leadership.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/memory.md +0 -313
- package/plugin-artifacts/codex/skills/build-loop/references/modular-systems-pack.md +0 -96
- package/plugin-artifacts/codex/skills/build-loop/references/output-style.md +0 -222
- package/plugin-artifacts/codex/skills/build-loop/references/pay-it-forward-arch.md +0 -98
- package/plugin-artifacts/codex/skills/build-loop/references/phase-1-assess.md +0 -249
- package/plugin-artifacts/codex/skills/build-loop/references/phase-2-plan.md +0 -86
- package/plugin-artifacts/codex/skills/build-loop/references/phase-3-execute.md +0 -49
- package/plugin-artifacts/codex/skills/build-loop/references/phase-4-review.md +0 -341
- package/plugin-artifacts/codex/skills/build-loop/references/phase-5-iterate.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/phase-6-learn.md +0 -58
- package/plugin-artifacts/codex/skills/build-loop/references/recent-design-structures.md +0 -274
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/ASSESSMENT.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +0 -149
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +0 -32
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +0 -48
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +0 -60
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +0 -51
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +0 -71
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +0 -52
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/trace-comparison.md +0 -202
- package/plugin-artifacts/codex/skills/build-loop/references/self-recursive-dev.md +0 -77
- package/plugin-artifacts/codex/skills/build-loop/references/self-review.md +0 -234
- package/plugin-artifacts/codex/skills/build-loop/references/ui-io-contract.md +0 -116
- package/plugin-artifacts/codex/skills/build-loop/references/verify-dispatch.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/scanners/audit-design-rules.mjs +0 -476
- package/plugin-artifacts/codex/skills/build-loop/scanners/require-visual-evidence.mjs +0 -239
- package/plugin-artifacts/codex/skills/build-loop/templates/backlog-item.md +0 -35
- package/plugin-artifacts/codex/skills/build-loop/templates/codex-worker-prompt.md +0 -100
- package/plugin-artifacts/codex/skills/build-loop/templates/ui-subagent-prompt.md +0 -179
- package/plugin-artifacts/codex/skills/build-loop/templates/ux-fix-plan.md +0 -40
- package/scripts/build_codex_plugin_artifact.py +0 -321
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for the read-only PostgreSQL table map.
|
|
3
|
+
|
|
4
|
+
The psql runner is monkeypatched with fixture output modelled on a real
|
|
5
|
+
production instance: an HNSW index with 33 lifetime scans against 402k table
|
|
6
|
+
inserts, a table reporting n_live_tup 0 while serving 222k index scans, and a
|
|
7
|
+
vector-search function whose proconfig carries enable_seqscan but no HNSW GUCs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import tempfile
|
|
16
|
+
import unittest
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from unittest import mock
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
MODULE_PATH = Path(__file__).with_name("db_table_map.py")
|
|
22
|
+
SPEC = importlib.util.spec_from_file_location("db_table_map", MODULE_PATH)
|
|
23
|
+
assert SPEC and SPEC.loader
|
|
24
|
+
MODULE = importlib.util.module_from_spec(SPEC)
|
|
25
|
+
SPEC.loader.exec_module(MODULE)
|
|
26
|
+
|
|
27
|
+
MB = 1024 * 1024
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def psql_output(sections: dict[str, list[list[object]]]) -> str:
|
|
31
|
+
lines: list[str] = []
|
|
32
|
+
for name, rows in sections.items():
|
|
33
|
+
lines.append(f"{MODULE.SECTION_MARK}{name}{MODULE.SECTION_END}")
|
|
34
|
+
for row in rows:
|
|
35
|
+
lines.append("\t".join(str(cell) for cell in row))
|
|
36
|
+
return "\n".join(lines) + "\n"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
FIXTURE: dict[str, list[list[object]]] = {
|
|
40
|
+
"window": [
|
|
41
|
+
[
|
|
42
|
+
"2026-02-01 00:00:00+00",
|
|
43
|
+
"2026-09-05 12:00:00+00",
|
|
44
|
+
"",
|
|
45
|
+
"appdb",
|
|
46
|
+
"PostgreSQL 17.4 on aarch64-unknown-linux-gnu",
|
|
47
|
+
]
|
|
48
|
+
],
|
|
49
|
+
"settings": [
|
|
50
|
+
["max_connections", "60", ""],
|
|
51
|
+
["shared_buffers", "32768", "8kB"],
|
|
52
|
+
["work_mem", "3584", "kB"],
|
|
53
|
+
],
|
|
54
|
+
# schema name total heap idx toast live dead ins upd del seq idx_scan vac ana reloptions
|
|
55
|
+
"tables": [
|
|
56
|
+
[
|
|
57
|
+
"public", "article_embedding_chunks",
|
|
58
|
+
3 * 1024 * MB, 400 * MB, 2400 * MB, 8 * MB,
|
|
59
|
+
398_000, 1_200, 402_158, 12, 40, 90, 33,
|
|
60
|
+
"2026-09-01 03:00:00+00", "2026-09-01 03:10:00+00", "",
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"public", "article_categories",
|
|
64
|
+
40 * MB, 20 * MB, 20 * MB, 0,
|
|
65
|
+
0, 0, 5_000, 10, 4_900, 12, 222_793,
|
|
66
|
+
"", "", "",
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"public", "ingest_queue",
|
|
70
|
+
8 * MB, 4 * MB, 4 * MB, 0,
|
|
71
|
+
0, 900, 12_000, 0, 12_000, 3, 0,
|
|
72
|
+
"", "", "fillfactor=70",
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"public", "reference_lookup",
|
|
76
|
+
1 * MB, 512 * 1024, 512 * 1024, 0,
|
|
77
|
+
148, 0, 0, 0, 0, 40, 0,
|
|
78
|
+
"", "", "",
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
"public", "legacy_import_staging",
|
|
82
|
+
0, 0, 0, 0,
|
|
83
|
+
0, 0, 0, 0, 0, 0, 0,
|
|
84
|
+
"", "", "",
|
|
85
|
+
],
|
|
86
|
+
],
|
|
87
|
+
"indexes": [
|
|
88
|
+
[
|
|
89
|
+
"public", "article_embedding_chunks", "idx_embedding_chunks_hnsw",
|
|
90
|
+
"hnsw", 2203 * MB, 33,
|
|
91
|
+
"CREATE INDEX idx_embedding_chunks_hnsw ON public.article_embedding_chunks "
|
|
92
|
+
"USING hnsw (embedding vector_cosine_ops) WITH (m='16', ef_construction='64')",
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
"public", "article_categories", "article_categories_pkey",
|
|
96
|
+
"btree", 8 * MB, 222_793,
|
|
97
|
+
"CREATE UNIQUE INDEX article_categories_pkey ON public.article_categories USING btree (id)",
|
|
98
|
+
],
|
|
99
|
+
],
|
|
100
|
+
"special_columns": [
|
|
101
|
+
["public", "article_embedding_chunks", "embedding", "vector"],
|
|
102
|
+
["public", "article_categories", "search_vector", "tsvector"],
|
|
103
|
+
],
|
|
104
|
+
"foreign_keys": [
|
|
105
|
+
[
|
|
106
|
+
"article_categories_article_id_fkey",
|
|
107
|
+
"public", "article_categories",
|
|
108
|
+
"public", "article_embedding_chunks",
|
|
109
|
+
]
|
|
110
|
+
],
|
|
111
|
+
"functions": [
|
|
112
|
+
["public", "similarity_search_article_chunks", "enable_seqscan=off", "vector"],
|
|
113
|
+
["public", "refresh_counters", "search_path=public", ""],
|
|
114
|
+
# A platform function that only carries a search_path GUC. Its body uses
|
|
115
|
+
# `<>`, which a bare `<+>` regex would misread as a vector operator.
|
|
116
|
+
["storage", "search", 'search_path=""', ""],
|
|
117
|
+
],
|
|
118
|
+
"database_temp": [["336253", str(2053 * 1024 * MB), "900000", "8100000", "0"]],
|
|
119
|
+
"statements": [
|
|
120
|
+
[
|
|
121
|
+
"221092", "220000000", "996.0", "39.0", "1.00", "0",
|
|
122
|
+
"insert into article_embedding_chunks (article_id, embedding) values ($1, $2)",
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"5000", "1500000", "300.0", "4.0", "12.00", "10",
|
|
126
|
+
"select * from entities where properties->>'name' ilike $1 and similarity(name, $2) > $3",
|
|
127
|
+
],
|
|
128
|
+
],
|
|
129
|
+
"statement_temp": [["91", "5100000", "with recent as (select ...) select * from recent"]],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def fake_runner(has_pgss: bool = True, fixture: dict[str, list[list[object]]] | None = None):
|
|
134
|
+
"""Return a run_psql stand-in: first call is the extension probe."""
|
|
135
|
+
calls: list[str] = []
|
|
136
|
+
payload = FIXTURE if fixture is None else fixture
|
|
137
|
+
|
|
138
|
+
def runner(dsn: str, script: str, timeout_s: int, psql: str = "psql") -> str:
|
|
139
|
+
calls.append(script)
|
|
140
|
+
MODULE.assert_read_only(script)
|
|
141
|
+
if "pg_extension" in script:
|
|
142
|
+
return psql_output({"probe": [["1" if has_pgss else "0"]]})
|
|
143
|
+
sections = dict(payload)
|
|
144
|
+
if not has_pgss:
|
|
145
|
+
sections.pop("statements", None)
|
|
146
|
+
sections.pop("statement_temp", None)
|
|
147
|
+
return psql_output(sections)
|
|
148
|
+
|
|
149
|
+
runner.calls = calls # type: ignore[attr-defined]
|
|
150
|
+
return runner
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def build_fixture_map(has_pgss: bool = True, fixture=None) -> dict:
|
|
154
|
+
runner = fake_runner(has_pgss, fixture)
|
|
155
|
+
with mock.patch.object(MODULE, "run_psql", runner):
|
|
156
|
+
sections, flag = MODULE.collect("postgres://u:p@h/db", 20)
|
|
157
|
+
return MODULE.build_map(sections, MODULE.DEFAULT_DOMAIN_RULES, "postgres://u:***@h/db", flag)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class DsnTests(unittest.TestCase):
|
|
161
|
+
def test_strips_psql_unsupported_uri_params(self) -> None:
|
|
162
|
+
dsn = (
|
|
163
|
+
"postgres://user:pw@host:6543/postgres"
|
|
164
|
+
"?pgbouncer=true&connection_limit=1&pool_timeout=0&sslmode=require"
|
|
165
|
+
)
|
|
166
|
+
cleaned = MODULE.sanitize_dsn(dsn)
|
|
167
|
+
self.assertNotIn("pgbouncer", cleaned)
|
|
168
|
+
self.assertNotIn("connection_limit", cleaned)
|
|
169
|
+
self.assertNotIn("pool_timeout", cleaned)
|
|
170
|
+
self.assertIn("sslmode=require", cleaned)
|
|
171
|
+
self.assertIn("user:pw@host:6543", cleaned)
|
|
172
|
+
|
|
173
|
+
def test_leaves_a_clean_uri_and_a_keyword_dsn_untouched(self) -> None:
|
|
174
|
+
clean = "postgres://user@host/db?sslmode=require"
|
|
175
|
+
self.assertEqual(MODULE.sanitize_dsn(clean), clean)
|
|
176
|
+
kv = "host=localhost dbname=app user=app"
|
|
177
|
+
self.assertEqual(MODULE.sanitize_dsn(kv), kv)
|
|
178
|
+
|
|
179
|
+
def test_redacts_the_password(self) -> None:
|
|
180
|
+
self.assertEqual(
|
|
181
|
+
MODULE.redact_dsn("postgres://user:secret@host:5432/db?sslmode=require"),
|
|
182
|
+
"host:5432/db",
|
|
183
|
+
)
|
|
184
|
+
self.assertNotIn("secret", MODULE.redact_dsn("host=h password=secret"))
|
|
185
|
+
self.assertEqual(MODULE.redact_dsn("host=h port=5432 dbname=app password=secret"), "h:5432/app")
|
|
186
|
+
# No URL shape and no user survive: secret scanners flag both.
|
|
187
|
+
self.assertNotIn("://", MODULE.redact_dsn("postgres://user:secret@host:5432/db"))
|
|
188
|
+
self.assertNotIn("user", MODULE.redact_dsn("postgres://user:secret@host:5432/db"))
|
|
189
|
+
|
|
190
|
+
def test_resolve_dsn_prefers_flag_then_env(self) -> None:
|
|
191
|
+
env = {"DATABASE_URL": "a", "DIRECT_URL": "b"}
|
|
192
|
+
self.assertEqual(MODULE.resolve_dsn("flag", env), "flag")
|
|
193
|
+
self.assertEqual(MODULE.resolve_dsn(None, env), "a")
|
|
194
|
+
self.assertEqual(MODULE.resolve_dsn(None, {"DIRECT_URL": "b"}), "b")
|
|
195
|
+
with self.assertRaises(SystemExit):
|
|
196
|
+
MODULE.resolve_dsn(None, {})
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ScriptSafetyTests(unittest.TestCase):
|
|
200
|
+
def test_script_is_wrapped_in_a_read_only_transaction(self) -> None:
|
|
201
|
+
script = MODULE.build_script(MODULE.SECTION_SQL, 20)
|
|
202
|
+
self.assertTrue(script.startswith("BEGIN READ ONLY;"))
|
|
203
|
+
self.assertIn("SET LOCAL statement_timeout = '20s';", script)
|
|
204
|
+
self.assertTrue(script.strip().endswith("ROLLBACK;"))
|
|
205
|
+
MODULE.assert_read_only(script)
|
|
206
|
+
|
|
207
|
+
def test_statement_timeout_is_configurable(self) -> None:
|
|
208
|
+
self.assertIn(
|
|
209
|
+
"SET LOCAL statement_timeout = '5s';", MODULE.build_script(MODULE.SECTION_SQL, 5)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def test_function_query_excludes_extension_internals(self) -> None:
|
|
213
|
+
# Regression: a live run flagged 107 pgvector-internal C functions
|
|
214
|
+
# (vector_add, halfvec_in, ...) as "vector function without HNSW GUCs".
|
|
215
|
+
sql = dict(MODULE.SECTION_SQL)["functions"]
|
|
216
|
+
self.assertIn("deptype = 'e'", sql)
|
|
217
|
+
self.assertIn("lanname in ('sql', 'plpgsql')", sql)
|
|
218
|
+
self.assertIn("select distinct", sql)
|
|
219
|
+
|
|
220
|
+
def test_function_query_escapes_the_plus_and_skips_platform_schemas(self) -> None:
|
|
221
|
+
sql = dict(MODULE.SECTION_SQL)["functions"]
|
|
222
|
+
# `<+>` unescaped matches the plpgsql not-equals operator `<>`, which
|
|
223
|
+
# flags every platform function in a Supabase database.
|
|
224
|
+
self.assertIn(r"<\+>", sql)
|
|
225
|
+
self.assertNotIn("<+>", sql)
|
|
226
|
+
self.assertIn("'storage'", sql)
|
|
227
|
+
self.assertIn("prorettype", sql)
|
|
228
|
+
self.assertIn("'vector', 'halfvec', 'sparsevec'", sql)
|
|
229
|
+
|
|
230
|
+
def test_ddl_and_dml_are_refused(self) -> None:
|
|
231
|
+
for bad in (
|
|
232
|
+
"BEGIN READ ONLY; drop table article_embeddings; ROLLBACK;",
|
|
233
|
+
"BEGIN READ ONLY; insert into t values (1); ROLLBACK;",
|
|
234
|
+
"BEGIN READ ONLY; update t set a = 1; ROLLBACK;",
|
|
235
|
+
"BEGIN READ ONLY; set work_mem = '1GB'; ROLLBACK;",
|
|
236
|
+
"BEGIN READ ONLY; vacuum full t; ROLLBACK;",
|
|
237
|
+
):
|
|
238
|
+
with self.assertRaises(ValueError):
|
|
239
|
+
MODULE.assert_read_only(bad)
|
|
240
|
+
|
|
241
|
+
def test_run_psql_refuses_before_spawning_a_process(self) -> None:
|
|
242
|
+
with mock.patch.object(MODULE.subprocess, "run") as run:
|
|
243
|
+
with self.assertRaises(ValueError):
|
|
244
|
+
MODULE.run_psql("postgres://h/db", "truncate table t;", 20)
|
|
245
|
+
run.assert_not_called()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class ParsingTests(unittest.TestCase):
|
|
249
|
+
def test_sections_split_on_markers(self) -> None:
|
|
250
|
+
parsed = MODULE.parse_sections(psql_output({"a": [["1", "x"]], "b": []}))
|
|
251
|
+
self.assertEqual(parsed["a"], [["1", "x"]])
|
|
252
|
+
self.assertEqual(parsed["b"], [])
|
|
253
|
+
|
|
254
|
+
def test_shared_buffers_blocks_convert_to_bytes(self) -> None:
|
|
255
|
+
self.assertEqual(
|
|
256
|
+
MODULE._setting_bytes({"setting": "32768", "unit": "8kB"}), 256 * MB
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class LivenessTests(unittest.TestCase):
|
|
261
|
+
def setUp(self) -> None:
|
|
262
|
+
self.data = build_fixture_map()
|
|
263
|
+
self.tables = {t["key"]: t for t in self.data["tables"]}
|
|
264
|
+
|
|
265
|
+
def test_index_scans_beat_a_zero_live_tuple_estimate(self) -> None:
|
|
266
|
+
table = self.tables["article_categories"]
|
|
267
|
+
self.assertEqual(table["n_live_tup"], 0)
|
|
268
|
+
self.assertEqual(table["liveness"], "live")
|
|
269
|
+
self.assertIn("222,793", table["liveness_reason"])
|
|
270
|
+
|
|
271
|
+
def test_written_and_drained_reads_as_written_only(self) -> None:
|
|
272
|
+
table = self.tables["ingest_queue"]
|
|
273
|
+
self.assertEqual(table["liveness"], "written-only")
|
|
274
|
+
self.assertIn("drained queue", table["liveness_reason"])
|
|
275
|
+
|
|
276
|
+
def test_populated_but_untouched_reads_as_idle(self) -> None:
|
|
277
|
+
self.assertEqual(self.tables["reference_lookup"]["liveness"], "idle")
|
|
278
|
+
|
|
279
|
+
def test_no_reads_and_no_writes_reads_as_never_written(self) -> None:
|
|
280
|
+
self.assertEqual(self.tables["legacy_import_staging"]["liveness"], "never-written")
|
|
281
|
+
|
|
282
|
+
def test_estimates_are_labelled_as_estimates(self) -> None:
|
|
283
|
+
self.assertIn("estimates", self.tables["ingest_queue"]["estimates_note"])
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class CollectionTests(unittest.TestCase):
|
|
287
|
+
def test_table_facts_indexes_columns_and_keys_are_attached(self) -> None:
|
|
288
|
+
data = build_fixture_map()
|
|
289
|
+
tables = {t["key"]: t for t in data["tables"]}
|
|
290
|
+
chunks = tables["article_embedding_chunks"]
|
|
291
|
+
self.assertEqual(data["table_count"], 5)
|
|
292
|
+
self.assertEqual(chunks["domain"], "embedding")
|
|
293
|
+
self.assertEqual(chunks["indexes"][0]["access_method"], "hnsw")
|
|
294
|
+
self.assertEqual(chunks["vector_columns"], [{"column": "embedding", "type": "vector"}])
|
|
295
|
+
self.assertEqual(
|
|
296
|
+
tables["article_categories"]["tsvector_columns"],
|
|
297
|
+
[{"column": "search_vector", "type": "tsvector"}],
|
|
298
|
+
)
|
|
299
|
+
self.assertEqual(
|
|
300
|
+
tables["article_categories"]["fk_out"][0]["references"], "article_embedding_chunks"
|
|
301
|
+
)
|
|
302
|
+
self.assertEqual(
|
|
303
|
+
tables["article_embedding_chunks"]["fk_in"][0]["from"], "article_categories"
|
|
304
|
+
)
|
|
305
|
+
self.assertEqual(tables["article_categories"]["domain"], "taxonomy")
|
|
306
|
+
# "staging" must not match the taxonomy rule through the substring "tag".
|
|
307
|
+
self.assertEqual(tables["legacy_import_staging"]["domain"], MODULE.DEFAULT_DOMAIN)
|
|
308
|
+
self.assertEqual(tables["ingest_queue"]["reloptions"], "fillfactor=70")
|
|
309
|
+
self.assertEqual(data["shared_buffers_bytes"], 256 * MB)
|
|
310
|
+
|
|
311
|
+
def test_functions_carry_their_proconfig(self) -> None:
|
|
312
|
+
data = build_fixture_map()
|
|
313
|
+
function = next(
|
|
314
|
+
f for f in data["functions"] if f["name"] == "similarity_search_article_chunks"
|
|
315
|
+
)
|
|
316
|
+
self.assertEqual(function["proconfig"], ["enable_seqscan=off"])
|
|
317
|
+
self.assertTrue(function["uses_vector_ops"])
|
|
318
|
+
|
|
319
|
+
def test_custom_domain_rules_override_the_defaults(self) -> None:
|
|
320
|
+
runner = fake_runner()
|
|
321
|
+
with mock.patch.object(MODULE, "run_psql", runner):
|
|
322
|
+
sections, flag = MODULE.collect("postgres://h/db", 20)
|
|
323
|
+
data = MODULE.build_map(sections, [["ingestion", "queue|staging"]], "dsn", flag)
|
|
324
|
+
tables = {t["key"]: t for t in data["tables"]}
|
|
325
|
+
self.assertEqual(tables["ingest_queue"]["domain"], "ingestion")
|
|
326
|
+
self.assertEqual(tables["article_categories"]["domain"], MODULE.DEFAULT_DOMAIN)
|
|
327
|
+
|
|
328
|
+
def test_missing_pg_stat_statements_degrades_instead_of_failing(self) -> None:
|
|
329
|
+
data = build_fixture_map(has_pgss=False)
|
|
330
|
+
self.assertFalse(data["pg_stat_statements"])
|
|
331
|
+
self.assertIsNone(data["statements"])
|
|
332
|
+
markdown = MODULE.render_markdown(data)
|
|
333
|
+
self.assertIn("`pg_stat_statements` is not installed", markdown)
|
|
334
|
+
|
|
335
|
+
def test_load_domain_rules_rejects_a_malformed_file(self) -> None:
|
|
336
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
337
|
+
path = Path(tmp) / "rules.json"
|
|
338
|
+
path.write_text(json.dumps([["only-one-field"]]), encoding="utf-8")
|
|
339
|
+
with self.assertRaises(SystemExit):
|
|
340
|
+
MODULE.load_domain_rules(str(path))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ShapeTests(unittest.TestCase):
|
|
344
|
+
def setUp(self) -> None:
|
|
345
|
+
self.shapes = build_fixture_map()["shapes"]
|
|
346
|
+
self.by_shape: dict[str, list[dict]] = {}
|
|
347
|
+
for finding in self.shapes:
|
|
348
|
+
self.by_shape.setdefault(finding["shape"], []).append(finding)
|
|
349
|
+
|
|
350
|
+
def test_cold_hnsw_index_matches_index_maintenance_on_writes(self) -> None:
|
|
351
|
+
finding = self.by_shape["index-maintenance-on-writes"][0]
|
|
352
|
+
self.assertIn("idx_embedding_chunks_hnsw", finding["object"])
|
|
353
|
+
self.assertIn("33 lifetime scans", finding["evidence"])
|
|
354
|
+
self.assertIn("402,158", finding["evidence"])
|
|
355
|
+
|
|
356
|
+
def test_oversized_hnsw_index_matches_the_insert_shape(self) -> None:
|
|
357
|
+
finding = self.by_shape["vector-insert-above-cache"][0]
|
|
358
|
+
self.assertIn("2.2 GB", finding["evidence"])
|
|
359
|
+
self.assertIn("256.0 MB", finding["evidence"])
|
|
360
|
+
|
|
361
|
+
def test_vector_function_without_hnsw_gucs_is_flagged_as_drift(self) -> None:
|
|
362
|
+
drift = [
|
|
363
|
+
f for f in self.by_shape["vector-read-above-cache"]
|
|
364
|
+
if "similarity_search_article_chunks" in f["object"]
|
|
365
|
+
]
|
|
366
|
+
self.assertEqual(len(drift), 1)
|
|
367
|
+
self.assertIn("enable_seqscan=off", drift[0]["evidence"])
|
|
368
|
+
self.assertIn("ALTER FUNCTION", drift[0]["action"])
|
|
369
|
+
|
|
370
|
+
def test_a_platform_function_with_no_vector_ops_is_never_flagged(self) -> None:
|
|
371
|
+
flagged = [f["object"] for f in self.shapes]
|
|
372
|
+
self.assertNotIn("storage.search", flagged)
|
|
373
|
+
self.assertNotIn("public.refresh_counters", flagged)
|
|
374
|
+
|
|
375
|
+
def test_jsonb_plus_trigram_statement_matches_the_per_row_shape(self) -> None:
|
|
376
|
+
finding = self.by_shape["per-row-jsonb-trigram"][0]
|
|
377
|
+
self.assertIn("5,000 calls", finding["evidence"])
|
|
378
|
+
|
|
379
|
+
def test_every_finding_names_a_known_shape(self) -> None:
|
|
380
|
+
for finding in self.shapes:
|
|
381
|
+
self.assertIn(finding["shape"], MODULE.SHAPES)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class MarkdownTests(unittest.TestCase):
|
|
385
|
+
def test_every_section_is_rendered(self) -> None:
|
|
386
|
+
markdown = MODULE.render_markdown(build_fixture_map())
|
|
387
|
+
for heading in MODULE.MD_SECTIONS:
|
|
388
|
+
self.assertIn(f"## {heading}", markdown)
|
|
389
|
+
self.assertNotIn(f"## {MODULE.MD_DIFF_SECTION}", markdown)
|
|
390
|
+
|
|
391
|
+
def test_body_carries_the_counter_window_and_the_estimate_warning(self) -> None:
|
|
392
|
+
markdown = MODULE.render_markdown(build_fixture_map())
|
|
393
|
+
self.assertIn("2026-02-01 00:00:00+00", markdown)
|
|
394
|
+
self.assertIn("never reset", markdown)
|
|
395
|
+
self.assertIn("planner estimates", markdown)
|
|
396
|
+
self.assertIn("no DDL or DML was issued", markdown)
|
|
397
|
+
|
|
398
|
+
def test_liveness_section_lists_the_non_live_tables(self) -> None:
|
|
399
|
+
markdown = MODULE.render_markdown(build_fixture_map())
|
|
400
|
+
self.assertIn("`legacy_import_staging`", markdown)
|
|
401
|
+
self.assertIn("`ingest_queue`", markdown)
|
|
402
|
+
|
|
403
|
+
def test_diff_section_appears_only_with_a_previous_map(self) -> None:
|
|
404
|
+
current = build_fixture_map()
|
|
405
|
+
markdown = MODULE.render_markdown(current, MODULE.diff_maps(current, current))
|
|
406
|
+
self.assertIn(f"## {MODULE.MD_DIFF_SECTION}", markdown)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class DiffTests(unittest.TestCase):
|
|
410
|
+
def setUp(self) -> None:
|
|
411
|
+
self.current = build_fixture_map()
|
|
412
|
+
self.previous = copy.deepcopy(self.current)
|
|
413
|
+
self.previous["generated_at"] = "2026-08-01T00:00:00Z"
|
|
414
|
+
|
|
415
|
+
def _diff(self) -> dict:
|
|
416
|
+
return MODULE.diff_maps(self.previous, self.current)
|
|
417
|
+
|
|
418
|
+
def test_identical_maps_report_no_change(self) -> None:
|
|
419
|
+
diff = self._diff()
|
|
420
|
+
self.assertEqual(diff["new_tables"], [])
|
|
421
|
+
self.assertEqual(diff["removed_tables"], [])
|
|
422
|
+
self.assertEqual(diff["size_deltas"], [])
|
|
423
|
+
self.assertEqual(diff["stalled_indexes"], [])
|
|
424
|
+
self.assertEqual(diff["liveness_changes"], [])
|
|
425
|
+
|
|
426
|
+
def test_new_and_removed_tables_are_named(self) -> None:
|
|
427
|
+
self.previous["tables"] = [
|
|
428
|
+
t for t in self.previous["tables"] if t["key"] != "ingest_queue"
|
|
429
|
+
]
|
|
430
|
+
self.previous["tables"].append(dict(self.current["tables"][0], key="dropped_table"))
|
|
431
|
+
diff = self._diff()
|
|
432
|
+
self.assertEqual(diff["new_tables"], ["ingest_queue"])
|
|
433
|
+
self.assertEqual(diff["removed_tables"], ["dropped_table"])
|
|
434
|
+
|
|
435
|
+
def test_size_moves_over_ten_percent_are_reported(self) -> None:
|
|
436
|
+
for table in self.previous["tables"]:
|
|
437
|
+
if table["key"] == "article_embedding_chunks":
|
|
438
|
+
table["total_bytes"] = int(table["total_bytes"] / 2)
|
|
439
|
+
if table["key"] == "article_categories":
|
|
440
|
+
table["total_bytes"] = int(table["total_bytes"] * 1.05)
|
|
441
|
+
diff = self._diff()
|
|
442
|
+
moved = {entry["table"] for entry in diff["size_deltas"]}
|
|
443
|
+
self.assertIn("article_embedding_chunks", moved)
|
|
444
|
+
self.assertNotIn("article_categories", moved)
|
|
445
|
+
self.assertGreater(diff["size_deltas"][0]["pct"], 10)
|
|
446
|
+
|
|
447
|
+
def test_an_index_read_flat_while_inserts_grew_is_flagged(self) -> None:
|
|
448
|
+
for table in self.previous["tables"]:
|
|
449
|
+
if table["key"] == "article_embedding_chunks":
|
|
450
|
+
table["n_tup_ins"] = 300_000
|
|
451
|
+
diff = self._diff()
|
|
452
|
+
stalled = diff["stalled_indexes"]
|
|
453
|
+
self.assertEqual(len(stalled), 1)
|
|
454
|
+
self.assertEqual(stalled[0]["index"], "idx_embedding_chunks_hnsw")
|
|
455
|
+
self.assertEqual(stalled[0]["scan_delta"], 0)
|
|
456
|
+
self.assertEqual(stalled[0]["insert_delta"], 102_158)
|
|
457
|
+
|
|
458
|
+
def test_liveness_flips_are_reported(self) -> None:
|
|
459
|
+
for table in self.previous["tables"]:
|
|
460
|
+
if table["key"] == "ingest_queue":
|
|
461
|
+
table["liveness"] = "live"
|
|
462
|
+
diff = self._diff()
|
|
463
|
+
self.assertEqual(diff["liveness_changes"][0]["table"], "ingest_queue")
|
|
464
|
+
self.assertEqual(diff["liveness_changes"][0]["after"], "written-only")
|
|
465
|
+
|
|
466
|
+
def test_a_counter_reset_between_maps_invalidates_the_deltas(self) -> None:
|
|
467
|
+
self.previous["window"]["stats_reset"] = "2026-07-01 00:00:00+00"
|
|
468
|
+
diff = self._diff()
|
|
469
|
+
self.assertTrue(diff["counters_reset_between_maps"])
|
|
470
|
+
rendered = "\n".join(MODULE._render_diff(diff)).lower()
|
|
471
|
+
self.assertIn("counters reset between the two maps", rendered)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class CliTests(unittest.TestCase):
|
|
475
|
+
def test_main_writes_both_artifacts_and_the_diff(self) -> None:
|
|
476
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
477
|
+
prev_path = Path(tmp) / "prev.json"
|
|
478
|
+
previous = build_fixture_map()
|
|
479
|
+
for table in previous["tables"]:
|
|
480
|
+
if table["key"] == "article_embedding_chunks":
|
|
481
|
+
table["n_tup_ins"] = 300_000
|
|
482
|
+
prev_path.write_text(json.dumps(previous), encoding="utf-8")
|
|
483
|
+
|
|
484
|
+
out_json = Path(tmp) / "audits" / "map.json"
|
|
485
|
+
out_md = Path(tmp) / "audits" / "map.md"
|
|
486
|
+
runner = fake_runner()
|
|
487
|
+
with mock.patch.object(MODULE, "run_psql", runner):
|
|
488
|
+
code = MODULE.main(
|
|
489
|
+
[
|
|
490
|
+
"--dsn",
|
|
491
|
+
"postgres://u:secret@h/db?pgbouncer=true",
|
|
492
|
+
"--out-json",
|
|
493
|
+
str(out_json),
|
|
494
|
+
"--out-md",
|
|
495
|
+
str(out_md),
|
|
496
|
+
"--prev",
|
|
497
|
+
str(prev_path),
|
|
498
|
+
"--statement-timeout",
|
|
499
|
+
"10",
|
|
500
|
+
]
|
|
501
|
+
)
|
|
502
|
+
self.assertEqual(code, 0)
|
|
503
|
+
data = json.loads(out_json.read_text(encoding="utf-8"))
|
|
504
|
+
markdown = out_md.read_text(encoding="utf-8")
|
|
505
|
+
|
|
506
|
+
self.assertNotIn("secret", data["dsn"])
|
|
507
|
+
self.assertNotIn("secret", markdown)
|
|
508
|
+
self.assertEqual(data["diff"]["stalled_indexes"][0]["index"], "idx_embedding_chunks_hnsw")
|
|
509
|
+
self.assertIn(f"## {MODULE.MD_DIFF_SECTION}", markdown)
|
|
510
|
+
self.assertIn("SET LOCAL statement_timeout = '10s';", runner.calls[-1])
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
if __name__ == "__main__": # pragma: no cover
|
|
514
|
+
unittest.main()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debug-loop
|
|
3
|
-
description: Use when a fix didn't hold, `/build-loop:debug` is invoked, the user
|
|
3
|
+
description: Iteratively investigate and fix a live, currently-broken bug. Use when a fix didn't hold, `/build-loop:debug` is invoked, or the user wants it debugged right now. Not for checking whether this bug happened before (use `debugging-memory` first) or a post-fix blameless analysis (use `root-cause-analysis`).
|
|
4
4
|
version: 1.1.0
|
|
5
|
-
user-invocable:
|
|
5
|
+
user-invocable: false
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
@@ -29,7 +29,7 @@ Before entering the loop, assess whether it's warranted. The trigger is the **ve
|
|
|
29
29
|
|
|
30
30
|
**Goal**: Understand what's actually failing and why, not just what it looks like.
|
|
31
31
|
|
|
32
|
-
1. **Search debugging memory** — invoke `build-loop:debugging-memory` with the symptom.
|
|
32
|
+
1. **Search debugging memory** — invoke `build-loop:debugging-memory` with the symptom. Its native search reads the project's structured `.claude/memory/` store.
|
|
33
33
|
2. **Reproduce the issue** — identify exact steps, commands, or conditions that trigger the bug
|
|
34
34
|
3. **Deploy root-cause-investigator agent** — pass the symptom and reproduction steps for causal tree analysis. The agent explores multiple branches (not a single chain), prioritizes by evidence strength, and prunes with evidence
|
|
35
35
|
4. **Research gate** — if the investigator flags unfamiliar error codes, library behavior, or version-specific issues:
|
|
@@ -37,9 +37,36 @@ Before entering the loop, assess whether it's warranted. The trigger is the **ve
|
|
|
37
37
|
- Document what was searched and what was found
|
|
38
38
|
- If search is unavailable, document what SHOULD be searched
|
|
39
39
|
5. **Assess completeness** — does the investigation explain ALL reported symptoms? Check for multi-causal bugs (2+ independent root causes)
|
|
40
|
+
6. **Qualify every observation before it becomes evidence** — the gate below. Run it on each number and each instrument reading, INCLUDING the ones that agree with you
|
|
40
41
|
|
|
41
42
|
**Output**: Causal tree (with confirmed and pruned branches), reproduction steps, evidence gathered, research performed
|
|
42
43
|
|
|
44
|
+
### Evidence qualification gate (run per observation, before it enters the report)
|
|
45
|
+
|
|
46
|
+
Three questions. They take seconds and they catch the failure class that survives every
|
|
47
|
+
other check — a *correct* instrument, read correctly, generalized one step too far.
|
|
48
|
+
|
|
49
|
+
| # | Question | Catches |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| 1 | **Unit** — one unit of this number is one *what*? | `zcard(queue:prioritized)=20,823` quoted as "20,823 articles". Job ids were `<entity>-<retry-bucket>`, so each retry minted a new id for the same entity. Walk it: 20,823 jobs ÷ **5,001 distinct ids** = 4.17x duplication; of those 5,001, **2,178 were already deleted** from the DB, leaving real work ≈2,704. **Overstated 7x** (20,823 ÷ 2,704). The two ratios measure different things — keep the distinct count between them or they read as contradictory |
|
|
52
|
+
| 2 | **Instance** — is the thing I checked the only one of its kind? Enumerate before generalizing. | "Redis is healthy" from one connection, when the code built **two** — the healthy singleton, and a separate BullMQ-owned connection that the hanging call path actually used |
|
|
53
|
+
| 3 | **Second source** — what else can answer this same question? Run it and diff. | Queue said 20,823, database said 3,531. Both were queried; neither was reconciled until a human asked |
|
|
54
|
+
|
|
55
|
+
**Disagreement between two sources is not noise to resolve — it IS the finding.** Write the
|
|
56
|
+
reconciliation as a script so it stays runnable and re-runnable, not as a one-off query.
|
|
57
|
+
|
|
58
|
+
**The gate has a known hole: it fires on conclusions, not on narration.** Observations that
|
|
59
|
+
merely *support* the current hypothesis slip through unexamined, because they never feel like
|
|
60
|
+
claims. Apply the gate hardest to the numbers that agree with you — a measurement that
|
|
61
|
+
confirms the story is exactly the one nobody re-derives. Any number crossing from an
|
|
62
|
+
instrument into a sentence is a claim, whatever its grammatical role.
|
|
63
|
+
|
|
64
|
+
Corollary for tools: a listing/introspection API returning empty is not proof of absence
|
|
65
|
+
(`Queue.getWorkers()` returned `[]` for a worker that was provably alive and heartbeating).
|
|
66
|
+
Confirm absence against a second, independent observation channel — see
|
|
67
|
+
`skills/build-loop/references/verify-dispatch.md` and the `verify_the_instrument_before_the_finding`
|
|
68
|
+
lesson.
|
|
69
|
+
|
|
43
70
|
### Root-Cause Frameworks
|
|
44
71
|
|
|
45
72
|
Use the lightest framework that fits the failure. Stack frameworks only when the current one stalls.
|
|
@@ -170,8 +197,7 @@ Every item in the report gets one marker:
|
|
|
170
197
|
|
|
171
198
|
> **Durable post-failure RCA:** for the blameless durable-lever pass (creation+escape paths, action-strength hierarchy, lever+actuator, regression artifact, spread check), delegate to the shared `references/root-cause-analysis/` suite. This skill/agent finds and fixes the live issue; that suite is the post-failure prevention layer.
|
|
172
199
|
|
|
173
|
-
- **Store the incident**
|
|
174
|
-
- **Record the outcome** through standalone Coding Debugger only if that optional plugin supplied the prior incident
|
|
200
|
+
- **Store the incident** through `build-loop:debugging-memory` `{op:"store"}` so it lands in the same structured store used by search
|
|
175
201
|
- **Write state** to `.build-loop/debug-loop/scorecard.md`
|
|
176
202
|
|
|
177
203
|
## Iteration Rules
|
|
@@ -220,7 +246,7 @@ If the bundled assessor coverage isn't enough (e.g., the failure crosses a domai
|
|
|
220
246
|
Skill("build-loop:debugging-memory") with input { op: "assess", symptom, scope: "global", calledBy: "debug-loop", reason: "stuck-iteration" }
|
|
221
247
|
```
|
|
222
248
|
|
|
223
|
-
The native skill includes domain-specific assessors (api / database / frontend / performance)
|
|
249
|
+
The native skill includes domain-specific assessors (api / database / frontend / performance) and uses Build Loop's project-local structured debugger memory.
|
|
224
250
|
|
|
225
251
|
### State Tracking
|
|
226
252
|
|
|
@@ -278,3 +304,7 @@ MEMORY SEARCH → INVESTIGATE → HYPOTHESIZE → FIX → VERIFY → SCORE
|
|
|
278
304
|
| Declaring victory without evidence | Every claim needs a ✅/⚠️/❓ marker |
|
|
279
305
|
| Skipping research when stuck | If you don't know why something behaves this way, search for it |
|
|
280
306
|
| Hiding uncertainty | ⚠️ and ❓ are not failures — they're honest. Hiding them is the failure |
|
|
307
|
+
| Quoting a counter without its unit | Name the unit before the magnitude. "20,823 *what*?" A queue depth is jobs; a composite job id defeats dedup and inflates it |
|
|
308
|
+
| Scrutinizing only the evidence that challenges you | Run the qualification gate on confirming observations too — those are the ones that ship wrong |
|
|
309
|
+
| Treating an empty listing as proof of absence | Absence needs a second observation channel; introspection APIs return `[]` for live things |
|
|
310
|
+
| Two sources, pick the convenient one | Diff them. The gap is the finding, and the diff belongs in a script |
|