@hatk/hatk 0.0.1-alpha.96 → 0.0.1-alpha.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/admin.html CHANGED
@@ -935,130 +935,6 @@
935
935
  border-radius: 10px;
936
936
  background: var(--bg);
937
937
  }
938
- /* ── Review queue ── */
939
- .review-card {
940
- border-bottom: 1px solid var(--border);
941
- padding: 0.9rem 1rem 0.9rem 0.75rem;
942
- border-left: 3px solid transparent;
943
- transition: background 0.1s, border-color 0.1s;
944
- }
945
- .review-card:last-child {
946
- border-bottom: none;
947
- }
948
- .review-card.selected {
949
- background: #faf8f6;
950
- border-left-color: var(--accent);
951
- }
952
- .review-top {
953
- display: flex;
954
- align-items: flex-start;
955
- justify-content: space-between;
956
- gap: 0.75rem;
957
- }
958
- .review-handle {
959
- font-weight: 650;
960
- font-size: 0.95rem;
961
- color: var(--text);
962
- }
963
- .review-score {
964
- font-family: var(--mono);
965
- font-size: 0.8rem;
966
- font-weight: 700;
967
- padding: 0.15rem 0.45rem;
968
- border-radius: 6px;
969
- flex-shrink: 0;
970
- background: var(--bg);
971
- color: var(--text-2);
972
- }
973
- .review-score.high {
974
- background: #fdeaea;
975
- color: #b4242a;
976
- }
977
- .review-score.mid {
978
- background: #fdf3e3;
979
- color: #9a6412;
980
- }
981
- .signal-row {
982
- display: flex;
983
- gap: 0.3rem;
984
- flex-wrap: wrap;
985
- margin-top: 0.45rem;
986
- }
987
- .signal-chip {
988
- font-family: var(--mono);
989
- font-size: 0.7rem;
990
- padding: 0.1rem 0.4rem;
991
- border-radius: 5px;
992
- background: var(--bg);
993
- color: var(--text-3);
994
- border: 1px solid var(--border);
995
- }
996
- .signal-chip.crossed {
997
- background: #fdeaea;
998
- color: #b4242a;
999
- border-color: #f3c9c9;
1000
- }
1001
- .review-state {
1002
- font-size: 0.85rem;
1003
- color: var(--text-2);
1004
- margin-top: 0.45rem;
1005
- white-space: pre-wrap;
1006
- word-break: break-word;
1007
- }
1008
- .review-state .k {
1009
- font-family: var(--mono);
1010
- font-size: 0.72rem;
1011
- color: var(--text-3);
1012
- }
1013
- .review-thumbs {
1014
- display: flex;
1015
- gap: 0.35rem;
1016
- flex-wrap: wrap;
1017
- margin-top: 0.55rem;
1018
- }
1019
- .review-thumbs img {
1020
- width: 72px;
1021
- height: 72px;
1022
- object-fit: cover;
1023
- border-radius: 8px;
1024
- background: var(--bg);
1025
- }
1026
- .review-actions {
1027
- display: flex;
1028
- gap: 0.3rem;
1029
- align-items: center;
1030
- margin-top: 0.6rem;
1031
- flex-wrap: wrap;
1032
- }
1033
- .scan-bar {
1034
- display: flex;
1035
- align-items: center;
1036
- gap: 0.6rem;
1037
- flex-wrap: wrap;
1038
- padding: 0.6rem 1rem;
1039
- border-bottom: 1px solid var(--border);
1040
- font-size: 0.82rem;
1041
- color: var(--text-2);
1042
- }
1043
- .kbd {
1044
- font-family: var(--mono);
1045
- font-size: 0.68rem;
1046
- padding: 0.05rem 0.3rem;
1047
- border: 1px solid var(--border);
1048
- border-bottom-width: 2px;
1049
- border-radius: 4px;
1050
- color: var(--text-3);
1051
- background: #fff;
1052
- }
1053
- .review-hint {
1054
- font-size: 0.75rem;
1055
- color: var(--text-3);
1056
- padding: 0.5rem 1rem;
1057
- display: flex;
1058
- gap: 0.7rem;
1059
- flex-wrap: wrap;
1060
- align-items: center;
1061
- }
1062
938
  .blob-link {
1063
939
  font-family: var(--mono);
1064
940
  font-size: 0.72rem;
@@ -1337,7 +1213,6 @@
1337
1213
  <button class="tab active" data-tab="overview">Overview</button>
1338
1214
  <button class="tab" data-tab="repos">Repos</button>
1339
1215
  <button class="tab" data-tab="content">Content</button>
1340
- <button class="tab" data-tab="review">Review</button>
1341
1216
  <button class="tab" data-tab="reports">Reports</button>
1342
1217
  </nav>
1343
1218
 
@@ -1414,28 +1289,6 @@
1414
1289
  </div>
1415
1290
 
1416
1291
  <!-- Reports -->
1417
- <div class="tab-panel" id="panel-review">
1418
- <div class="search-bar">
1419
- <select class="search-input" id="review-classifier" style="max-width: 200px">
1420
- <option value="">All classifiers</option>
1421
- </select>
1422
- <select class="search-input" id="review-state" style="max-width: 180px">
1423
- <option value="pending">Needs review</option>
1424
- <option value="actioned">Actioned</option>
1425
- <option value="all">All scored</option>
1426
- </select>
1427
- <select class="search-input" id="review-min" style="max-width: 160px">
1428
- <option value="0.5">Score &ge; 0.50</option>
1429
- <option value="0.6">Score &ge; 0.60</option>
1430
- <option value="0.7">Score &ge; 0.70</option>
1431
- <option value="0.8">Score &ge; 0.80</option>
1432
- <option value="0">Any score</option>
1433
- </select>
1434
- <button class="btn btn-sm" id="review-scan-btn">Scan</button>
1435
- </div>
1436
- <div id="review-results"></div>
1437
- </div>
1438
-
1439
1292
  <div class="tab-panel" id="panel-reports">
1440
1293
  <div class="search-bar">
1441
1294
  <select class="search-input" id="reports-status" style="max-width: 200px">
@@ -1457,7 +1310,6 @@
1457
1310
  <button class="bnav-btn active" data-tab="overview">Overview</button>
1458
1311
  <button class="bnav-btn" data-tab="repos">Repos</button>
1459
1312
  <button class="bnav-btn" data-tab="content">Content</button>
1460
- <button class="bnav-btn" data-tab="review">Review</button>
1461
1313
  <button class="bnav-btn" data-tab="reports">Reports</button>
1462
1314
  </div>
1463
1315
  </div>
@@ -1650,7 +1502,6 @@
1650
1502
  if (tab === 'overview') loadOverview()
1651
1503
  if (tab === 'repos') loadRepos()
1652
1504
  if (tab === 'content') loadContent()
1653
- if (tab === 'review') loadReview()
1654
1505
  if (tab === 'reports') loadReports()
1655
1506
  if (push) pushURL({ tab, status: '', q: '', offset: 0, cq: '' })
1656
1507
  }
