# Local `search` in `redmine_tickets.py` (pre-sanity snapshot) **Date:** 2026-07-14 **Status:** documentation of current behavior *before* any search sanity/cleanup pass **Script:** `/home/jason/projects/redmine/redmine_tickets.py` **Typical cache:** `/home/jason/projects/redmine/cache/redmine_tickets` This note captures what local `search` does and does **not** do as of 2026-07-14, after remote sync filters and run-log work landed. It is intentionally frank: docs had been thin; this is the baseline. --- ## Mental model ```text sync = talk to Redmine → update local JSON cache search = filter/list what is already cached ``` `search` is a **cache browser / structured filterer**, not: - a live Redmine issue query tool - full-text corpus search - the semantic Redmine search path (that lives elsewhere) If a ticket is not already under the cache directory for the selected project, **`search` cannot see it.** --- ## Where data comes from Cached issues live as one JSON file per ticket: ```text //.json ``` Example for the durable customer-service cache: ```text /home/jason/projects/redmine/cache/redmine_tickets/customer-service/*.json /home/jason/projects/redmine/cache/redmine_tickets/manifest.json ``` `search` loads via `TicketCache.iter_cached_issues()` (reads every `*.json` in the project subdir). It does **not** call Redmine and does **not** require `REDMINE_API_KEY`. --- ## CLI surface (current) Parent options still apply: - `--base-url` (unused by search, but accepted on the parent parser) - `--project` - `--cache-dir` Subcommand: ```bash ./redmine_tickets.py \ --project customer-service \ --cache-dir /home/jason/projects/redmine/cache/redmine_tickets \ search [filters...] [output...] ``` ### Filters (all local, AND’d) | Flag | Matches on | Notes | |------|------------|--------| | `--updated-since` | issue `updated_on` ≥ value | via `parse_dt` (absolute date/datetime) | | `--updated-before` | issue `updated_on` ≤ value | same | | `--status` | status **name** | case-insensitive **exact** match (`Closed`, `New`, …) — **not** `status_id` | | `--category` | category **name** | case-insensitive **exact** match | Implementation: `filter_issues()` in `redmine_tickets.py`. ### Order + limit | Flag | Behavior | |------|----------| | `--order newest-updated-first` | **default** | | `--order oldest-updated-first` | | | `--order id-asc` / `id-desc` | | | `--order as-is` | filesystem glob order from `iter_cached_issues` | | `--max-tickets N` | after filter + sort, keep first N | Implementation: `select_issues()`. ### Output | Mode | What you get | |------|----------------| | default table | `#id`, `updated_on`, status name, category name, subject (via `print_issue_rows`) | | `--json` | full cached issue payloads as one JSON array | | `--jsonl` | one full issue JSON object per line (good for piping) | Human table is summary-only. Full subject/body/journals/helpdesk are only in `--json` / `--jsonl`. --- ## What it allows (summary) 1. Scope one project + one cache root. 2. Filter cached issues by `updated_on` window, status **name**, category **name**. 3. Sort and cap results. 4. Emit table, JSON, or JSONL. 5. Run offline against cache (no Redmine, no API key). --- ## What it does **not** allow (current gaps) This is the important half of the snapshot: - **No free-text search** (subject / description / journals / from-address) - **No sender / email / contact filters** - **No tracker, assignee, priority, tags** - **No `status_id` / `open` / `closed` shorthand** on search (only status **name**) - **No remote/API filters** on `search` (`--remote-updated-on`, `--remote-updated-since`, `--remote-status-id` are **sync-only**) - **No relative time helpers** like `30d` / `2 days ago` on local search flags (those helpers exist for **remote sync** convenience flags; local `--updated-since` wants a real parseable date/datetime) - **No boolean logic** (AND/OR/NOT nesting) - **No query language** comparable to Redmine UI filters or `rg` - **No ranking / relevance** - **No “search then sync missing ones”** - **No pagination UI** beyond `--max-tickets` - **No writes** (search never mutates cache or Redmine) ### Practical content search today Pipe full payloads and use external tools: ```bash ./redmine_tickets.py \ --project customer-service \ --cache-dir /home/jason/projects/redmine/cache/redmine_tickets \ search --jsonl | rg -i 'gaylord|rate sheet' ``` Or inspect per-ticket files under the project cache dir directly. --- ## Examples that work ```bash cd /home/jason/projects/redmine # Everything currently cached for customer-service (table) ./redmine_tickets.py \ --project customer-service \ --cache-dir /home/jason/projects/redmine/cache/redmine_tickets \ search # Cached tickets updated since a date, newest first, cap 50 ./redmine_tickets.py \ --project customer-service \ --cache-dir /home/jason/projects/redmine/cache/redmine_tickets \ search \ --updated-since 2026-06-14 \ --max-tickets 50 # Only closed, as full JSONL for downstream tools ./redmine_tickets.py \ --project customer-service \ --cache-dir /home/jason/projects/redmine/cache/redmine_tickets \ search \ --status Closed \ --jsonl ``` --- ## Related commands people confuse with “search” | Command | Relation to local search | |---------|--------------------------| | `sync` | fills the cache that `search` reads; supports **server-side** `status_id` + `updated_on` | | `spam-check` | walks **cache**, scores spam; not general search | | `known-spam-scan` | profile match over **cache** | | `decompose` | one-ticket analysis, cache-first | | semantic index / external search | separate stack; not this subcommand | --- ## Remote vs local filter naming (context for sanity later) As of the 2026-07-14 sync work: | Side | Flags (examples) | Used by | |------|------------------|---------| | **Remote (API)** | `--status-id` / `--remote-status-id`, `--remote-updated-on`, `--remote-updated-since`, `--remote-updated-before` | `sync` list call only | | **Local (post)** | `--updated-since`, `--updated-before`, `--status`, `--category` | `sync` post-filter **and** `search` | Help text for the shared local flags still says things like “applied after API list,” which is accurate for `sync` but slightly misleading when those same flags appear under `search` (where there is no API list). A future sanity pass may want search-specific wording. --- ## Implementation pointers | Piece | Location | |-------|----------| | Filter logic | `filter_issues()` | | Sort / max | `select_issues()` | | Cache read | `TicketCache.iter_cached_issues()` | | Table output | `print_issue_rows()` | | CLI wiring | `build_parser()` → `search` subparser; `main()` branch `args.command == "search"` | | Shared filter args | `add_common_filters()` | --- ## Bottom line (baseline for sanity) **Does:** local structured filter/list over already-synced ticket JSON — date window, status name, category name, sort, limit, table/json/jsonl. **Doesn’t:** live Redmine query, free-text, contact/sender, remote filters, relative-date helpers on local flags, or advanced query syntax. Any “make search sane” work after this file should treat the above as the pre-change contract and call out intentional expansions (e.g. `--from`, `--subject-contains`, `--text`, relative dates on local filters) separately.