Phase 3: JEV classification, rules table, editable categories (AUD-69)

Implements FR-3, FR-4, FR-5 and FR-14.

- internal/classify: TypeSafe System One (JEV) HTTP client with the confirmed
  request/response shape, retry with backoff on 429/529/5xx, a plain-text rule
  engine (first match wins, overrides only the named fields), and bounded
  orchestration with a per-run cap.
- internal/store: migration v3 adds an editable `categories` table (name =
  JEV Choice option and classifications key; rename cascades, remove reassigns)
  and seeds a starter rules version once. Classification persistence, coverage,
  and pending/all message queries.
- internal/server: POST /actions/classify and /actions/reclassify, plus the
  FR-14 category editor (GET /categories and add/rename/remove). The triage list
  now groups by the live category list and shows action + confidence.
- CLI: -classify and -classify-all for n8n.
- Docs: docs/phase3-classify.md with the confirmed JEV shape, a live read-only
  classification, and the DoD evidence.

DoD: all 100 stored messages have a non-null category, importance, and suggested
action (0 nulls); JEV picked a newly added/renamed category for 6 real messages
over HTTP. One live JEV call recorded in the phase doc.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-11 21:37:38 +11:00
parent bf43b6bbef
commit af3b5e5f0e
27 changed files with 3235 additions and 102 deletions

View File

@@ -326,3 +326,52 @@ a:hover { text-decoration: underline; }
/* subtle, non-decorative loading pulse (no spinners) */
.htmx-request .empty-state { opacity: .55; transition: opacity .15s ease; }
.btn.htmx-request { opacity: .55; transition: opacity .15s ease; }
/* Phase 3: classifier confidence on each row */
.msg-confidence { font: var(--type-caption); color: var(--ink-faint); white-space: nowrap; }
/* Phase 3: editable category list (FR-14) */
.nav-group-title { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs); }
.nav-edit { font: var(--type-caption); color: var(--primary); }
.categories-panel { margin-top: var(--space-md); }
.categories-help { font: var(--type-body-sm); margin-bottom: var(--space-md); max-width: 68ch; }
.category-add {
display: grid;
grid-template-columns: 1fr 2fr auto;
align-items: end;
gap: var(--space-sm);
padding-bottom: var(--space-md);
border-bottom: 1px solid var(--hairline);
}
.category-list { display: flex; flex-direction: column; }
.category-row {
display: flex;
align-items: end;
gap: var(--space-sm);
padding: var(--space-sm) 0;
border-bottom: 1px solid var(--hairline);
}
.category-row:last-child { border-bottom: 0; }
.category-rename {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) auto auto;
align-items: end;
gap: var(--space-sm);
flex: 1 1 auto;
}
.category-desc { min-width: 0; padding-bottom: var(--space-xs); }
.category-desc .muted { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.category-rename .count { white-space: nowrap; padding-bottom: var(--space-xs); }
.category-remove { flex: 0 0 auto; }
.btn-danger { color: var(--sticker-red); border-color: var(--sticker-red); }
@media (max-width: 759px) {
.category-add { grid-template-columns: 1fr; }
.category-row { flex-direction: column; align-items: stretch; }
.category-rename { grid-template-columns: 1fr; }
.category-remove .btn { width: 100%; }
}