Route capability entries as evidence pool

This commit is contained in:
732642856
2026-06-29 11:55:16 +08:00
parent 178251e7e6
commit 1c08af34fa
9 changed files with 1337 additions and 94 deletions
+10 -4
View File
@@ -4,7 +4,7 @@
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue)](https://www.python.org/)
[![Techniques](https://img.shields.io/badge/techniques-89-blueviolet)](references/technique_registry.json)
[![Capabilities](https://img.shields.io/badge/capabilities-89-blueviolet)](references/technique_registry.json)
[![Covered](https://img.shields.io/badge/covered-79-green)](references/technique_registry.json)
[![Complete](https://img.shields.io/badge/complete-10-brightgreen)](references/technique_registry.json)
[![Partial](https://img.shields.io/badge/partial-0-lightgrey)](references/technique_registry.json)
@@ -31,7 +31,7 @@
This is a **Vedic (Jyotish) astrology analysis system** designed for deep, auditable full-chart readings. It is NOT a simple ephemeris calculator — it is a multi-stage interpretive pipeline that:
1. **Computes** divisional charts (D1/D9/D10/...) via Swiss Ephemeris
2. **Runs** 89 registered techniques (Dashas, Yogas, Shadbala, Ashtakavarga, Transits...)
2. **Routes** 89 capability entries as a backend evidence pool (Dashas, Yogas, Shadbala, Ashtakavarga, Transits...)
3. **Routes** the analysis through strict workflow paths depending on question type (career / relationship / wealth / timing)
4. **Audits** every technique used — declaring what was called, what is complete/covered, and which limitations affect confidence
5. **Degrades gracefully** — limitations are labeled, not silently over-promising
@@ -45,7 +45,7 @@ This is a **Vedic (Jyotish) astrology analysis system** designed for deep, audit
| Technique Audit Table (confidence labeling) | ✅ | ❌ | ❌ | ❌ |
| Capability degradation (limits are explicit) | ✅ | ❌ | ❌ | ❌ |
| MEVG external verification gates | ✅ | ❌ | ❌ | ❌ |
| 89 techniques registered | ✅ | ✅ (50+) | ✅ (200+) | ✅ |
| 89 capability entries routed as a backend evidence pool | ✅ | ✅ (50+) | ✅ (200+) | ✅ |
| Traditional algorithm benchmarked | ✅ mixed depth | ✅ | ✅ | ✅ |
| Docker / MCP Server | ✅ | ❌ | ✅ | ❌ |
| English docs / PyPI package | ✅ in progress | ✅ | ✅ | ✅ |
@@ -454,7 +454,13 @@ The AI does NOT require the user to name techniques (e.g., "Chara Dasha"). It au
## Technique Coverage
Current registry count: **89 techniques** (79 covered, 10 complete, 0 partial, 0 missing).
Current registry count: **89 capability entries** (79 covered, 10 complete, 0 partial, 0 missing).
These entries are a **backend evidence pool**, not a flat list of 89 user-facing
prediction sources. Ordinary users see topic-level conclusions and evidence
summaries. The question-domain router selects a small primary chain, then uses
supporting indicators only to raise/lower confidence. Audit-only and alias
entries cannot affect astrological conclusions.
The table below lists representative high-value entries. Treat
`references/technique_registry.json` as the source of truth for the full
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Summarize the technique registry as a backend capability evidence pool."""
from __future__ import annotations
import argparse
import json
from collections import Counter
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_REGISTRY = ROOT / "references" / "technique_registry.json"
def load_registry(path: Path = DEFAULT_REGISTRY) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def build_capability_evidence_pool_summary(registry: dict[str, Any] | None = None) -> dict[str, Any]:
registry = registry if registry is not None else load_registry()
techniques = registry.get("techniques") if isinstance(registry, dict) else {}
if not isinstance(techniques, dict):
techniques = {}
entry_type_counts = Counter()
evidence_role_counts = Counter()
visibility_counts = Counter()
prediction_counts = Counter()
primary_entries: list[str] = []
audit_only_entries: list[str] = []
alias_entries: list[str] = []
for tech_id, tech in techniques.items():
if not isinstance(tech, dict):
continue
entry_type = tech.get("entry_type") or "supporting_indicator"
evidence_role = tech.get("evidence_role") or "secondary"
visibility = tech.get("user_visibility") or "expert_audit"
verification = tech.get("verification_level") if isinstance(tech.get("verification_level"), dict) else {}
prediction = verification.get("prediction") or "not_claimed"
entry_type_counts[entry_type] += 1
evidence_role_counts[evidence_role] += 1
visibility_counts[visibility] += 1
prediction_counts[prediction] += 1
if evidence_role == "primary":
primary_entries.append(tech_id)
elif evidence_role == "audit_only":
audit_only_entries.append(tech_id)
elif evidence_role == "alias":
alias_entries.append(tech_id)
return {
"scope": "backend_capability_evidence_pool",
"total_entries": len(techniques),
"public_label": registry.get("public_label", f"{len(techniques)} capability entries"),
"ordinary_user_policy": registry.get(
"ordinary_user_policy",
"Users see topic-level conclusions; capability entries are routed behind the scenes.",
),
"entry_type_counts": dict(sorted(entry_type_counts.items())),
"evidence_role_counts": dict(sorted(evidence_role_counts.items())),
"user_visibility_counts": dict(sorted(visibility_counts.items())),
"prediction_verification_counts": dict(sorted(prediction_counts.items())),
"primary_entries": sorted(primary_entries),
"audit_only_entries": sorted(audit_only_entries),
"alias_entries": sorted(alias_entries),
"conclusion_policy": {
"primary_chain_required": True,
"all_89_entries_must_not_be_flattened_into_conclusions": True,
"supporting_entries_can_only_raise_or_lower_confidence": True,
"audit_only_entries_cannot_affect_astrological_conclusions": True,
"conflicts_must_downgrade_confidence": True,
},
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--registry", default=str(DEFAULT_REGISTRY))
parser.add_argument("--format", choices=["json"], default="json")
args = parser.parse_args(argv)
summary = build_capability_evidence_pool_summary(load_registry(Path(args.registry)))
print(json.dumps(summary, ensure_ascii=False, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""Evidence-backed topic discovery for ordinary users.
This layer does not calculate astrology. It ranks existing full-reading
evidence into a small set of next topics a user can tap or ask about.
"""
from __future__ import annotations
from typing import Any
def _as_dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _as_list(value: Any) -> list[Any]:
return value if isinstance(value, list) else []
def _current_dasha(modules: dict[str, Any]) -> tuple[str | None, str | None, str | None, str | None]:
dasha = _as_dict(modules.get("dasha"))
current = _as_dict(dasha.get("current_dasha"))
antar = _as_dict(current.get("antardasha"))
return current.get("lord"), antar.get("lord"), current.get("start"), current.get("end")
def _convergence_for(modules: dict[str, Any], *tokens: str) -> dict[str, Any]:
convergence = _as_dict(modules.get("dasa_convergence"))
activations = _as_dict(convergence.get("domain_activations"))
for domain, row in activations.items():
if any(token in str(domain).lower() for token in tokens):
found = dict(_as_dict(row))
found.setdefault("domain", domain)
return found
for row in _as_list(convergence.get("top_convergent_domains")):
if isinstance(row, dict) and any(token in str(row.get("domain", "")).lower() for token in tokens):
return row
if isinstance(row, (list, tuple)) and row and any(token in str(row[0]).lower() for token in tokens):
return {"domain": row[0], "convergence_level": row[1] if len(row) > 1 else None}
return {}
def _vedastro_snapshot(modules: dict[str, Any], domain: str) -> dict[str, Any]:
overview = _as_dict(modules.get("vedastro_range_scan_result"))
metadata = _as_dict(overview.get("source_metadata"))
counts = _as_dict(metadata.get("domain_event_counts"))
statuses = _as_dict(metadata.get("domain_statuses"))
top = _as_dict(overview.get("top_events_by_domain")).get(domain)
status = statuses.get(domain) or overview.get("status")
event_count = int(counts.get(domain) or 0)
if status == "ok" or event_count:
return {
"status": "used",
"domain": domain,
"event_count": event_count,
"top_event": top if isinstance(top, dict) else None,
"source": "modules.vedastro_range_scan_result",
}
return {
"status": "blocked" if overview else "not_available",
"domain": domain,
"event_count": event_count,
"top_event": None,
"source": "modules.vedastro_range_scan_result",
}
def _evidence_line(label: str, value: Any) -> dict[str, str]:
return {"label": label, "value": str(value)}
def _topic(
*,
topic_id: str,
title: str,
reality_value: str,
why: str,
evidence: list[dict[str, str]],
confidence: str,
vedastro: dict[str, Any],
questions: list[str],
answer_mode: str = "tap_or_ask",
priority: int = 50,
) -> dict[str, Any]:
return {
"id": topic_id,
"title": title,
"reality_value": reality_value,
"why_worth_exploring": why,
"evidence": evidence,
"confidence": confidence,
"vedastro": vedastro,
"suggested_questions": questions,
"answer_mode": answer_mode,
"priority": priority,
}
def build_guided_topics(report: dict[str, Any]) -> list[dict[str, Any]]:
modules = _as_dict(report.get("modules"))
chart = _as_dict(report.get("chart") or modules.get("chart"))
planets = _as_dict(chart.get("planets"))
md, ad, md_start, md_end = _current_dasha(modules)
md_label = f"{md or '-'} / {ad or '-'}"
fbm = _as_dict(_as_dict(report.get("ai_prompt_pack")).get("evidence_snapshot")).get("functional_benefic_malefic")
fbm = _as_dict(fbm) or _as_dict(modules.get("functional_benefic_malefic"))
career_conv = _convergence_for(modules, "career", "status", "profession", "work")
marriage_conv = _convergence_for(modules, "marriage", "partnership", "relationship")
wealth_conv = _convergence_for(modules, "wealth", "finance", "income", "gain")
relationship = _as_dict(modules.get("relationship_strict_evidence"))
rel_judgement = _as_dict(relationship.get("event_judgement"))
rel_present = _as_dict(relationship.get("present_evidence"))
d9 = _as_dict(rel_present.get("d9_navamsa"))
ul = _as_dict(rel_present.get("upapada_lagna"))
dk = _as_dict(rel_present.get("darakaraka"))
ketu_house = _as_dict(planets.get("Ketu")).get("house")
topics = [
_topic(
topic_id="relationship_partnership",
title="婚恋与长期合作为什么是当前强主题",
reality_value="帮助用户判断关系、合作、相亲、公开关系或长期承诺是否值得深入推进。",
why="婚恋/合作不是靠用户主动问才触发;当前证据里第7宫、D9、UL、DK 与多系统时间层已经可读。",
evidence=[
_evidence_line("Vimshottari", md_label),
_evidence_line("Dasa 收敛", marriage_conv.get("convergence_level") or "not_found"),
_evidence_line("D9", f"Asc={_as_dict(d9.get('Ascendant')).get('sign', '-')}; 7th={_as_dict(d9.get('_d9_analysis')).get('navamsa_7th_sign', '-')}"),
_evidence_line("UL", f"{ul.get('sign', '-')} H{ul.get('house', '-')}"),
_evidence_line("DK", f"{dk.get('dk_planet', '-')} H{dk.get('dk_house', '-')}"),
_evidence_line("Strict verdict", rel_judgement.get("verdict") or "not_available"),
],
confidence="medium" if marriage_conv or relationship else "low",
vedastro=_vedastro_snapshot(modules, "marriage"),
questions=[
"我现在适合认真发展关系,还是更适合筛选和观察?",
"我的伴侣画像、认识场景和相处风险是什么?",
"未来哪些时间窗口适合推进关系公开或承诺?",
],
priority=90 if marriage_conv else 65,
),
_topic(
topic_id="career_direction",
title="事业定位是否正在重构",
reality_value="帮助用户判断是继续深耕、换方向、做产品化,还是先修系统和长期资产。",
why="事业主题需要把10宫、A10/D10、多系统 Dasha 与 VedAstro 事业雷达放在一起看。",
evidence=[
_evidence_line("Vimshottari", md_label),
_evidence_line("10宫触发", f"Ketu house={ketu_house}" if ketu_house else "check D10/A10"),
_evidence_line("Dasa 收敛", career_conv.get("convergence_level") or "not_found"),
_evidence_line("Functional layer", f"benefics={fbm.get('functional_benefics', [])}; malefics={fbm.get('functional_malefics', [])}"),
],
confidence="medium" if career_conv or ketu_house == 10 else "low",
vedastro=_vedastro_snapshot(modules, "career"),
questions=[
"我现在适合换方向还是继续深耕?",
"2026 年事业吉利在哪里,不利在哪里?",
"哪些月份适合推进项目、发布产品或谈合作?",
],
priority=82 if career_conv or ketu_house == 10 else 60,
),
_topic(
topic_id="birth_time_rectification",
title="出生时间是否需要微调",
reality_value="帮助用户把婚恋、事业、财富应期从泛泛判断推进到可回验时间窗口。",
why="D9、D10、UL、A10 对出生时间敏感;如果用户想问具体月份/日期,先校正时间更有价值。",
evidence=[
_evidence_line("birth time", _as_dict(report.get("birth_info")).get("time", "-")),
_evidence_line("sensitive layers", "D9 / D10 / UL / A10"),
_evidence_line("current timing", f"{md_label}; {md_start or '-'} to {md_end or '-'}"),
],
confidence="medium",
vedastro=_vedastro_snapshot(modules, "marriage"),
questions=[
"我可以用过去事件校正出生时间吗?",
"哪些人生事件最适合用来校正出生时间?",
"我只知道一个时间区间,系统应该先问我哪些 yes/no 问题?",
],
answer_mode="yes_no_or_free_text",
priority=80,
),
_topic(
topic_id="wealth_risk",
title="财富、借贷和交易风险怎样用数据拆开",
reality_value="帮助用户把收入、现金流、借贷、买卖和投资风险分开判断,而不是只说财运好坏。",
why="财富主题必须同时看2宫、11宫、D2/D11、Dasha 与 VedAstro wealth 标签。",
evidence=[
_evidence_line("Vimshottari", md_label),
_evidence_line("Dasa 收敛", wealth_conv.get("convergence_level") or "not_found"),
_evidence_line("required vargas", "D2 / D11"),
],
confidence="medium" if wealth_conv else "low",
vedastro=_vedastro_snapshot(modules, "wealth"),
questions=[
"2026 年哪些钱可以赚,哪些钱要避险?",
"我适合靠项目、投资、合作还是长期积累赚钱?",
"哪些时间窗口不适合借贷、买卖或大额投入?",
],
priority=70 if wealth_conv else 50,
),
]
topics.sort(key=lambda item: (-int(item.get("priority", 0)), item["id"]))
return topics[:4]
+37
View File
@@ -81,6 +81,26 @@ def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
return chart_result
def _attach_guided_topics(chart_result):
if not isinstance(chart_result, dict):
return chart_result
modules = chart_result.setdefault('modules', {})
if not isinstance(modules, dict):
modules = {}
chart_result['modules'] = modules
if isinstance(modules.get('guided_topics'), list):
return chart_result
try:
builder = _load_local_module('guided_topic_discovery').build_guided_topics
modules['guided_topics'] = builder(chart_result)
except Exception as exc:
modules['guided_topics'] = []
warnings = chart_result.setdefault('warnings', [])
if isinstance(warnings, list):
warnings.append(f'guided-topics: {exc}')
return chart_result
def _build_vedastro_overview_payload_from_chart(chart):
modules = chart.get('modules') if isinstance(chart, dict) else {}
overview = modules.get('vedastro_range_scan_result') if isinstance(modules, dict) else {}
@@ -2184,6 +2204,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'today': body.get('today') or body.get('current_date'),
'transit_date': body.get('transit_date'),
})
_attach_guided_topics(result)
result['ai_prompt_pack'] = self._build_chart_prompt_pack(result)
return result
except ImportError:
@@ -2267,6 +2288,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'ayanamsa': 'lahiri',
'node_mode': 'mean',
})
_attach_guided_topics(result)
result['ai_prompt_pack'] = self._build_chart_prompt_pack(result)
return result
@@ -2278,6 +2300,19 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
shadbala = chart.get('shadbala') or {}
functional_layer = self._functional_benefic_malefic_snapshot(planets, ascendant)
vedastro_overview = _build_vedastro_overview_payload_from_chart(chart)
_attach_guided_topics(chart)
modules = chart.get('modules') if isinstance(chart.get('modules'), dict) else {}
guided_topics = modules.get('guided_topics') if isinstance(modules.get('guided_topics'), list) else []
try:
capability_evidence_pool = _load_local_module('capability_evidence_pool').build_capability_evidence_pool_summary()
except Exception:
capability_evidence_pool = {
'scope': 'backend_capability_evidence_pool',
'total_entries': 0,
'conclusion_policy': {
'all_89_entries_must_not_be_flattened_into_conclusions': True,
},
}
top_strength = sorted(
[
{
@@ -2348,6 +2383,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
},
'functional_benefic_malefic': functional_layer,
'vedastro_overview': vedastro_overview,
'guided_topics': guided_topics,
'capability_evidence_pool': capability_evidence_pool,
'quality_boundary': {
'external_oracle_status': 'D1/D9/VedAstro longitude boundary covered; Dasha/Shadbala external absolute calibration still requires multi-source oracle expansion.',
},
+13
View File
@@ -48,6 +48,8 @@ from datetime import datetime, timedelta
from typing import Dict, List
from tabulate import tabulate
from life_stage_hook import generate_life_stage_hooks
from capability_evidence_pool import build_capability_evidence_pool_summary
from guided_topic_discovery import build_guided_topics
from ayanamsa_utils import (
AYANAMSA_DISPLAY_NAMES,
@@ -1165,6 +1167,8 @@ def _build_ai_prompt_pack(report):
relationship_narrative = _build_relationship_narrative_payload(modules.get('relationship_strict_evidence'))
vimsopaka_semantic_summary = _build_vimsopaka_semantic_summary(modules.get('vimsopaka'))
vedastro_overview = _build_vedastro_overview_payload(modules)
guided_topics = modules.get('guided_topics') if isinstance(modules.get('guided_topics'), list) else build_guided_topics(report)
capability_evidence_pool = build_capability_evidence_pool_summary()
shadbala_ranking = []
for planet_name, pdata in sorted(
@@ -1236,6 +1240,8 @@ def _build_ai_prompt_pack(report):
'oracle_progress': oracle_progress,
'functional_benefic_malefic': functional_layer,
'vedastro_overview': vedastro_overview,
'guided_topics': guided_topics,
'capability_evidence_pool': capability_evidence_pool,
'technique_audit_table': technique_audit_table,
'relationship_narrative': relationship_narrative,
'vimsopaka_semantic_summary': vimsopaka_semantic_summary,
@@ -1250,6 +1256,7 @@ def _build_ai_prompt_pack(report):
"输出结构建议:参数声明、核心星盘、关系/事业/财富/健康分主题、当前时机、证据表、风险边界、可行动建议。",
"若引用经典法则,请优先检索 retrieval_plan.local_reference_docs;需要外部断语时再做 web/source verification。",
"若 evidence_snapshot.vedastro_overview.status 为 ok,请把它作为用户可见外部概览证据明确写出,但不要把 overview-only 结果误当作长周期精扫结论。",
"若 evidence_snapshot.capability_evidence_pool 存在,请把 89 项视为后台备选证据池;不要把所有能力条目平铺成结论,也不要让 audit_only/alias 条目影响占星判断。",
]
return {
@@ -5130,6 +5137,12 @@ def cmd_full_reading(args):
except Exception as e:
report['warnings'].append(f"vedastro-main-entry-overview: {e}")
try:
report['modules']['guided_topics'] = build_guided_topics(report)
except Exception as e:
report['warnings'].append(f"guided-topics: {e}")
report['modules']['guided_topics'] = []
report['ai_prompt_pack'] = _build_ai_prompt_pack(report)
return report
+72
View File
@@ -0,0 +1,72 @@
from __future__ import annotations
import json
from pathlib import Path
from scripts.capability_evidence_pool import build_capability_evidence_pool_summary
ROOT = Path(__file__).resolve().parents[1]
REGISTRY = ROOT / "references" / "technique_registry.json"
README = ROOT / "README.md"
def test_registry_is_backend_evidence_pool_not_flat_user_skill_list() -> None:
registry = json.loads(REGISTRY.read_text(encoding="utf-8"))
techniques = registry["techniques"]
assert registry["registry_role"] == "backend_capability_evidence_pool"
assert registry["public_label"] == "89 capability entries"
assert "question-domain router" in registry["ordinary_user_policy"]
allowed_entry_types = {
"core_technique",
"supporting_indicator",
"composite_adjudicator",
"workflow_or_engineering",
"alias_entry",
}
allowed_roles = {"primary", "secondary", "context", "audit_only", "alias"}
allowed_visibility = {"ordinary_topic_router", "expert_audit", "hidden"}
allowed_prediction = {
"case_validated_partial",
"support_only",
"not_claimed",
"not_applicable",
}
for tech_id, tech in techniques.items():
assert tech["entry_type"] in allowed_entry_types, tech_id
assert tech["evidence_role"] in allowed_roles, tech_id
assert tech["user_visibility"] in allowed_visibility, tech_id
assert tech["verification_level"]["calculation"] in {"verified", "partial", "not_applicable"}, tech_id
assert tech["verification_level"]["rule"] in {"verified", "partial", "not_applicable"}, tech_id
assert tech["verification_level"]["prediction"] in allowed_prediction, tech_id
assert tech["conclusion_policy"], tech_id
assert techniques["case_validator"]["evidence_role"] == "audit_only"
assert techniques["thematic_report_orchestrator"]["entry_type"] == "workflow_or_engineering"
assert techniques["neechabhanga"]["evidence_role"] == "alias"
assert techniques["special_lagnas"]["evidence_role"] == "alias"
def test_evidence_pool_summary_routes_few_primary_items_and_many_support_items() -> None:
summary = build_capability_evidence_pool_summary()
assert summary["scope"] == "backend_capability_evidence_pool"
assert summary["total_entries"] == 89
assert summary["ordinary_user_policy"].startswith("Users see topic-level")
assert summary["evidence_role_counts"]["primary"] >= 8
assert summary["evidence_role_counts"]["secondary"] > summary["evidence_role_counts"]["primary"]
assert summary["evidence_role_counts"]["audit_only"] >= 3
assert summary["prediction_verification_counts"]["not_claimed"] > 0
assert summary["conclusion_policy"]["primary_chain_required"] is True
assert summary["conclusion_policy"]["all_89_entries_must_not_be_flattened_into_conclusions"] is True
def test_readme_uses_capability_entries_language_instead_of_89_techniques_claim() -> None:
readme = README.read_text(encoding="utf-8")
assert "89 capability entries" in readme
assert "89 techniques" not in readme
assert "backend evidence pool" in readme
+5
View File
@@ -334,6 +334,11 @@ def test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack() -> None:
assert vedastro_rows[0]["status"] in {"used", "blocked"}
assert "overview only" in vedastro_rows[0]["note"]
assert "domain_statuses" in vedastro_rows[0]["note"]
capability_pool = prompt_pack["evidence_snapshot"]["capability_evidence_pool"]
assert capability_pool["scope"] == "backend_capability_evidence_pool"
assert capability_pool["total_entries"] == 89
assert capability_pool["conclusion_policy"]["all_89_entries_must_not_be_flattened_into_conclusions"] is True
assert "后台备选证据池" in prompt_pack["prompt_zh"]
def test_full_reading_generates_guided_topics_from_real_evidence() -> None:
+1 -1
View File
@@ -27,7 +27,7 @@ def test_readme_badges_match_technique_registry_counts() -> None:
counts = Counter(item["status"] for item in techniques)
total = len(registry["techniques"])
assert _readme_badge_value("Techniques") == total
assert _readme_badge_value("Capabilities") == total
assert _readme_badge_value("Covered") == counts["covered"]
assert _readme_badge_value("Complete") == counts["complete"]
assert _readme_badge_value("Partial") == counts["partial"]