@@ -2317,324 +2168,6 @@
2317
2168
  loadReports()
2318
2169
  })
2319
2170
 
2320
- // ── Review queue ──
2321
- //
2322
- // The queue exists so nobody has to look at every account. Each card
2323
- // carries the state the model was shown and the score it returned, so a
2324
- // decision can be made from the list without opening anything.
2325
-
2326
- const review = { items: [], index: 0, total: 0, offset: 0, limit: 25, thumbs: new Map() }
2327
- let reviewClassifiersLoaded = false
2328
- let scanPoll = null
2329
-
2330
- function scoreClass(v) {
2331
- return v >= 0.8 ? 'high' : v >= 0.6 ? 'mid' : ''
2332
- }
2333
-
2334
- async function loadReview() {
2335
- const container = document.getElementById('review-results')
2336
- if (!reviewClassifiersLoaded) {
2337
- try {
2338
- const stats = await api('/admin/review/stats')
2339
- const sel = document.getElementById('review-classifier')
2340
- for (const name of stats.classifiers || []) {
2341
- const o = document.createElement('option')
2342
- o.value = o.textContent = name
2343
- sel.appendChild(o)
2344
- }
2345
- reviewClassifiersLoaded = true
2346
- } catch (e) {
2347
- container.innerHTML = `<div class="empty-state">${escapeHtml(e.message)}</div>`
2348
- return
2349
- }
2350
- }
2351
- const classifier = document.getElementById('review-classifier').value
2352
- const state = document.getElementById('review-state').value
2353
- const minScore = document.getElementById('review-min').value
2354
- container.innerHTML = '<div class="loading">Loading</div>'
2355
- try {
2356
- let url = `/admin/review?state=${state}&minScore=${minScore}&limit=${review.limit}&offset=${review.offset}`
2357
- if (classifier) url += `&classifier=${encodeURIComponent(classifier)}`
2358
- const result = await api(url)
2359
- review.items = result.rows || []
2360
- review.total = result.total
2361
- review.index = 0
2362
- renderReview()
2363
- } catch (e) {
2364
- container.innerHTML = `<div class="empty-state">${escapeHtml(e.message)}</div>`
2365
- }
2366
- }
2367
-
2368
- function renderScanBar(stats) {
2369
- const el = document.getElementById('review-scan-status')
2370
- if (!el) return
2371
- if (!stats.configured) {
2372
- el.textContent = 'System One not configured — set `typesafe` in hatk.config.ts'
2373
- return
2374
- }
2375
- const scan = stats.scan
2376
- if (scan?.running) {
2377
- el.textContent = `Scanning… ${scan.scored} scored, ${scan.skipped} skipped, ${scan.filed} filed`
2378
- } else if (scan) {
2379
- const cost = ((scan.inputTokens / 1e6) * 0.042).toFixed(3)
2380
- el.textContent = `Last scan: ${scan.scored} scored, ${scan.skipped} unchanged, ${scan.filed} filed, $${cost}`
2381
- } else {
2382
- el.textContent = `${stats.total} scored · ${stats.pending} awaiting review`
2383
- }
2384
- }
2385
-
2386
- // Render the model's state as short "key: value" lines. Long values are
2387
- // clipped rather than dropped: the reason an account looks wrong is often
2388
- // the first few words of its bio.
2389
- function renderState(state) {
2390
- if (!state || typeof state !== 'object') return ''
2391
- const parts = []
2392
- for (const [k, v] of Object.entries(state)) {
2393
- if (v == null || v === '') continue
2394
- let text
2395
- if (typeof v === 'string') text = v
2396
- else if (Array.isArray(v)) {
2397
- text = v
2398
- .map((x) => (typeof x === 'string' ? x : x?.title || x?.text || JSON.stringify(x)))
2399
- .filter(Boolean)
2400
- .slice(0, 3)
2401
- .join(' · ')
2402
- } else if (typeof v === 'object') {
2403
- text = Object.entries(v)
2404
- .filter(([, n]) => n)
2405
- .map(([n, val]) => `${n} ${val}`)
2406
- .join(', ')
2407
- } else text = String(v)
2408
- if (!text) continue
2409
- if (text.length > 180) text = text.slice(0, 180) + '…'
2410
- parts.push(`<span class="k">${escapeHtml(k)}</span> ${escapeHtml(text)}`)
2411
- }
2412
- return parts.join('<br>')
2413
- }
2414
-
2415
- function renderReview() {
2416
- const container = document.getElementById('review-results')
2417
- const labelOptions = labelDefinitions.map((d) => `<option value="${d.identifier}">${d.identifier}</option>`).join('')
2418
-
2419
- if (!review.items.length) {
2420
- container.innerHTML = `
2421
- <div class="scan-bar"><span id="review-scan-status"></span></div>
2422
- <div class="empty-state">Nothing awaiting review</div>`
2423
- api('/admin/review/stats').then(renderScanBar).catch(() => {})
2424
- return
2425
- }
2426
-
2427
- const showPagination = review.total > review.limit
2428
- container.innerHTML = `
2429
- <div class="card">
2430
- <div class="scan-bar"><span id="review-scan-status"></span></div>
2431
- <div class="review-hint">
2432
- <span><span class="kbd">j</span><span class="kbd">k</span> move</span>
2433
- <span><span class="kbd">t</span> takedown</span>
2434
- <span><span class="kbd">l</span> label</span>
2435
- <span><span class="kbd">d</span> dismiss</span>
2436
- </div>
2437
- ${review.items
2438
- .map((r, i) => {
2439
- const signals = Object.entries(r.signals || {}).sort((a, b) => b[1].score - a[1].score)
2440
- const top = Number(r.top_score || 0)
2441
- const handle = r.handle ? '@' + escapeHtml(r.handle) : escapeHtml(r.subject_did)
2442
- const takendown = r.repo_status === 'takendown'
2443
- return `<div class="review-card${i === review.index ? ' selected' : ''}" data-review-index="${i}">
2444
- <div class="review-top">
2445
- <div style="min-width:0">
2446
- <div class="review-handle">${handle}${takendown ? ' <span class="label-tag">taken down</span>' : ''}</div>
2447
- <div class="record-uri" title="${escapeHtml(r.subject_uri)}">${escapeHtml(r.subject_uri)}</div>
2448
- </div>
2449
- <div class="review-score ${scoreClass(top)}">${top.toFixed(2)}</div>
2450
- </div>
2451
- <div class="signal-row">
2452
- ${signals
2453
- .map(
2454
- ([name, sig]) =>
2455
- `<span class="signal-chip${sig.score >= 0.5 ? ' crossed' : ''}" title="${escapeHtml(sig.classifier || '')}">${escapeHtml(name)} ${sig.score.toFixed(2)}${
2456
- sig.choice ? ' · ' + escapeHtml(String(sig.choice)) : ''
2457
- }</span>`,
2458
- )
2459
- .join('')}
2460
- </div>
2461
- <div class="review-state">${renderState(r.state)}</div>
2462
- <div class="review-thumbs" data-thumbs-for="${escapeHtml(r.subject_did)}"></div>
2463
- <div class="review-actions">
2464
- <button class="btn btn-sm" data-review-act="takedown" data-i="${i}" style="background:#b4242a;color:white">Takedown</button>
2465
- <select class="search-input" data-review-label="${i}" style="max-width:150px;font-size:0.8rem;padding:0.25rem 0.5rem">
2466
- ${labelOptions}
2467
- </select>
2468
- <button class="btn btn-sm" data-review-act="label" data-i="${i}">Apply label</button>
2469
- <button class="btn btn-sm" data-review-act="dismiss" data-i="${i}">Dismiss</button>
2470
- <a class="btn btn-sm" href="/admin?tab=repos&q=${encodeURIComponent(r.subject_did)}">Open in Repos</a>
2471
- </div>
2472
- </div>`
2473
- })
2474
- .join('')}
2475
- ${
2476
- showPagination
2477
- ? `<div class="pagination">
2478
- <span>${review.offset + 1}–${Math.min(review.offset + review.limit, review.total)} of ${review.total.toLocaleString()}</span>
2479
- <div class="pagination-buttons">
2480
- <button class="btn btn-sm" data-review-page="prev" ${review.offset === 0 ? 'disabled' : ''}>Prev</button>
2481
- <button class="btn btn-sm" data-review-page="next" ${review.offset + review.limit >= review.total ? 'disabled' : ''}>Next</button>
2482
- </div>
2483
- </div>`
2484
- : ''
2485
- }
2486
- </div>`
2487
-
2488
- api('/admin/review/stats').then(renderScanBar).catch(() => {})
2489
- wireReview()
2490
- loadReviewThumbs()
2491
- }
2492
-
2493
- // Thumbnails come from a second request per subject, so they are fetched
2494
- // after the cards are on screen and cached — the queue stays useful
2495
- // whether or not they arrive.
2496
- async function loadReviewThumbs() {
2497
- for (const r of review.items) {
2498
- const holder = document.querySelector(`[data-thumbs-for="${CSS.escape(r.subject_did)}"]`)
2499
- if (!holder || holder.dataset.done) continue
2500
- holder.dataset.done = '1'
2501
- try {
2502
- let ctx = review.thumbs.get(r.subject_did)
2503
- if (!ctx) {
2504
- ctx = await api(`/admin/review/subject/${encodeURIComponent(r.subject_did)}`)
2505
- review.thumbs.set(r.subject_did, ctx)
2506
- }
2507
- const blobs = []
2508
- for (const records of Object.values(ctx.records || {})) {
2509
- for (const rec of records) {
2510
- for (const b of findBlobs(rec.value || rec)) {
2511
- if (b.cid && (b.mimeType || '').startsWith('image/')) blobs.push(b)
2512
- }
2513
- }
2514
- }
2515
- holder.innerHTML = blobs
2516
- .slice(0, 6)
2517
- .map((b) => `<img src="${blobUrl(r.subject_did, b.cid)}" loading="lazy" alt="">`)
2518
- .join('')
2519
- } catch {
2520
- /* context is a nicety; the score and state already carry the decision */
2521
- }
2522
- }
2523
- }
2524
-
2525
- function selectReview(i) {
2526
- if (!review.items.length) return
2527
- review.index = Math.max(0, Math.min(review.items.length - 1, i))
2528
- document.querySelectorAll('.review-card').forEach((el, n) => {
2529
- el.classList.toggle('selected', n === review.index)
2530
- if (n === review.index) el.scrollIntoView({ block: 'nearest' })
2531
- })
2532
- }
2533
-
2534
- async function actOnReview(i, action) {
2535
- const item = review.items[i]
2536
- if (!item) return
2537
- const body = { did: item.subject_did, uri: item.subject_uri, action }
2538
- if (action === 'label') {
2539
- const sel = document.querySelector(`[data-review-label="${i}"]`)
2540
- body.label = sel?.value
2541
- if (!body.label) return toast('Pick a label first', 'error')
2542
- }
2543
- try {
2544
- await api('/admin/review/act', { method: 'POST', body: JSON.stringify(body) })
2545
- toast(action === 'dismiss' ? 'Dismissed' : action === 'takedown' ? 'Taken down' : 'Label applied')
2546
- review.items.splice(i, 1)
2547
- review.total = Math.max(0, review.total - 1)
2548
- if (review.index >= review.items.length) review.index = Math.max(0, review.items.length - 1)
2549
- renderReview()
2550
- } catch (e) {
2551
- toast(e.message, 'error')
2552
- }
2553
- }
2554
-
2555
- function wireReview() {
2556
- document.querySelectorAll('[data-review-index]').forEach((el) => {
2557
- el.addEventListener('click', (e) => {
2558
- if (e.target.closest('button,select,a')) return
2559
- selectReview(Number(el.dataset.reviewIndex))
2560
- })
2561
- })
2562
- document.querySelectorAll('[data-review-act]').forEach((b) => {
2563
- b.addEventListener('click', () => actOnReview(Number(b.dataset.i), b.dataset.reviewAct))
2564
- })
2565
- document.querySelectorAll('[data-review-page]').forEach((b) => {
2566
- b.addEventListener('click', () => {
2567
- review.offset += b.dataset.reviewPage === 'next' ? review.limit : -review.limit
2568
- review.offset = Math.max(0, review.offset)
2569
- loadReview()
2570
- })
2571
- })
2572
- }
2573
-
2574
- document.getElementById('review-classifier').addEventListener('change', () => {
2575
- review.offset = 0
2576
- loadReview()
2577
- })
2578
- document.getElementById('review-state').addEventListener('change', () => {
2579
- review.offset = 0
2580
- loadReview()
2581
- })
2582
- document.getElementById('review-min').addEventListener('change', () => {
2583
- review.offset = 0
2584
- loadReview()
2585
- })
2586
-
2587
- document.getElementById('review-scan-btn').addEventListener('click', async () => {
2588
- const classifier = document.getElementById('review-classifier').value
2589
- try {
2590
- await api('/admin/review/scan', {
2591
- method: 'POST',
2592
- body: JSON.stringify(classifier ? { classifier } : {}),
2593
- })
2594
- toast('Scan started')
2595
- if (scanPoll) clearInterval(scanPoll)
2596
- scanPoll = setInterval(async () => {
2597
- try {
2598
- const stats = await api('/admin/review/stats')
2599
- renderScanBar(stats)
2600
- if (!stats.scan?.running) {
2601
- clearInterval(scanPoll)
2602
- scanPoll = null
2603
- loadReview()
2604
- }
2605
- } catch {
2606
- clearInterval(scanPoll)
2607
- scanPoll = null
2608
- }
2609
- }, 2000)
2610
- } catch (e) {
2611
- toast(e.message, 'error')
2612
- }
2613
- })
2614
-
2615
- document.addEventListener('keydown', (e) => {
2616
- const panel = document.getElementById('panel-review')
2617
- if (!panel?.classList.contains('active')) return
2618
- if (e.target.matches('input,select,textarea')) return
2619
- const key = e.key.toLowerCase()
2620
- if (key === 'j' || e.key === 'ArrowDown') {
2621
- e.preventDefault()
2622
- selectReview(review.index + 1)
2623
- } else if (key === 'k' || e.key === 'ArrowUp') {
2624
- e.preventDefault()
2625
- selectReview(review.index - 1)
2626
- } else if (key === 't') {
2627
- e.preventDefault()
2628
- actOnReview(review.index, 'takedown')
2629
- } else if (key === 'l') {
2630
- e.preventDefault()
2631
- actOnReview(review.index, 'label')
2632
- } else if (key === 'd' || key === 'x') {
2633
- e.preventDefault()
2634
- actOnReview(review.index, 'dismiss')
2635
- }
2636
- })
2637
-
2638
2171
  async function loadReports() {
2639
2172
  populateReportsLabelFilter()
2640
2173
  const status = document.getElementById('reports-status').value
@@ -1,168 +0,0 @@
1
- /** A typed question in the shape the System One API accepts. */
2
- export interface ClassifierQuestion {
3
- type: 'noul' | 'choice' | 'score';
4
- instructions: string | Record<string, unknown> | unknown[];
5
- criteria?: unknown;
6
- }
7
- /** The thing being scored: an account (by DID) or a single record. */
8
- export interface ClassifierSubject {
9
- /** `at://` URI for a record subject; the bare DID for an account subject. */
10
- uri: string;
11
- did: string;
12
- handle?: string | null;
13
- collection?: string;
14
- value?: Record<string, any>;
15
- }
16
- export interface ClassifierContext {
17
- db: {
18
- query: (sql: string, params?: any[]) => Promise<any[]>;
19
- run: (sql: string, ...params: any[]) => Promise<void>;
20
- };
21
- subject: ClassifierSubject;
22
- }
23
- export interface ClassifierModule {
24
- /** `account` scans every indexed repo; `record` scans rows of `collections`. */
25
- subject: 'account' | 'record';
26
- /** Collections to scan. Required when `subject` is `record`. */
27
- collections?: string[];
28
- /** The questions to ask. Answers come back under the same keys. Omit when
29
- * the classifier supplies its own `classify`. */
30
- questions?: Record<string, ClassifierQuestion>;
31
- /**
32
- * Produce signals without asking a System One model.
33
- *
34
- * A classifier that runs its own model — an image classifier on the host, a
35
- * hash lookup, a heuristic — sets this instead of `questions`. Everything
36
- * downstream is unchanged: the same thresholds, the same report, the same
37
- * review card. Return a bare number for a plain score.
38
- */
39
- classify?: (state: Record<string, unknown>, ctx: ClassifierContext) => Promise<Record<string, number | Signal>>;
40
- /**
41
- * Build the model `state` for this subject. Return `null` to skip it — a
42
- * skipped subject costs no tokens. Gate on whatever makes the question
43
- * answerable: an account with no content cannot be judged on its content.
44
- */
45
- buildState: (ctx: ClassifierContext) => Promise<Record<string, unknown> | null>;
46
- /**
47
- * Score at or above which a signal files a report. A number applies to every
48
- * question; a map sets it per question. Questions absent from the map never
49
- * file, though their scores are still stored and shown.
50
- */
51
- threshold?: number | Record<string, number>;
52
- /** Label the filed report carries. A function receives the crossing signal. */
53
- label?: string | ((signal: string, score: number) => string);
54
- }
55
- export declare function defineClassifier(module: ClassifierModule): {
56
- /** `account` scans every indexed repo; `record` scans rows of `collections`. */
57
- subject: "account" | "record";
58
- /** Collections to scan. Required when `subject` is `record`. */
59
- collections?: string[];
60
- /** The questions to ask. Answers come back under the same keys. Omit when
61
- * the classifier supplies its own `classify`. */
62
- questions?: Record<string, ClassifierQuestion>;
63
- /**
64
- * Produce signals without asking a System One model.
65
- *
66
- * A classifier that runs its own model — an image classifier on the host, a
67
- * hash lookup, a heuristic — sets this instead of `questions`. Everything
68
- * downstream is unchanged: the same thresholds, the same report, the same
69
- * review card. Return a bare number for a plain score.
70
- */
71
- classify?: (state: Record<string, unknown>, ctx: ClassifierContext) => Promise<Record<string, number | Signal>>;
72
- /**
73
- * Build the model `state` for this subject. Return `null` to skip it — a
74
- * skipped subject costs no tokens. Gate on whatever makes the question
75
- * answerable: an account with no content cannot be judged on its content.
76
- */
77
- buildState: (ctx: ClassifierContext) => Promise<Record<string, unknown> | null>;
78
- /**
79
- * Score at or above which a signal files a report. A number applies to every
80
- * question; a map sets it per question. Questions absent from the map never
81
- * file, though their scores are still stored and shown.
82
- */
83
- threshold?: number | Record<string, number>;
84
- /** Label the filed report carries. A function receives the crossing signal. */
85
- label?: string | ((signal: string, score: number) => string);
86
- __type: "classifiers";
87
- };
88
- interface LoadedClassifier extends ClassifierModule {
89
- name: string;
90
- }
91
- /** Discover and load classifier modules from the `classifiers/` directory. */
92
- export declare function initClassifiers(classifiersDir: string): Promise<void>;
93
- /**
94
- * Register a classifier directly, bypassing directory discovery.
95
- *
96
- * Mirrors {@link registerLabelModule}: tests and embedders that build their own
97
- * module graph have no `classifiers/` directory to read.
98
- */
99
- export declare function registerClassifier(name: string, module: ClassifierModule): void;
100
- /** Clear all registered classifiers (for hot-reload). */
101
- export declare function clearClassifiers(): void;
102
- /** Names of all loaded classifiers. */
103
- export declare function listClassifiers(): string[];
104
- export declare function getClassifier(name: string): LoadedClassifier | undefined;
105
- export interface SystemOneConfig {
106
- apiKey: string;
107
- model: string;
108
- endpoint: string;
109
- }
110
- export declare function configureSystemOne(config: SystemOneConfig | null): void;
111
- export declare function isSystemOneConfigured(): boolean;
112
- /** A single answer, normalized to a 0–1 score regardless of question type. */
113
- export interface Signal {
114
- score: number;
115
- type: string;
116
- /** Choice/Score only: the selected option and its confidence. */
117
- choice?: string;
118
- confidence?: number;
119
- }
120
- /**
121
- * Ask the model one request's worth of questions about one state.
122
- *
123
- * Retries on 429 and 5xx, honoring `retry-after` when the response carries one.
124
- * The caller's concurrency is what keeps us under the rate limit; this only
125
- * recovers the requests that still slip past it.
126
- */
127
- export declare function askSystemOne(state: Record<string, unknown>, questions: Record<string, ClassifierQuestion>, opts?: {
128
- attempts?: number;
129
- signal?: AbortSignal;
130
- }): Promise<{
131
- answers: Record<string, Signal>;
132
- model: string;
133
- inputTokens: number;
134
- }>;
135
- export interface ScanProgress {
136
- /** Subjects considered. */
137
- scanned: number;
138
- /** Gated out by `buildState` returning null, or unchanged since the last scan. */
139
- skipped: number;
140
- /** Sent to the model and stored. */
141
- scored: number;
142
- /** Reports filed for crossing a threshold. */
143
- filed: number;
144
- errors: number;
145
- inputTokens: number;
146
- running: boolean;
147
- startedAt: string;
148
- finishedAt?: string;
149
- error?: string;
150
- }
151
- /** Progress of the scan in flight, or the last one to finish. */
152
- export declare function getScanProgress(): ScanProgress | null;
153
- /**
154
- * Score every subject a classifier applies to, storing results and filing a
155
- * report for each crossing signal.
156
- *
157
- * Subjects whose state is unchanged since the last scan are skipped unless
158
- * `force` is set, so a repeat scan costs only what actually moved.
159
- */
160
- export declare function runScan(opts?: {
161
- classifier?: string;
162
- force?: boolean;
163
- limit?: number;
164
- concurrency?: number;
165
- signal?: AbortSignal;
166
- }): Promise<ScanProgress>;
167
- export {};
168
- //# sourceMappingURL=classifiers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"classifiers.d.ts","sourceRoot":"","sources":["../src/classifiers.ts"],"names":[],"mappings":"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"}