"""Per-tool manifest registry. Single source of truth for what tools exist, their display strings, the left-nav section they belong to, and the tier (which controls whether a tool ships in a given build SKU). The home-page sidebar consumes this list; future per-tool packaging will filter it via the ``tier`` field. Adding a tool: append one ``Tool`` entry. Page filenames must match the ``page_slug`` so Streamlit's automatic page discovery picks them up. Selling subsets: builds can filter ``TOOLS`` by tier or tool_id at import time — no other code changes required, since pages key off ``tool_id`` for findings badges and the home grid renders whatever's in the filtered list. """ from __future__ import annotations from dataclasses import dataclass from typing import Literal Tier = Literal["core", "pro", "enterprise"] Status = Literal["Ready", "Coming Soon"] # Sidebar grouping. Tools are bucketed by what the user is trying to # accomplish rather than by implementation detail. Section = Literal[ "cleaners", "transformations", "automations", "finance", "coming_soon", ] @dataclass(frozen=True) class Tool: """One tool's manifest entry.""" tool_id: str # Stable identifier matching the analyzer's tool field. icon: str # Single-glyph icon for the home grid card. name: str # Display name (sidebar + card title). description: str # One-sentence card body. page_slug: str # Streamlit page filename without ".py" (e.g. "1_Deduplicator"). status: Status # "Ready" or "Coming Soon" — drives the card badge color. section: Section # Sidebar group this tool belongs to. tier: Tier = "core" # Build-time gating hook; every tool is "core" today. # Order in this list IS the order shown in each sidebar section, so # arranging it carefully matters. Within "cleaners" the order is the # recommended PIPELINE order (Clean Text → Standardize → Fix Missing → # Find Duplicates) so a user running tools by hand follows the sequence # the orchestrator would. "Coming Soon" tools are grouped at the end in # their own section so they never interleave with working tools, and the # finance-oriented tools (Reconcile, PDF to CSV) live in their own group # (see DECISIONS.md 2026-06-08). TOOLS: list[Tool] = [ Tool( tool_id="02_text_cleaner", icon=":material/text_format:", name="Clean Text", description=( "Trim extra spaces and strip out odd characters that copy-paste " "leaves behind." ), page_slug="2_Text_Cleaner", status="Ready", section="cleaners", ), Tool( tool_id="03_format_standardizer", icon=":material/format_list_bulleted:", name="Standardize Formats", description=( "Make dates, phone numbers, currency, names, and addresses look " "the same throughout." ), page_slug="3_Format_Standardizer", status="Ready", section="cleaners", ), Tool( tool_id="04_missing_handler", icon=":material/help_outline:", name="Fix Missing Values", description=( "Find blank cells (even ones written as 'N/A' or '?') and fill " "them in or remove them." ), page_slug="4_Missing_Values", status="Ready", section="cleaners", ), Tool( tool_id="01_deduplicator", icon=":material/search:", name="Find Duplicates", description=( "Find rows that repeat — exact and similar — and remove the extras." ), page_slug="1_Deduplicator", status="Ready", section="cleaners", ), Tool( tool_id="05_column_mapper", icon=":material/view_column:", name="Map Columns", description=( "Rename columns, change their order, and set each one as text, " "number, or date." ), page_slug="5_Column_Mapper", status="Ready", section="transformations", ), Tool( tool_id="09_pipeline_runner", icon=":material/auto_awesome:", name="Automated Workflows", description=( "Run several tools in a row — save the steps once, reuse them " "anytime." ), page_slug="9_Pipeline_Runner", status="Ready", section="automations", ), Tool( tool_id="11_reconciler", icon=":material/compare_arrows:", name="Reconcile Two Files", description=( "Compare two lists of transactions (e.g. bank vs. ledger) and " "flag what doesn't match." ), page_slug="11_Reconciler", status="Ready", section="finance", ), Tool( tool_id="10_pdf_extractor", icon=":material/picture_as_pdf:", name="PDF to CSV", description=( "Pull transactions out of bank-statement PDFs into a clean CSV file." ), page_slug="10_PDF_Extractor", status="Ready", section="finance", ), Tool( tool_id="06_outlier_detector", icon=":material/insights:", name="Find Unusual Values", description=( "Spot values that look wrong — way too high, way too low, or " "breaking your rules." ), page_slug="6_Outlier_Detector", status="Coming Soon", section="coming_soon", ), Tool( tool_id="08_validator_reporter", icon=":material/check_circle:", name="Quality Check", description=( "Check your file against rules you set, and export a PDF or " "Excel report." ), page_slug="8_Validator_Reporter", status="Coming Soon", section="coming_soon", ), Tool( tool_id="07_multi_file_merger", icon=":material/account_tree:", name="Combine Files", description=( "Combine several CSV or Excel files into one — even if their " "columns don't match." ), page_slug="7_Multi_File_Merger", status="Coming Soon", section="coming_soon", ), ] # Display labels for each sidebar section. Kept here so i18n falls back # to a sensible English string if a translation pack is missing the key. SECTION_LABELS: dict[Section, str] = { "cleaners": "Data Cleaners", "transformations": "Transformations", "automations": "Automations", "finance": "Finance", "coming_soon": "Coming soon", } def tools_for_tier(*tiers: Tier) -> list[Tool]: """Subset filter for build-time slicing. Empty *tiers* returns every tool. Used by per-tool packaging to ship only the relevant subset of pages and home-grid cards. """ if not tiers: return list(TOOLS) keep = set(tiers) return [t for t in TOOLS if t.tier in keep] def tools_in_section(section: Section) -> list[Tool]: """Return the tools in *section*, preserving registry order.""" return [t for t in TOOLS if t.section == section] def tool_by_id(tool_id: str) -> Tool | None: return next((t for t in TOOLS if t.tool_id == tool_id), None) def display_name(tool_id: str) -> str: """Return the human-readable name; fall back to the id when unknown.""" t = tool_by_id(tool_id) return t.name if t else tool_id def tool_name(tool_id: str) -> str: """Return the localized tool name, falling back to the registry default.""" from src.i18n import t as _t fallback = display_name(tool_id) translated = _t(f"tools.{tool_id}.name") return translated if translated != f"tools.{tool_id}.name" else fallback def tool_description(tool_id: str) -> str: """Return the localized tool description, falling back to the registry default.""" from src.i18n import t as _t tool = tool_by_id(tool_id) fallback = tool.description if tool else "" translated = _t(f"tools.{tool_id}.description") return translated if translated != f"tools.{tool_id}.description" else fallback def section_label(section: Section) -> str: """Return the localized sidebar section header.""" from src.i18n import t as _t fallback = SECTION_LABELS[section] key = f"nav.section_{section}" translated = _t(key) return translated if translated != key else fallback