diff --git a/docs/deletion-sites-inventory.md b/docs/deletion-sites-inventory.md index 7f51e12bd..fa4a1178e 100644 --- a/docs/deletion-sites-inventory.md +++ b/docs/deletion-sites-inventory.md @@ -93,7 +93,6 @@ Inventory of every non-test, non-scratch, non-atomic-tmp destructive removal (`s | file:line | target | trigger | path validation | audit log | dry-run | class | why | | --- | --- | --- | --- | --- | --- | --- | --- | | `solstone/apps/speakers/routes.py:313` | `entities//voiceprints.npz` | `api_correct_attribution()` when `_remove_voiceprint()` removes the NPZ's last matching row | entity memory path must resolve, NPZ must exist, and the `(day, segment_key, source, sentence_id)` metadata tuple must match before unlink | yes (`voiceprint_removal` records `not_found`, `rewritten`, or `unlinked`) | no | `✅` | fixed in this sweep: audit payload now records every removal outcome without asserting a removal when nothing matched | -| `solstone/apps/speakers/discovery.py:87` | `awareness/discovery_clusters.json` | discovery starts without an owner centroid | fixed awareness cache path | no | no | `⚠️` | awareness cache invalidation, not journal-domain deletion | | `solstone/apps/speakers/discovery.py:494` | `awareness/discovery_clusters.json` | `identify_unknown_speaker()` completes | fixed awareness cache path | no | no | `⚠️` | cache cleanup after identification; out of scope for this sweep | | `solstone/apps/speakers/owner.py:419,446` | owner-candidate NPZ | owner candidate confirm/reject flows | fixed candidate path under awareness state | no (state update only at `solstone/apps/speakers/owner.py:421-428,447-452`) | no | `⚠️` | awareness candidate lifecycle cleanup, not a journal-domain delete | diff --git a/docs/design/native-sol-client/03-batch-prep.md b/docs/design/native-sol-client/03-batch-prep.md index a629aebe2..a896d36b9 100644 --- a/docs/design/native-sol-client/03-batch-prep.md +++ b/docs/design/native-sol-client/03-batch-prep.md @@ -206,7 +206,7 @@ default and persist only when passed (`solstone/apps/speakers/call.py:10`). | `speakers correct` | `` required, `` required, `` required, `` required, `` required, `` required, `--json` | POST `/app/speakers/api/correct-attribution`; body-JSON `day`, `stream`, `segment_key`, `source`, `sentence_id`, `new_speaker` (`solstone/apps/speakers/call.py:372`, `solstone/apps/speakers/routes.py:1803`). | Server: `missing_request_body`, `missing_required_field`, `invalid_day`, `invalid_segment_or_stream`, `speaker_not_found`, `entity_blocked`, `speaker_review_unavailable`, `speaker_sentence_missing`, `speaker_owner_voice_too_close`, `speaker_voiceprint_busy`, `speaker_labels_busy`. | Immediate mutation; human correction and propagation offer, or JSON. | | `speakers day-segments` | `` required, `--limit/-n` default 20, `--json` | GET `/app/speakers/api/segments-cli/{day}`; path `day`; query `limit` (`solstone/apps/speakers/call.py:1203`, `solstone/apps/speakers/routes.py:1293`). | Server: `invalid_day`, `invalid_request_value`. | Human bounded list or JSON; client-visible truncation by `limit`. | | `speakers detect` | `--force` | POST `/app/speakers/api/owner/detect`; body-JSON `force` (`solstone/apps/speakers/call.py:1022`, `solstone/apps/speakers/routes.py:2204`). | Server: `speaker_voiceprint_busy`. | JSON pretty result; mutation. | -| `speakers discover` | `--json` | POST `/app/speakers/api/discovery/scan` (`solstone/apps/speakers/call.py:655`, `solstone/apps/speakers/routes.py:2363`). | No explicit route reason codes. | Human clusters or JSON; writes discovery cache. | +| `speakers discover` | `--json` | POST `/app/speakers/api/discovery/scan` (`solstone/apps/speakers/native/command.rs:448`, `solstone/apps/speakers/routes.py:2366`). | Server: `speaker_discovery_failed` at 503/500. | Human clusters or JSON; degraded scans show warnings; successful scans may write discovery cache. | | `speakers dismiss-cluster` | `` required, `--disposition VALUE` required | POST `/app/speakers/api/discovery/dismiss`; body-JSON `cluster_id`, `disposition` (`solstone/apps/speakers/call.py:858`, `solstone/apps/speakers/routes.py:2927`). | Server: `missing_required_field`, `invalid_request_value`, `speaker_review_unavailable`, `speaker_command_failed`. CLI maps `speaker_command_failed` detail to stderr exit 1. | JSON pretty result; mutation. | | `speakers dismissals` | no params | GET `/app/speakers/api/discovery/dismissals` (`solstone/apps/speakers/call.py:882`, `solstone/apps/speakers/routes.py:2972`). | No explicit route reason codes. | JSON pretty result; read-only. | | `speakers identify` | `` required, optional ``, `--entity-id`, `--create`, `--entity-type` default `Person`, `--resolve-only`, `--request-id`, repeated `--reviewed-near-match-entity-id` | POST `/app/speakers/api/discovery/identify-cli`; body-JSON `cluster_id`, `name`, `entity_id`, `create_new`, `entity_type`, `resolve_only`, `request_id`, `reviewed_near_match_entity_ids` (`solstone/apps/speakers/call.py:756`, `solstone/apps/speakers/routes.py:2825`). | Local BadParameter: `name or --entity-id is required` (`solstone/apps/speakers/call.py:785`). Server: `missing_required_field`, `invalid_request_value`, `speaker_identify_recoverable`, `speaker_identify_repair_required`, `speaker_identify_conflict`, `speaker_identify_operation_not_found`, `speaker_not_found`, `invalid_entity_type`, `speaker_command_failed`, `speaker_labels_busy`, `speaker_voiceprint_busy` (`solstone/apps/speakers/routes.py:2526`). CLI prints retry/inspect guidance for identify failure codes (`solstone/apps/speakers/call.py:107`). | JSON pretty result. `--resolve-only` is dry-run resolution. | @@ -225,7 +225,7 @@ default and persist only when passed (`solstone/apps/speakers/call.py:10`). | `speakers seed-from-imports` | `--commit`, `--json` | POST `/app/speakers/api/seed-from-imports`; body-JSON `commit` (`solstone/apps/speakers/call.py:938`, `solstone/apps/speakers/routes.py:2703`). | Server: `speaker_owner_centroid_required`. | Default preview banner; human stats or JSON; `--commit` persists. | | `speakers sentences` | `` required, `` required, `` required, `` required, `--json` | GET `/app/speakers/api/review-cli/{day}/{stream}/{segment}/{source}` (`solstone/apps/speakers/call.py:1180`, `solstone/apps/speakers/routes.py:1597`). | Server: `invalid_day`, `invalid_segment_or_stream`, `speaker_review_unavailable`. | Human sentence rows or JSON; read-only. | | `speakers status` | optional `
` | GET `/app/speakers/api/status`; `section` selected locally (`solstone/apps/speakers/call.py:133`, `solstone/apps/speakers/routes.py:2675`). | Local unknown section returns JSON `{"error": "Unknown section '...'. Valid: ..."}` and exits 0. No route-specific reason codes. | JSON pretty; read-only. | -| `speakers suggest` | `--limit/-n INT` default 5, `--json` | GET `/app/speakers/api/suggest`; query `limit` (`solstone/apps/speakers/call.py:1005`, `solstone/apps/speakers/routes.py:2811`). | Server: `invalid_request_value` for invalid limit. | Markdown from server or JSON item array; read-only. | +| `speakers suggest` | `--limit/-n INT` default 5, `--json` | GET `/app/speakers/api/suggest`; query `limit` (`solstone/apps/speakers/native/command.rs:791`, `solstone/apps/speakers/routes.py:2823`). | Server: `invalid_request_value` for invalid limit. | Markdown from server or complete server JSON body; read-only. | | `speakers tag-owner` | `` required, `` required, `` required, `` required, `` required, `--json` | POST `/app/speakers/api/owner/tag-cli`; body-JSON `day`, `stream`, `segment_key`, `source`, `sentence_id` (`solstone/apps/speakers/call.py:1134`, `solstone/apps/speakers/routes.py:2267`). | Server: `missing_request_body`, `speaker_owner_identity_required`, plus assign path `missing_required_field`, `invalid_day`, `invalid_segment_or_stream`, `speaker_sentence_missing`, `entity_blocked`, `speaker_not_found`, `speaker_owner_voice_too_close`, `speaker_voiceprint_busy`, `speaker_labels_busy`. CLI maps selected codes to stderr detail exit 1 (`solstone/apps/speakers/call.py:1157`). | Human tagged/already-assigned line or JSON; immediate mutation. | | `speakers wipe` | `--commit`, `--json` | POST `/app/speakers/api/wipe`; body-JSON `commit` (`solstone/apps/speakers/call.py:598`, `solstone/apps/speakers/routes.py:2740`). | No explicit route reason codes. | Default preview banner; destructive mutation only with `--commit`; human counts or JSON. | @@ -341,7 +341,7 @@ lead support attachment route. Shared upload support lives at | `ledger list` | Shared paginator injects `limit/offset`; optional `--top` client cap. | | `profile list-active` | Shared paginator injects `limit/offset`; no CLI `--top`. | | `speakers day-segments` | Server query `limit`; response includes returned/total. | -| `speakers suggest` | Server query `limit`; response includes bounded items/markdown. | +| `speakers suggest` | Server query `limit`; response is the complete server body with limit-bounded items. | | `transcripts read` | Client-side output truncation by `--max` bytes; not server pagination. | ### Env-default leaves diff --git a/solstone/apps/speakers/tests/test_suggest.py b/solstone/apps/speakers/tests/test_suggest.py index 9781e3647..8bd12dde1 100644 --- a/solstone/apps/speakers/tests/test_suggest.py +++ b/solstone/apps/speakers/tests/test_suggest.py @@ -454,6 +454,52 @@ def test_suggest_unknown_recurring_kernel_failure_uses_discovery_issue( ) +def test_suggest_first_generator_failure_still_aggregates_later_item( + speakers_env, + monkeypatch, +): + speakers_env() + later_item = { + "type": "import_linkable", + "name": "Romeo Montague", + "entity_id": "romeo", + "meetings_mentioned": 1, + "meeting_days": ["20240101"], + } + + def fail_discovery(): + raise SpeakerDiscoveryKernelError(stage="invoke", reason="timeout") + + def later_generator(): + return [later_item] + + def empty_generator(): + return [] + + monkeypatch.setattr(suggest_module, "_discovery_helpers", lambda: fail_discovery) + monkeypatch.setattr(suggest_module, "_import_linkable", later_generator) + monkeypatch.setattr(suggest_module, "_name_variant", empty_generator) + monkeypatch.setattr(suggest_module, "_candidate_pair_review", empty_generator) + monkeypatch.setattr(suggest_module, "_low_confidence_review", empty_generator) + + result = suggest_opportunities() + + assert set(result) == {"status", "items", "issues", "markdown"} + assert result["status"] == "degraded" + assert result["items"] == [later_item] + assert len(result["issues"]) == 1 + issue = result["issues"][0] + assert set(issue) == {"reason_code", "generator", "message"} + assert issue["reason_code"] == "speaker_discovery_failed" + assert issue["generator"] == "_unknown_recurring" + assert issue["message"] == "i couldn't look for new voices right now." + markdown = result["markdown"] + assert "Import linkable: Romeo Montague (romeo)" in markdown + assert "some speaker suggestions are incomplete:" in markdown + assert "i couldn't look for new voices right now. (_unknown_recurring)" in markdown + assert "No speaker curation suggestions found" not in markdown + + def test_suggest_every_invoked_generator_failed_returns_failed( speakers_env, monkeypatch,