Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-FE-PHRASEBOOK-EDITOR-AND-CRUD — write path with validation + revert#177

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-PHRASEBOOK-EDITOR-AND-CRUD
May 24, 2026
Merged

feat(kora): KR-FE-PHRASEBOOK-EDITOR-AND-CRUD — write path with validation + revert#177
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-PHRASEBOOK-EDITOR-AND-CRUD

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Build-list follow-on to PR #167 (phrasebook viewer, read-only). Operator can now add/edit/remove phrasebook entries via the cockpit instead of editing YAML by hand. Validation prevents bad regex / unknown snapshot paths / catastrophic backtracking from saving. Backups + revert preserve a safe rollback path. New phrasebook.updated audit seam lets the future promotion-loop bucket reuse the same write surface (actor="kora_proposal_approved").

Screenshots

Edit mode active — client-side preview mirrors dm_phrasebook.match_message + render_reply so operator can preview in-progress edits without saving.

Edit mode

Validation error (422) — server returns structured per-entry errors; FE renders each next to the offending field. Top-level errors (duplicates) render at the top. The 4 inline errors here are: bad regex / unknown snapshot path / catastrophic backtracking / missing required field — exercising each of the 8 validation checks.

Validation

Backups + revert dialog — newest-first list with per-row Revert confirm. Corrupt backups (entry_count null) greyed out + Revert disabled.

Revert

Audit seam confirmation

New seam phrasebook.updated added to SeamName Literal at kora_cli/audit/jsonl_sink.py. Each successful PUT (or revert) emits one entry with:

{
  "seam": "phrasebook.updated",
  "details": {
    "actor": "operator",
    "action": "put",         // or "revert"
    "entry_count_before": N,
    "entry_count_after": M,
    "backup_filename": "slack_dm.2026-05-23T20-15-22Z.yml",
    "rotated_backup_count": 0
  }
}

Drives the future KR-PROMOTION-REVIEW-PANEL via the existing audit-panel infrastructure (PR #155). Future actor extension: \"kora_proposal_approved\" from the promotion-loop bucket reuses this exact shape.

What's in here

Backend (3 new endpoints + new module + audit seam extension):

  • PUT /api/phrasebook/slack_dm — validate → backup → atomic-write → rotate → audit
  • POST /api/phrasebook/slack_dm/revert — by named backup OR most-recent OR remove-override
  • GET /api/phrasebook/slack_dm/backups — newest-first list with entry_count
  • New module kora_cli/short_circuit/phrasebook_editor.py hosts validation + backup discipline + write/revert. Read path stays in dm_phrasebook — editor module is import-once-on-write, never on the hot DM path.
  • New audit seam phrasebook.updated in SeamName Literal at kora_cli/audit/jsonl_sink.py

Frontend:

  • New file web/src/pages/PhrasebookEditor.tsx hosts editor sub-components: EntryEditorRow (4 editable fields + per-field errors), EditModeControls (Save / Cancel / Add), BackupsDialog (modal with revert flow), ClientSidePreview (TS mirror of dm_phrasebook.match + render_reply so operator can preview in-progress edits)
  • api.ts: putSlackDmPhrasebook / revertSlackDmPhrasebook / getSlackDmPhrasebookBackups + 7 new TS types
  • PhrasebookPage.tsx: edit-mode toggle (Edit / Backups buttons in view-mode → swap to editor in edit-mode). 422 body parsing routes structured errors back to editor for inline rendering.

Validation surface

8 checks, in order (later checks skipped for an entry that failed earlier, to avoid noise):

# Check Failure target
1 Payload is a list of dicts root
2 List length ≤ 200 entries root
3 Required fields present + non-empty strings per-field
4 Length caps (pattern ≤ 512, reply ≤ 4096, desc ≤ 256, cat ≤ 64) per-field
5 Pattern compiles as Python regex (re.IGNORECASE) pattern
6 No nested unbounded quantifier (catches (a+)+, (.*)*, (\w*)?, (.+)+) pattern
7 Every {snapshot.X.Y} placeholder resolves to SNAPSHOT_SCALAR_PATHS reply_template
8 No duplicate (pattern, category) tuple root (points at first occurrence)

STOP-ASK conditions (none triggered; design choices noted inline)

Spec §4 flagged 3 possible STOP-ASKs. All resolved inline per spec license:

  1. Snapshot path validation source — used the spec-offered alternative: a STATIC SNAPSHOT_SCALAR_PATHS frozen-set in the editor module, pinned by test_static_schema_matches_snapshot_collectors which greps each leaf against state_snapshot.py. Avoids the dynamic-walk problem where freshly-booted daemons with everything \"unknown\" would mark canonical paths as invalid.
  2. Audit seam Literal extension — followed the probe.wake_requested precedent (PR feat(kora): KR-PROBE-AUDIT-AND-CONVERT — cheap-cron + wake-event + fix-envelope per probe #163). New value + comment explaining future actor extension. No consumer drift.
  3. Operator-defined category collision — free-form for v1 (runtime doesn't reserve any names yet). Promotion-loop bucket can add reserved-prefix discipline when its UX lands.

Backup rotation

  • Default: keep 10 most recent backups
  • Env override: KORA_PHRASEBOOK_BACKUP_COUNT (clamped to [1, 1000])
  • Filenames: slack_dm.YYYY-MM-DDTHH-MM-SSZ.yml (sorts chronologically as plain strings)
  • Same-second collisions get a counter suffix (-1, -2, ...) so rapid sequential writes can't clobber

Test plan

  • 76/76 phrasebook tests pass (44 BE editor + 17 FE pins + 15 from PR feat(kora): KR-FE-PHRASEBOOK-VIEWER — read + test-render phrasebook in cockpit #167's existing read tests)
  • pnpm tsc -b clean
  • pnpm build clean
  • Manual smoke: open /phrasebook against a daemon → click Edit → modify a row → Save → check ${KORA_HOME}/phrasebook/backups/ for the new backup file + tail kora_audit_log.jsonl for the phrasebook.updated row → click Backups → revert.

Follow-on

KR-FE-PROMOTION-REVIEW-PANEL reuses this same write surface — when KR-PROMOTE-PHRASEBOOK ships, its proposals will be "pending" phrasebook edits the operator approves/rejects via this UI. The actor field in the audit seam extends to \"kora_proposal_approved\" for that flow.

Refs

🤖 Generated with Claude Code

…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>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant