This repository was archived by the owner on May 26, 2026. It is now read-only.
feat(kora): KR-FE-PHRASEBOOK-VIEWER — read + test-render phrasebook in cockpit#167
Merged
rafe-walker merged 1 commit intoMay 24, 2026
Conversation
…n cockpit Read-only viewer + live regex tester for the Slack DM short- circuit phrasebook (PR #160). Operator can SEE which patterns the live handler consults before invoking the reasoning engine AND test what would happen for an operator-supplied sample message against the current snapshot — does NOT call the reasoning engine, does NOT send DMs. Write/edit path is a follow-on bucket (KR-FE-PHRASEBOOK-EDITOR + KR-API-PHRASEBOOK-CRUD). Backend (kora_cli/web_server.py) ================================= Two new read-only endpoints: * GET /api/phrasebook/slack_dm Returns the same entries the live handler matches against (via dm_phrasebook.load_phrasebook — honors operator override at ${KORA_HOME}/phrasebook/slack_dm.yml, falls back to bundled default). Each entry's referenced_snapshot_fields is the list of snapshot paths its reply_template references — operator sees per-entry snapshot dependencies without re-parsing client-side. Also echoes override_candidate_path even when absent so operator knows where to drop the YAML to start overriding. * POST /api/phrasebook/slack_dm/test Operator-supplied text → matched entry + rendered reply. Pure preview of what the live handler would do RIGHT NOW. The would_fall_through_to_reasoning_engine boolean is the single answer the operator usually wants ("is this $0 or cents right now?"). Helper _extract_phrasebook_snapshot_refs(template) → sorted + deduped list of paths from {snapshot.X.Y} placeholders. The regex (_PHRASEBOOK_PLACEHOLDER_RE) is pinned by test_placeholder_regex_matches_dm_phrasebook_source to be identical to dm_phrasebook._PLACEHOLDER_RE so the FE's "this will fall through" affordance agrees with what render_reply walks at runtime. Frontend ========= * web/src/pages/PhrasebookPage.tsx — new top-level page * api.getSlackDmPhrasebook() + api.testSlackDmPhrasebook(text) wrappers + PhrasebookEntryDto / PhrasebookResponse / PhrasebookTestResponse TS types * /phrasebook route + sidebar nav entry (BookOpen icon; placed next to /cost-telemetry since both surface the cheap-substrate thesis — phrasebook hits are the $0 short_circuit path that show up in cost-telemetry's model breakdown) * usePanelView("PhrasebookPage") for usage instrumentation Page layout: * Source banner — operator override vs bundled default; candidate override path echoed when absent so operator knows where to create * Live tester — text input + Test button; result panel visually differentiates the 3 outcome states (per spec "screenshots of matched / unmatched / would-fall-through") * Entries table — pattern / category / description / reply template + per-row "current viability" badge. Referenced snapshot fields render as outline badges by default; warning-toned when the live snapshot value is "unknown" / missing / null so operator sees per-entry which entries would fall through right now Per-row "current viability" check mirrors render_reply's fall- through triggers (dm_phrasebook.py:269-307): * snapshot is null → universal fall-through * any referenced field value is undefined / null / "unknown" → entry-specific fall-through Tests (tests/kora_cli/test_phrasebook_endpoints.py — 17 tests) ================================================================ Backend: * GET returns bundled default when no override; returns override when present; referenced_snapshot_fields extracted (sorted + deduped); override_candidate_path echoed even when absent * POST: non-matching text → unmatched + fall-through; matching text with no snapshot → matched but fall-through (per dm_phrasebook semantics — snapshot=None is universal fall-through trigger, even for no-placeholder templates); matching text with fresh snapshot + all fields → rendered reply populated; matching text with "unknown" sentinel → fall-through; oversized text truncated to 1024 chars defensively * SECURITY: non-matching test response does NOT echo snapshot contents (operator didn't request a render; endpoint must not become an inadvertent snapshot-readback surface for callers without proper auth) * Placeholder regex drift guard — _PHRASEBOOK_PLACEHOLDER_RE must match dm_phrasebook._PLACEHOLDER_RE exactly so referenced-fields extraction agrees with runtime walk Frontend source-pin: * api wrappers exist with correct shape (GET + POST signatures) * Phrasebook types declared (Dto / Response / TestResponse union) * Page exists, uses usePanelView, route registered, nav entry * Page renders all 3 tester outcome states ("No phrasebook entry matched" / "Matched · $0 reply" / "Matched but would fall through") * isDegradedSnapshotValue matches the "unknown" sentinel from dm_phrasebook.py:293 Verification: * 17/17 tests pass * tsc -b clean * vite build clean * Screenshot rendered: web/docs/phrasebook-viewer/states.png (all 3 tester states + preview HTML for reproducibility) Refs: * rafe-walker/kora-docs 17_cc_bucket_prompts/KR-FE-PHRASEBOOK-VIEWER_read_test_render.md * PR #160 — phrasebook + dm_phrasebook module (data source) * PR #157 — snapshot infrastructure (degraded "unknown" sentinel + read_snapshot) * PR #159 — usePanelView instrumentation pattern * PR #162 — api.getSnapshot wrapper (re-used for per-row viability check) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
rafe-walker
pushed a commit
that referenced
this pull request
May 24, 2026
…tion + revert Build-list follow-on to PR #167 (phrasebook read-only viewer). Adds the operator-edit story so the phrasebook can be modified from the cockpit instead of YAML by hand. Also sets the UX foundation that the eventual promotion-review panel will reuse (KR-PROMOTE-PHRASEBOOK proposals are pending edits to this same surface). Backend (kora_cli/short_circuit/phrasebook_editor.py — new) ========================================================== Module hosts everything the read-only viewer didn't need: * validate_entries(entries) → [EntryValidationError, ...] 8 checks (in order; later skipped for an entry that failed earlier to avoid noise): payload is list, length cap (200 entries), required-fields-non-empty, length caps on each field, regex compiles with re.IGNORECASE, catastrophic- backtracking guard (catches (a+)+, (.*)*, etc), snapshot placeholder paths resolve to known scalars, no duplicate (pattern, category) tuples. * SNAPSHOT_SCALAR_PATHS — static frozen-set of known snapshot v4 scalar paths the operator can reference in {snapshot.X.Y} placeholders. Drift-guarded by test_static_schema_matches_snapshot_collectors which greps each leaf key against the snapshot collector source. Only scalars (no dynamic-key dicts like listeners.X or cost_telemetry.X — those str() to Python repr and would render garbage in operator-facing DMs). * write_backup_for / rotate_backups / list_backups — timestamped backups under ${KORA_HOME}/phrasebook/backups/. KORA_PHRASEBOOK_BACKUP_COUNT env var tunes rotation (default 10; clamped to [1, 1000]). Filenames sort chronologically as plain strings (ISO-Z format). * write_phrasebook(entries) — atomic write via utils.atomic_replace (same pattern as snapshot writer). Deterministic field order for readable YAML diffs. * revert_phrasebook(filename=...) — revert to a specific backup OR (no filename) the most-recent OR (no backups) remove the override entirely. Path-traversal defense rejects any filename containing /, \, .., or not matching the slack_dm.*.yml shape. Backend (kora_cli/web_server.py — 3 new endpoints) ================================================== PUT /api/phrasebook/slack_dm — validate + back up + atomic-write + audit POST /api/phrasebook/slack_dm/revert — revert to backup GET /api/phrasebook/slack_dm/backups — list backups PUT contract: * Validation fails → 422 with structured per-entry errors; NO write; previous override preserved. * Validation passes → backup current (if exists) → atomic write → rotate backups → audit row → 200 with echoed entries + backup_filename + rotated_count. * Write itself fails → 500; backup preserved so operator can recover. Audit seam (new): phrasebook.updated ==================================== Extended SeamName Literal in kora_cli/audit/jsonl_sink.py. Each successful PUT (or revert) emits one entry with: * actor — "operator" (cockpit-driven; future "kora_proposal_approved" from the promotion-loop bucket reuses this shape) * action — "put" | "revert" * entry_count_before / entry_count_after * backup_filename (when applicable) * rotated_backup_count (when applicable) * reverted_to (when action=revert) Drives the future KR-PROMOTION-REVIEW-PANEL via the existing audit-panel infrastructure (KR-AUDIT-PANEL-ENDPOINTS PR #155). Frontend ======== * web/src/pages/PhrasebookEditor.tsx (new) — hosts the editor sub-components so PhrasebookPage stays readable: EntryEditorRow (4 inline editable fields + per-field validation errors), EditModeControls (Save / Cancel / Add), BackupsDialog (modal with newest-first list + per-row revert + confirm), ClientSidePreview (mirrors dm_phrasebook.match + render_reply in TS so operator can preview in-progress edits without saving). * api wrappers + types: putSlackDmPhrasebook / revertSlackDmPhrasebook / getSlackDmPhrasebookBackups + PhrasebookEntryWrite / PhrasebookPutResponse / PhrasebookValidationErrorEntry/Body / PhrasebookRevertResponse / PhrasebookBackupItem / PhrasebookBackupsResponse. * PhrasebookPage extended with edit-mode toggle. View-mode surface (read-only table + live tester) is unchanged for operators who just want to inspect. Edit-mode swaps to editor rows + client-side preview + Save/Cancel/Add. Backups button in view-mode opens the revert dialog. * 422 validation-error parsing: Save handler unmarshals fetchJSON's "STATUS: BODY" Error message; on 422 with error="validation_failed" the body's per-entry errors are routed to the editor for inline rendering. Tests ===== Backend (tests/kora_cli/test_phrasebook_editor.py — 44 tests): Validation (14): valid entries, non-list, missing field, empty whitespace, all 4 length caps, entries count cap, invalid regex, catastrophic-backtracking parametrized across 4 pathological patterns, unknown vs known snapshot path, duplicate (pattern, category), schema drift guard against state_snapshot.py. Backups (5): no-override returns None, copies content + timestamps filename, rotation keeps N most recent, env override + clamping parametrized, list returns newest- first with entry_count. Write+revert (5): round-trip load, revert by name, revert most-recent, revert with no backups removes override, path-traversal rejection parametrized across 5 attacks. Endpoints (7): PUT valid + audit, PUT invalid + preserve, PUT no-existing-override, POST revert + audit, POST revert invalid filename → 400, POST revert missing → 404, GET backups newest-first. Audit (1): SeamName Literal includes phrasebook.updated. Integration (1): round-trip PUT then revert restores seed. FE source-pins (tests/kora_cli/test_phrasebook_editor_fe_pins.py — 17 tests): api wrappers (3), TS types declared + shape pinned (2), editor file exists + exports (1), page wiring (3), edit button hidden in edit-mode (1), 422 body parsing branch present (1), ClientSidePreview mirrors backend regex flag + "unknown" sentinel + null-snapshot fall-through (3), SnapshotResponse type covers daemon_health (1). All 76 phrasebook tests pass (44 BE editor + 17 FE pins + 15 existing PR #167 read-side tests). tsc -b clean. vite build clean. Screenshots =========== web/docs/phrasebook-editor/edit_mode.png — edit-mode UI with client-side preview rendering a $0 reply web/docs/phrasebook-editor/validation.png — 422 response with 4 inline per-field errors (bad regex, unknown snapshot path, catastrophic backtracking, missing description) + top-level duplicate error web/docs/phrasebook-editor/revert.png — backups modal with newest-first list, per-row Revert confirm flow, greyed-out corrupt backup Design choices noted (no STOP-ASKs triggered) ============================================= * Snapshot field-path validation uses a STATIC allow-list (SNAPSHOT_SCALAR_PATHS), NOT a live snapshot walk. Spec §4 flagged this as a possible STOP-ASK; the static-list approach is what the spec offered as the alternative ("use SnapshotResponse static schema") and avoids the dynamic-snapshot-during-warm-up problem where freshly- booted daemons would mark canonical paths as invalid. Drift guard test pins each scalar leaf against the snapshot collector source. * Audit seam SeamName extension follows the probe.wake_requested precedent — extend Literal with a new value + add the comment explaining the future actor extension. No consumer drift. * Operator-defined category values: free-form for v1 (the runtime doesn't reserve any category names yet; promotion-loop bucket can add reserved-prefix discipline when its UX lands). Refs ==== * rafe-walker/kora-docs 17_cc_bucket_prompts/KR-FE-PHRASEBOOK-EDITOR-AND-CRUD_write_path_with_validation.md * PR #160 — phrasebook + dm_phrasebook module (read side) * PR #167 — KR-FE-PHRASEBOOK-VIEWER (read-only viewer this extends) * PR #170 — snapshot v4 daemon_health (referenced in SNAPSHOT_SCALAR_PATHS allow-list) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Read-only viewer + live regex tester for the Slack DM short-circuit phrasebook (PR #160). Operator can SEE which patterns the live handler consults before invoking the reasoning engine AND test what would happen for an operator-supplied sample message against the current snapshot.
Read-only v1. Write/edit path is a follow-on bucket (
KR-FE-PHRASEBOOK-EDITOR+KR-API-PHRASEBOOK-CRUD).Live tester — 3 outcome states
"unknown"/ missing → handler would still defer to reasoning. Operator sees which fields are degraded.Committed at
web/docs/phrasebook-viewer/states.png+ reproducible HTML atpreview.html.What's in here
Backend (2 new read-only endpoints):
GET /api/phrasebook/slack_dm— entries from override-or-bundled, with per-entryreferenced_snapshot_fieldsextracted via the same regexrender_replywalks at runtime. Echoesoverride_candidate_patheven when absent so operator knows where to drop YAML.POST /api/phrasebook/slack_dm/test— matched entry + rendered reply for operator-supplied text. Pure preview; doesn't call the reasoning engine, doesn't send DMs, doesn't mutate state.Frontend:
web/src/pages/PhrasebookPage.tsx— source banner, live tester, entries table with per-row "current viability" affordance (warning-toned badges for fields currently"unknown"in the live snapshot)api.getSlackDmPhrasebook()+api.testSlackDmPhrasebook(text)wrappers +PhrasebookEntryDto/PhrasebookResponse/PhrasebookTestResponsetypes/phrasebookroute + sidebar nav entry (BookOpenicon; placed next to/cost-telemetrysince both surface the cheap-substrate thesis — phrasebook hits show up asmodel_used="short_circuit"in cost telemetry's model breakdown)usePanelView("PhrasebookPage")per feat(kora): KR-PANEL-USE-INSTRUMENTATION — panel-view event emit (data-driven cut prerequisite) #159 disciplineSECURITY notes
test_post_does_not_echo_full_snapshot._PHRASEBOOK_PLACEHOLDER_RE(endpoint side) must matchdm_phrasebook._PLACEHOLDER_REexactly. Otherwise the FE's per-entry "referenced fields" list drifts from whatrender_replyactually walks at runtime and the "this will fall through" affordance becomes a lie. Pinned bytest_placeholder_regex_matches_dm_phrasebook_source.Test plan
tests/kora_cli/test_phrasebook_endpoints.py— 17 tests covering both endpoints + FE source-pins:"unknown"sentinel, oversized text cap, SECURITY no-snapshot-echo pin, placeholder regex drift guardpnpm tsc -bcleanpnpm buildclean/phrasebookagainst live daemon → entries render, tester input → POST returns + result panel matches one of the 3 states.STOP-ASK resolutions (none triggered)
/api/phrasebook/*endpoints — clean addition_phrasebook_override_path_or_none) in the endpoint that returns the candidate path EVEN WHEN ABSENT (the private_operator_override_pathreturns None when file is missing; for the viewer we want to surface "where to put the YAML" even pre-override). Mirrorsdm_phrasebookresolution otherwise.KORA_HOMEplumbing works via the establishedkora_constants.get_kora_homepattern (no env divergence)Refs
rafe-walker/kora-docs→17_cc_bucket_prompts/KR-FE-PHRASEBOOK-VIEWER_read_test_render.mddm_phrasebookmodule (data source)"unknown"sentinel +read_snapshot)usePanelViewinstrumentation patternapi.getSnapshotwrapper (re-used for per-row viability check)🤖 Generated with Claude Code