@marinoscar/kvox-eval 0.0.0-bootstrap → 0.1.0-next.0

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.
Files changed (245) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/dist/ask/index.d.ts +6 -0
  5. package/dist/ask/index.js +28 -0
  6. package/dist/ask/question-schema.d.ts +121 -0
  7. package/dist/ask/question-schema.js +167 -0
  8. package/dist/ask/report.d.ts +88 -0
  9. package/dist/ask/report.js +170 -0
  10. package/dist/ask/scorer.d.ts +104 -0
  11. package/dist/ask/scorer.js +164 -0
  12. package/dist/ask-run/ask-command.d.ts +62 -0
  13. package/dist/ask-run/ask-command.js +254 -0
  14. package/dist/ask-run/host/app.d.ts +65 -0
  15. package/dist/ask-run/host/app.js +146 -0
  16. package/dist/ask-run/host/db.d.ts +38 -0
  17. package/dist/ask-run/host/db.js +126 -0
  18. package/dist/ask-run/host/ports.d.ts +35 -0
  19. package/dist/ask-run/host/ports.js +113 -0
  20. package/dist/ask-run/index.d.ts +13 -0
  21. package/dist/ask-run/index.js +35 -0
  22. package/dist/ask-run/replay.d.ts +128 -0
  23. package/dist/ask-run/replay.js +212 -0
  24. package/dist/ask-run/run-ask-eval.d.ts +125 -0
  25. package/dist/ask-run/run-ask-eval.js +193 -0
  26. package/dist/ask-run/seed-golden-graph.d.ts +64 -0
  27. package/dist/ask-run/seed-golden-graph.js +495 -0
  28. package/dist/cli/index.d.ts +3 -0
  29. package/dist/cli/index.js +12 -0
  30. package/dist/cli/kg-command.d.ts +66 -0
  31. package/dist/cli/kg-command.js +238 -0
  32. package/dist/cli/main.d.ts +18 -0
  33. package/dist/cli/main.js +69 -0
  34. package/dist/fixtures/domains.d.ts +16 -0
  35. package/dist/fixtures/domains.js +33 -0
  36. package/dist/fixtures/fixture-schema.d.ts +323 -0
  37. package/dist/fixtures/fixture-schema.js +199 -0
  38. package/dist/fixtures/index.d.ts +16 -0
  39. package/dist/fixtures/index.js +40 -0
  40. package/dist/fixtures/load.d.ts +73 -0
  41. package/dist/fixtures/load.js +137 -0
  42. package/dist/fixtures/questions.d.ts +15 -0
  43. package/dist/fixtures/questions.js +26 -0
  44. package/dist/index.d.ts +20 -0
  45. package/dist/index.js +36 -0
  46. package/dist/kg/gold-runner.d.ts +13 -0
  47. package/dist/kg/gold-runner.js +57 -0
  48. package/dist/kg/index.d.ts +17 -0
  49. package/dist/kg/index.js +65 -0
  50. package/dist/kg/internals.d.ts +7 -0
  51. package/dist/kg/internals.js +17 -0
  52. package/dist/kg/prediction-schema.d.ts +245 -0
  53. package/dist/kg/prediction-schema.js +102 -0
  54. package/dist/kg/predictions.d.ts +11 -0
  55. package/dist/kg/predictions.js +49 -0
  56. package/dist/kg/report.d.ts +84 -0
  57. package/dist/kg/report.js +125 -0
  58. package/dist/kg/run-kg-eval.d.ts +77 -0
  59. package/dist/kg/run-kg-eval.js +137 -0
  60. package/dist/kg/runner.d.ts +72 -0
  61. package/dist/kg/runner.js +51 -0
  62. package/dist/kg/runners/extract-resolve-runner.d.ts +40 -0
  63. package/dist/kg/runners/extract-resolve-runner.js +199 -0
  64. package/dist/kg/runners/extract-runner.d.ts +65 -0
  65. package/dist/kg/runners/extract-runner.js +243 -0
  66. package/dist/kg/scorer.d.ts +226 -0
  67. package/dist/kg/scorer.js +463 -0
  68. package/dist/kg/targets.d.ts +17 -0
  69. package/dist/kg/targets.js +16 -0
  70. package/dist/kg/text.d.ts +45 -0
  71. package/dist/kg/text.js +100 -0
  72. package/dist/live/adapters.d.ts +16 -0
  73. package/dist/live/adapters.js +58 -0
  74. package/dist/live/env.d.ts +39 -0
  75. package/dist/live/env.js +56 -0
  76. package/dist/live/index.d.ts +6 -0
  77. package/dist/live/index.js +16 -0
  78. package/dist/live/live-ai.d.ts +104 -0
  79. package/dist/live/live-ai.js +157 -0
  80. package/dist/parity/constants.d.ts +79 -0
  81. package/dist/parity/constants.js +104 -0
  82. package/dist/parity/index.d.ts +20 -0
  83. package/dist/parity/index.js +38 -0
  84. package/dist/parity/internals.d.ts +8 -0
  85. package/dist/parity/internals.js +16 -0
  86. package/dist/parity/scenario.d.ts +45 -0
  87. package/dist/parity/scenario.js +245 -0
  88. package/dist/parity/script.d.ts +44 -0
  89. package/dist/parity/script.js +136 -0
  90. package/dist/parity/snapshot.d.ts +43 -0
  91. package/dist/parity/snapshot.js +71 -0
  92. package/dist/real-data/index.d.ts +2 -0
  93. package/dist/real-data/index.js +12 -0
  94. package/dist/real-data/real-data.d.ts +76 -0
  95. package/dist/real-data/real-data.js +156 -0
  96. package/fixtures/kg-golden/README.md +136 -0
  97. package/fixtures/kg-golden/ask-questions.json +458 -0
  98. package/fixtures/kg-golden/meetings/m01-northwind-pilot-kickoff.json +792 -0
  99. package/fixtures/kg-golden/meetings/m02-halden-migration-planning.json +814 -0
  100. package/fixtures/kg-golden/meetings/m03-larkspur-weekly-sync.json +601 -0
  101. package/fixtures/kg-golden/meetings/m04-corvid-intake-discovery.json +690 -0
  102. package/fixtures/kg-golden/meetings/m05-hosting-vendor-selection.json +631 -0
  103. package/fixtures/kg-golden/meetings/m06-northwind-floor-data-review.json +649 -0
  104. package/fixtures/kg-golden/meetings/m07-gullwing-intro-notes.json +428 -0
  105. package/fixtures/kg-golden/meetings/m08-halden-migration-checkpoint.json +672 -0
  106. package/fixtures/kg-golden/meetings/m09-northwind-platform-org-update.json +711 -0
  107. package/fixtures/kg-golden/meetings/m10-corvid-prototype-review.json +623 -0
  108. package/fixtures/kg-golden/meetings/m11-solvane-grid-dashboard-scoping.json +689 -0
  109. package/fixtures/kg-golden/meetings/m12-northwind-pilot-replan.json +681 -0
  110. package/fixtures/kg-golden/meetings/m13-halden-cutover-go-no-go.json +721 -0
  111. package/fixtures/kg-golden/meetings/m14-gullwing-discovery-readout.json +719 -0
  112. package/fixtures/kg-golden/meetings/m15-halden-handover-ravi-leaving.json +716 -0
  113. package/fixtures/kg-golden/meetings/m16-varden-claims-portal-intro.json +704 -0
  114. package/fixtures/kg-golden/meetings/m17-corvid-usability-debrief.json +572 -0
  115. package/fixtures/kg-golden/meetings/m18-hosting-decision-revisited.json +655 -0
  116. package/fixtures/kg-golden/meetings/m19-solvane-integration-kickoff.json +726 -0
  117. package/fixtures/kg-golden/meetings/m20-northwind-go-live-retro.json +673 -0
  118. package/fixtures/kg-golden/meetings/m21-quarterly-account-review.json +449 -0
  119. package/fixtures/kg-golden/meetings/m22-halden-intro-call-2020.json +558 -0
  120. package/fixtures/kg-golden/meetings/m23-gullwing-model-review.json +702 -0
  121. package/fixtures/kg-golden/meetings/m24-varden-discovery-midpoint.json +643 -0
  122. package/fixtures/kg-golden/meetings/m25-corvid-rollout-planning.json +642 -0
  123. package/fixtures/kg-golden/meetings/m26-larkspur-engineering-prep.json +695 -0
  124. package/fixtures/kg-golden/meetings/m27-solvane-phase-one-demo.json +649 -0
  125. package/fixtures/kg-golden/meetings/m28-northwind-building-5-scoping.json +715 -0
  126. package/fixtures/kg-golden/meetings/m29-halden-southern-cutover-review.json +752 -0
  127. package/fixtures/kg-golden/meetings/m30-varden-portal-build-kickoff.json +697 -0
  128. package/fixtures/kg-golden/meetings/m31-gullwing-rollout-renewal.json +681 -0
  129. package/fixtures/kg-golden/meetings/m32-brambleway-weekly-sync.json +472 -0
  130. package/fixtures/kg-golden/meetings/m33-nina-oliver-one-on-one.json +342 -0
  131. package/fixtures/kg-golden/meetings/m34-coffee-with-dana.json +249 -0
  132. package/fixtures/kg-golden/meetings/m35-brambleway-june-sync.json +553 -0
  133. package/fixtures/kg-golden/meetings/m36-copenhagen-conference-planning.json +296 -0
  134. package/host/generated/client.d.ts +1 -0
  135. package/host/generated/client.js +5 -0
  136. package/host/generated/default.d.ts +1 -0
  137. package/host/generated/default.js +5 -0
  138. package/host/generated/edge.d.ts +1 -0
  139. package/host/generated/edge.js +1658 -0
  140. package/host/generated/index-browser.js +1684 -0
  141. package/host/generated/index.d.ts +192335 -0
  142. package/host/generated/index.js +1658 -0
  143. package/host/generated/package.json +139 -0
  144. package/host/generated/query_compiler_fast_bg.js +2 -0
  145. package/host/generated/query_compiler_fast_bg.wasm +0 -0
  146. package/host/generated/query_compiler_fast_bg.wasm-base64.js +2 -0
  147. package/host/generated/runtime/client.d.ts +3433 -0
  148. package/host/generated/runtime/client.js +110 -0
  149. package/host/generated/runtime/index-browser.d.ts +90 -0
  150. package/host/generated/runtime/index-browser.js +6 -0
  151. package/host/generated/runtime/wasm-compiler-edge.js +100 -0
  152. package/host/generated/schema.prisma +6312 -0
  153. package/host/generated/wasm-edge-light-loader.mjs +5 -0
  154. package/host/generated/wasm-worker-loader.mjs +5 -0
  155. package/host/prisma/fragments/kvox.ask.prisma +126 -0
  156. package/host/prisma/fragments/kvox.graph.prisma +1073 -0
  157. package/host/prisma/fragments/kvox.notes.prisma +787 -0
  158. package/host/prisma/fragments/kvox.search.prisma +211 -0
  159. package/host/prisma/fragments/kvox.transcripts.prisma +790 -0
  160. package/host/prisma/kvox.lock +101 -0
  161. package/host/prisma/migrations/20240101000000_initial/migration.sql +297 -0
  162. package/host/prisma/migrations/20240101000001_add_personal_access_tokens/migration.sql +31 -0
  163. package/host/prisma/migrations/20240101000002_add_credentials/migration.sql +20 -0
  164. package/host/prisma/migrations/20240101000003_add_notification_deliveries/migration.sql +27 -0
  165. package/host/prisma/migrations/20240101000004_drop_stale_uuid_defaults/migration.sql +35 -0
  166. package/host/prisma/migrations/20240101000005_add_notifications/migration.sql +22 -0
  167. package/host/prisma/migrations/20240101000006_add_push_subscriptions/migration.sql +25 -0
  168. package/host/prisma/migrations/20240101000007_add_jobs/migration.sql +110 -0
  169. package/host/prisma/migrations/20240101000008_add_worker_nodes/migration.sql +90 -0
  170. package/host/prisma/migrations/20240101000009_add_database_backup_runs/migration.sql +104 -0
  171. package/host/prisma/migrations/20240101000010_add_notification_broadcasts/migration.sql +57 -0
  172. package/host/prisma/migrations/20240101000011_add_backup_run_job_link/migration.sql +87 -0
  173. package/host/prisma/migrations/20240101000012_add_job_node_secrets/migration.sql +61 -0
  174. package/host/prisma/migrations/20240101000013_add_backup_run_pg_dump_version/migration.sql +26 -0
  175. package/host/prisma/migrations/20240101000014_add_job_claim_token/migration.sql +92 -0
  176. package/host/prisma/migrations/20240101000015_add_ai_platform/migration.sql +108 -0
  177. package/host/prisma/migrations/20240101000016_revoke_viewer_ai_use/migration.sql +32 -0
  178. package/host/prisma/migrations/20240101000017_add_user_credentials/migration.sql +29 -0
  179. package/host/prisma/migrations/20240101000018_add_device_session_credential_link/migration.sql +52 -0
  180. package/host/prisma/migrations/20240101000019_add_worker_node_vitals/migration.sql +3 -0
  181. package/host/prisma/migrations/20240101000020_add_job_trace_context/migration.sql +6 -0
  182. package/host/prisma/migrations/20240101000021_add_retention_created_at_indexes/migration.sql +13 -0
  183. package/host/prisma/migrations/20240101000022_add_organizations/migration.sql +142 -0
  184. package/host/prisma/migrations/20240101000023_split_system_org_roles/migration.sql +131 -0
  185. package/host/prisma/migrations/20240101000024_org_scoped_rls/migration.sql +215 -0
  186. package/host/prisma/migrations/20240101000025_add_groups/migration.sql +165 -0
  187. package/host/prisma/migrations/20240101000026_add_grants/migration.sql +117 -0
  188. package/host/prisma/migrations/20240101000027_add_org_settings/migration.sql +49 -0
  189. package/host/prisma/migrations/20240101000028_add_org_credentials/migration.sql +54 -0
  190. package/host/prisma/migrations/20240101000029_add_jobs_org_id/migration.sql +18 -0
  191. package/host/prisma/migrations/20240101000030_add_jobs_org_id_status_index/migration.sql +6 -0
  192. package/host/prisma/migrations/20240101000031_add_broadcast_target_org/migration.sql +21 -0
  193. package/host/prisma/migrations/20240101000032_add_android_app/migration.sql +60 -0
  194. package/host/prisma/migrations/20240101000033_open_job_reason/migration.sql +16 -0
  195. package/host/prisma/migrations/20240101000034_ai_usage_feature/migration.sql +37 -0
  196. package/host/prisma/migrations/20240101000035_add_app_metadata/migration.sql +49 -0
  197. package/host/prisma/migrations/20240101000036_open_sharing_vocabularies/migration.sql +97 -0
  198. package/host/prisma/migrations/20260914130000_kvox_add_transcripts/migration.sql +322 -0
  199. package/host/prisma/migrations/20260914140000_kvox_add_notes/migration.sql +311 -0
  200. package/host/prisma/migrations/20260915120000_kvox_add_search_vectors/migration.sql +150 -0
  201. package/host/prisma/migrations/20260915130000_kvox_add_search_embeddings/migration.sql +362 -0
  202. package/host/prisma/migrations/20260915210000_kvox_add_note_title_source/migration.sql +30 -0
  203. package/host/prisma/migrations/20260923120000_kvox_add_note_generation_context_snapshot/migration.sql +24 -0
  204. package/host/prisma/migrations/20260923130000_kvox_add_user_hidden_note_templates/migration.sql +35 -0
  205. package/host/prisma/migrations/20260923140000_kvox_add_transcript_speaker_identities/migration.sql +6 -0
  206. package/host/prisma/migrations/20260923150000_kvox_transcript_name_checks/migration.sql +95 -0
  207. package/host/prisma/migrations/20260923160000_kvox_add_note_body_format/migration.sql +27 -0
  208. package/host/prisma/migrations/20260923170000_kvox_add_note_version_body_format/migration.sql +23 -0
  209. package/host/prisma/migrations/20260926012548_kvox_add_knowledge_graph/migration.sql +680 -0
  210. package/host/prisma/migrations/20260926030000_kvox_fix_kg_relations_valid_precision_chk/migration.sql +29 -0
  211. package/host/prisma/migrations/20260926030100_kvox_add_transcript_recorded_at/migration.sql +9 -0
  212. package/host/prisma/migrations/20260926030200_kvox_kg_evidence_invariant/migration.sql +60 -0
  213. package/host/prisma/migrations/20260926040000_kvox_add_kg_graph_layouts/migration.sql +30 -0
  214. package/host/prisma/migrations/20260926050000_kvox_add_ask_conversations/migration.sql +99 -0
  215. package/host/prisma/migrations/20260926231512_kvox_add_kg_exports/migration.sql +52 -0
  216. package/host/prisma/migrations/20261016120000_kvox_transcripts_org_id/migration.sql +49 -0
  217. package/host/prisma/platform.lock +319 -0
  218. package/host/prisma/schema/app.kvox.ask.prisma +110 -0
  219. package/host/prisma/schema/app.kvox.graph.prisma +1030 -0
  220. package/host/prisma/schema/app.kvox.notes.prisma +746 -0
  221. package/host/prisma/schema/app.kvox.search.prisma +208 -0
  222. package/host/prisma/schema/app.kvox.transcripts.prisma +726 -0
  223. package/host/prisma/schema/platform.ai.prisma +419 -0
  224. package/host/prisma/schema/platform.android-app.prisma +46 -0
  225. package/host/prisma/schema/platform.base.prisma +15 -0
  226. package/host/prisma/schema/platform.credentials.prisma +156 -0
  227. package/host/prisma/schema/platform.db-backup.prisma +359 -0
  228. package/host/prisma/schema/platform.identity.prisma +638 -0
  229. package/host/prisma/schema/platform.jobs.prisma +824 -0
  230. package/host/prisma/schema/platform.notifications.prisma +641 -0
  231. package/host/prisma/schema/platform.settings.prisma +53 -0
  232. package/host/prisma/schema/platform.sharing.prisma +208 -0
  233. package/host/prisma/schema/platform.storage.prisma +118 -0
  234. package/package.json +112 -1
  235. package/parity/requests.ask.json +2681 -0
  236. package/parity/requests.m01.json +1554 -0
  237. package/parity/requests.m02.json +1554 -0
  238. package/parity/requests.m03.json +1554 -0
  239. package/parity/requests.m04.json +1554 -0
  240. package/parity/requests.m05.json +1554 -0
  241. package/parity/requests.m06.json +1554 -0
  242. package/parity/requests.m07.json +1554 -0
  243. package/parity/requests.m08.json +1554 -0
  244. package/parity/requests.m09.json +1554 -0
  245. package/parity/requests.m10.json +1554 -0
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ // =============================================================================
3
+ // The Ask evaluation's report: per-category rollup + the two gates (#382)
4
+ // =============================================================================
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.askEvalReportSchema = void 0;
7
+ exports.buildAskEvalReport = buildAskEvalReport;
8
+ exports.formatAskEvalReport = formatAskEvalReport;
9
+ exports.formatAnswersMarkdown = formatAnswersMarkdown;
10
+ const zod_1 = require("zod");
11
+ const ratioSchema = zod_1.z.number().min(0).max(1);
12
+ /**
13
+ * The Ask evaluation report: the per-category rollup, the overall numbers, the two gates
14
+ * (citation validity 1.000, zero sensitive leaks), the answer-match threshold and every
15
+ * question's score. The JSON written as `report.json`.
16
+ *
17
+ * @stability experimental
18
+ */
19
+ exports.askEvalReportSchema = zod_1.z.object({
20
+ runAt: zod_1.z.string(),
21
+ model: zod_1.z.string().nullable(),
22
+ questions: zod_1.z.number().int(),
23
+ categories: zod_1.z.array(zod_1.z.object({
24
+ category: zod_1.z.string(),
25
+ count: zod_1.z.number().int(),
26
+ answerMatch: ratioSchema,
27
+ cited: ratioSchema,
28
+ entityRecall: ratioSchema,
29
+ meanSteps: zod_1.z.number(),
30
+ meanToolCalls: zod_1.z.number(),
31
+ meanPromptTokens: zod_1.z.number(),
32
+ meanCompletionTokens: zod_1.z.number(),
33
+ meanMs: zod_1.z.number().nullable(),
34
+ })),
35
+ overall: zod_1.z.object({
36
+ answerMatch: ratioSchema,
37
+ citationValidity: ratioSchema,
38
+ sensitiveLeaks: zod_1.z.number().int(),
39
+ }),
40
+ /** `citationValidity === 1` and `sensitiveLeaks === 0` — the two hard gates. */
41
+ gatesPassed: zod_1.z.boolean(),
42
+ /** `overall.answerMatch >= minMatch`. */
43
+ answerMatchPassed: zod_1.z.boolean(),
44
+ minMatch: ratioSchema,
45
+ perQuestion: zod_1.z.array(zod_1.z.object({
46
+ id: zod_1.z.string(),
47
+ category: zod_1.z.string(),
48
+ citationValidity: ratioSchema,
49
+ cited: zod_1.z.boolean(),
50
+ citeFromHit: zod_1.z.boolean(),
51
+ answerMatch: zod_1.z.boolean(),
52
+ entityRecall: ratioSchema,
53
+ sensitiveLeak: zod_1.z.boolean(),
54
+ steps: zod_1.z.number().int(),
55
+ toolCalls: zod_1.z.number().int(),
56
+ promptTokens: zod_1.z.number().int(),
57
+ completionTokens: zod_1.z.number().int(),
58
+ ms: zod_1.z.number().nullable(),
59
+ notes: zod_1.z.array(zod_1.z.string()),
60
+ })),
61
+ });
62
+ const mean = (values) => (values.length === 0 ? 0 : values.reduce((a, b) => a + b, 0) / values.length);
63
+ /**
64
+ * Rolls the per-question scores of one run up into a report.
65
+ *
66
+ * @param scores - one score per answered question.
67
+ * @param meta - the model, the answer-match threshold and (for a test) the run time.
68
+ * @returns the report.
69
+ *
70
+ * @stability experimental
71
+ */
72
+ function buildAskEvalReport(scores, meta) {
73
+ const byCategory = new Map();
74
+ for (const s of scores)
75
+ byCategory.set(s.category, [...(byCategory.get(s.category) ?? []), s]);
76
+ const categories = [...byCategory.entries()]
77
+ .sort(([a], [b]) => a.localeCompare(b))
78
+ .map(([category, rows]) => ({
79
+ category,
80
+ count: rows.length,
81
+ answerMatch: mean(rows.map((r) => (r.answerMatch ? 1 : 0))),
82
+ cited: mean(rows.map((r) => (r.cited ? 1 : 0))),
83
+ entityRecall: mean(rows.map((r) => r.entityRecall)),
84
+ meanSteps: mean(rows.map((r) => r.steps)),
85
+ meanToolCalls: mean(rows.map((r) => r.toolCalls)),
86
+ meanPromptTokens: mean(rows.map((r) => r.promptTokens)),
87
+ meanCompletionTokens: mean(rows.map((r) => r.completionTokens)),
88
+ meanMs: rows.every((r) => r.ms === null) ? null : mean(rows.map((r) => r.ms ?? 0)),
89
+ }));
90
+ const citationValidity = mean(scores.map((s) => s.citationValidity));
91
+ const sensitiveLeaks = scores.filter((s) => s.sensitiveLeak).length;
92
+ const answerMatch = mean(scores.map((s) => (s.answerMatch ? 1 : 0)));
93
+ const gatesPassed = citationValidity === 1 && sensitiveLeaks === 0;
94
+ return {
95
+ runAt: (meta.runAt ?? new Date()).toISOString(),
96
+ model: meta.model,
97
+ questions: scores.length,
98
+ categories,
99
+ overall: { answerMatch, citationValidity, sensitiveLeaks },
100
+ gatesPassed,
101
+ answerMatchPassed: answerMatch >= meta.minMatch,
102
+ minMatch: meta.minMatch,
103
+ perQuestion: scores.map((s) => ({
104
+ id: s.questionId,
105
+ category: s.category,
106
+ citationValidity: s.citationValidity,
107
+ cited: s.cited,
108
+ citeFromHit: s.citeFromHit,
109
+ answerMatch: s.answerMatch,
110
+ entityRecall: s.entityRecall,
111
+ sensitiveLeak: s.sensitiveLeak,
112
+ steps: s.steps,
113
+ toolCalls: s.toolCalls,
114
+ promptTokens: s.promptTokens,
115
+ completionTokens: s.completionTokens,
116
+ ms: s.ms,
117
+ notes: s.notes,
118
+ })),
119
+ };
120
+ }
121
+ const fmt = (v) => v.toFixed(3);
122
+ const pad = (s, n) => String(s).padEnd(n);
123
+ /**
124
+ * The report as the fixed-column table `ask:eval` prints, then the gates and the failing questions.
125
+ *
126
+ * @param r - the report.
127
+ * @returns the table.
128
+ *
129
+ * @stability experimental
130
+ */
131
+ function formatAskEvalReport(r) {
132
+ const lines = [];
133
+ lines.push(`ask:eval questions=${r.questions} model=${r.model ?? '-'} ${r.runAt}`);
134
+ lines.push(`${pad('CATEGORY', 14)}${pad('N', 4)}${pad('ANSWER', 8)}${pad('CITED', 8)}${pad('ENT.RECALL', 12)}${pad('STEPS', 8)}${pad('TOKENS(p/c)', 14)}MS`);
135
+ for (const c of r.categories) {
136
+ lines.push(`${pad(c.category, 14)}${pad(c.count, 4)}${pad(fmt(c.answerMatch), 8)}${pad(fmt(c.cited), 8)}${pad(fmt(c.entityRecall), 12)}${pad(c.meanSteps.toFixed(1), 8)}${pad(`${c.meanPromptTokens.toFixed(0)}/${c.meanCompletionTokens.toFixed(0)}`, 14)}${c.meanMs === null ? '-' : c.meanMs.toFixed(0)}`);
137
+ }
138
+ lines.push('');
139
+ lines.push(`OVERALL ANSWER MATCH ${fmt(r.overall.answerMatch)} threshold ≥ ${r.minMatch.toFixed(2)} ${r.answerMatchPassed ? 'PASS' : 'FAIL'}`);
140
+ lines.push(`CITATION VALIDITY ${fmt(r.overall.citationValidity)} required = 1.000 ${r.overall.citationValidity === 1 ? 'PASS' : 'FAIL'}`);
141
+ lines.push(`SENSITIVE LEAKS ${r.overall.sensitiveLeaks} required = 0 ${r.overall.sensitiveLeaks === 0 ? 'PASS' : 'FAIL'}`);
142
+ const failing = r.perQuestion.filter((q) => !q.answerMatch || q.citationValidity < 1 || q.sensitiveLeak);
143
+ if (failing.length > 0) {
144
+ lines.push('');
145
+ lines.push('FAILING QUESTIONS:');
146
+ for (const q of failing) {
147
+ lines.push(` ${q.id} (${q.category}): ${q.notes.join('; ') || (q.citationValidity < 1 ? 'invalid citation' : 'sensitive leak')}`);
148
+ }
149
+ }
150
+ return lines.join('\n');
151
+ }
152
+ /**
153
+ * `answers.md`: every question, its answer text and its per-question numbers — for reading, not for scripts.
154
+ *
155
+ * @stability experimental
156
+ */
157
+ function formatAnswersMarkdown(report, answers) {
158
+ const lines = [`# Ask evaluation answers\n`, `Run at ${report.runAt}, model \`${report.model ?? '-'}\`.\n`];
159
+ for (const q of report.perQuestion) {
160
+ const a = answers.get(q.id);
161
+ lines.push(`## ${q.id} (${q.category})`);
162
+ if (a)
163
+ lines.push(`\n**Q:** ${a.question}\n\n**A:** ${a.content}\n`);
164
+ lines.push(`_citationValidity=${fmt(q.citationValidity)} cited=${q.cited} citeFromHit=${q.citeFromHit} answerMatch=${q.answerMatch} entityRecall=${fmt(q.entityRecall)} sensitiveLeak=${q.sensitiveLeak} steps=${q.steps} toolCalls=${q.toolCalls} tokens=${q.promptTokens}/${q.completionTokens}${q.ms !== null ? ` ms=${q.ms}` : ''}_`);
165
+ if (q.notes.length > 0)
166
+ lines.push(`\n> ${q.notes.join('\n> ')}`);
167
+ lines.push('');
168
+ }
169
+ return lines.join('\n');
170
+ }
@@ -0,0 +1,104 @@
1
+ import type { AskCitation, AskMessageResponse } from '@marinoscar/kvox-contract/ask';
2
+ import type { AskEvalQuestion } from './question-schema.js';
3
+ /**
4
+ * What the scorer needs from the seeder's `idMap` — just the reverse evidence lookup.
5
+ *
6
+ * @stability experimental
7
+ */
8
+ export type EvidenceSourceMap = ReadonlyMap<string, string>;
9
+ /**
10
+ * One question's score: the gate numbers (citation validity, a sensitive leak), the answer
11
+ * and citation checks, and the turn's cost (steps, tool calls, tokens, time).
12
+ *
13
+ * @stability experimental
14
+ */
15
+ export interface AskEvalScore {
16
+ questionId: string;
17
+ category: string;
18
+ /** Valid citations / all citation markers. `1` when there were none to check. */
19
+ citationValidity: number;
20
+ /** At least one valid citation (irrelevant for a `notFound` question, which is scored separately). */
21
+ cited: boolean;
22
+ /** Some valid evidence/document citation resolves, via `idMap`, to a listed `citeFrom` source. */
23
+ citeFromHit: boolean;
24
+ /** Every `answerAll` group matched, no `answerNone` string present, and the `notFound` rule (if any) held. */
25
+ answerMatch: boolean;
26
+ /** Expected entities cited (by citation `label`) / expected entity count. `1` when none were expected. */
27
+ entityRecall: number;
28
+ /** An `answerNone` string turned up in a captured prompt or in the final answer. THE LEAK GATE: must be `false` on every question. */
29
+ sensitiveLeak: boolean;
30
+ steps: number;
31
+ toolCalls: number;
32
+ promptTokens: number;
33
+ completionTokens: number;
34
+ /** Wall-clock time the caller measured for this turn, if it did. */
35
+ ms: number | null;
36
+ /** Human-readable reasons `answerMatch`/`citationValidity`/`citeFromHit` failed, for `answers.md`. */
37
+ notes: string[];
38
+ }
39
+ /**
40
+ * Valid citations / all markers. `1` (vacuously satisfied) with zero markers — a question that expects none.
41
+ *
42
+ * @stability experimental
43
+ */
44
+ export declare function scoreCitationValidity(citations: readonly AskCitation[]): number;
45
+ /**
46
+ * Whether the answer carries at least one valid citation.
47
+ *
48
+ * @param citations - the answer's citations.
49
+ * @returns `true` when one is valid.
50
+ *
51
+ * @stability experimental
52
+ */
53
+ export declare function scoreCited(citations: readonly AskCitation[]): boolean;
54
+ /**
55
+ * Whether a valid evidence citation resolves, through the seeder's map, to one
56
+ * of the question's `citeFrom` sources.
57
+ *
58
+ * @param citeFrom - the expected sources (`mNN-sNNN`, `mNN#note`).
59
+ * @param citations - the answer's citations.
60
+ * @param evidenceSource - evidence id to source, from the seeder.
61
+ * @returns `true` on a hit (vacuously `true` with no `citeFrom`).
62
+ *
63
+ * @stability experimental
64
+ */
65
+ export declare function scoreCiteFromHit(citeFrom: readonly string[], citations: readonly AskCitation[], evidenceSource: EvidenceSourceMap): boolean;
66
+ /**
67
+ * Expected entities cited (by citation label, case-insensitive exact) over the expected count.
68
+ *
69
+ * @param expectedEntities - the labels that must be cited.
70
+ * @param citations - the answer's citations.
71
+ * @returns the ratio (`1` when none were expected).
72
+ *
73
+ * @stability experimental
74
+ */
75
+ export declare function scoreEntityRecall(expectedEntities: readonly string[], citations: readonly AskCitation[]): number;
76
+ /**
77
+ * THE LEAK GATE: whether an `answerNone` string appears in the answer or in any prompt the
78
+ * provider received.
79
+ *
80
+ * @param answerNone - the strings that must never leave.
81
+ * @param content - the final answer.
82
+ * @param capturedPrompts - every request the provider received, serialized.
83
+ * @returns `true` on a leak.
84
+ *
85
+ * @stability experimental
86
+ */
87
+ export declare function scoreSensitiveLeak(answerNone: readonly string[], content: string, capturedPrompts: readonly string[]): boolean;
88
+ /**
89
+ * Every `answerAll` group matched, no `answerNone` present, and (for a `notFound` question) no valid citation plus a not-found phrasing.
90
+ *
91
+ * @stability experimental
92
+ */
93
+ export declare function scoreAnswerMatch(expected: AskEvalQuestion['expected'], content: string, citations: readonly AskCitation[]): {
94
+ matched: boolean;
95
+ reasons: string[];
96
+ };
97
+ /**
98
+ * The full score for one question's answered turn.
99
+ *
100
+ * @stability experimental
101
+ */
102
+ export declare function scoreAnswer(question: AskEvalQuestion, message: Pick<AskMessageResponse, 'content' | 'citations' | 'promptTokens' | 'completionTokens' | 'toolCalls'>, capturedPrompts: readonly string[], evidenceSource: EvidenceSourceMap, timing?: {
103
+ ms?: number;
104
+ }): AskEvalScore;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ // =============================================================================
3
+ // The Ask evaluation's scorer (issue #382, epic #348; docs/specs/ontology.md §21)
4
+ // =============================================================================
5
+ //
6
+ // PURE: no Nest, no Prisma, no provider. Takes one question, the assistant
7
+ // message `ask.respond` wrote for it (`AskMessageResponse`, #376's wire
8
+ // contract), every `messages` array the run's provider actually sent (for the
9
+ // leak check), and the seeder's evidence-source map — and returns one
10
+ // `AskEvalScore`. Called by the replay DB spec, the real runner, and its own
11
+ // unit spec, so the three can never disagree about what a number means.
12
+ //
13
+ // Two GATES (spec: 100% citation validity, zero sensitive leaks across the
14
+ // WHOLE run) are per-question booleans/ratios here; the run aggregates them.
15
+ // =============================================================================
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.scoreCitationValidity = scoreCitationValidity;
18
+ exports.scoreCited = scoreCited;
19
+ exports.scoreCiteFromHit = scoreCiteFromHit;
20
+ exports.scoreEntityRecall = scoreEntityRecall;
21
+ exports.scoreSensitiveLeak = scoreSensitiveLeak;
22
+ exports.scoreAnswerMatch = scoreAnswerMatch;
23
+ exports.scoreAnswer = scoreAnswer;
24
+ const NOT_FOUND_PATTERN = /couldn't find|could not find|no (record|information)|not in your|don't have (any )?(record|information)|no results/i;
25
+ function includesCI(haystack, needle) {
26
+ return haystack.toLowerCase().includes(needle.toLowerCase());
27
+ }
28
+ function groupMatched(content, group) {
29
+ return group.some((alt) => includesCI(content, alt));
30
+ }
31
+ /**
32
+ * Valid citations / all markers. `1` (vacuously satisfied) with zero markers — a question that expects none.
33
+ *
34
+ * @stability experimental
35
+ */
36
+ function scoreCitationValidity(citations) {
37
+ if (citations.length === 0)
38
+ return 1;
39
+ const valid = citations.filter((c) => c.valid).length;
40
+ return valid / citations.length;
41
+ }
42
+ /**
43
+ * Whether the answer carries at least one valid citation.
44
+ *
45
+ * @param citations - the answer's citations.
46
+ * @returns `true` when one is valid.
47
+ *
48
+ * @stability experimental
49
+ */
50
+ function scoreCited(citations) {
51
+ return citations.some((c) => c.valid);
52
+ }
53
+ /**
54
+ * Whether a valid evidence citation resolves, through the seeder's map, to one
55
+ * of the question's `citeFrom` sources.
56
+ *
57
+ * @param citeFrom - the expected sources (`mNN-sNNN`, `mNN#note`).
58
+ * @param citations - the answer's citations.
59
+ * @param evidenceSource - evidence id to source, from the seeder.
60
+ * @returns `true` on a hit (vacuously `true` with no `citeFrom`).
61
+ *
62
+ * @stability experimental
63
+ */
64
+ function scoreCiteFromHit(citeFrom, citations, evidenceSource) {
65
+ if (citeFrom.length === 0)
66
+ return true; // nothing was required (e.g. a negative question)
67
+ return citations.some((c) => {
68
+ if (!c.valid || c.kind !== 'evidence' || !c.id)
69
+ return false;
70
+ const source = evidenceSource.get(c.id);
71
+ return source !== undefined && citeFrom.includes(source);
72
+ });
73
+ }
74
+ /**
75
+ * Expected entities cited (by citation label, case-insensitive exact) over the expected count.
76
+ *
77
+ * @param expectedEntities - the labels that must be cited.
78
+ * @param citations - the answer's citations.
79
+ * @returns the ratio (`1` when none were expected).
80
+ *
81
+ * @stability experimental
82
+ */
83
+ function scoreEntityRecall(expectedEntities, citations) {
84
+ if (expectedEntities.length === 0)
85
+ return 1;
86
+ const citedLabels = new Set(citations.filter((c) => c.valid && c.label).map((c) => c.label.toLowerCase()));
87
+ const hit = expectedEntities.filter((label) => citedLabels.has(label.toLowerCase())).length;
88
+ return hit / expectedEntities.length;
89
+ }
90
+ /**
91
+ * THE LEAK GATE: whether an `answerNone` string appears in the answer or in any prompt the
92
+ * provider received.
93
+ *
94
+ * @param answerNone - the strings that must never leave.
95
+ * @param content - the final answer.
96
+ * @param capturedPrompts - every request the provider received, serialized.
97
+ * @returns `true` on a leak.
98
+ *
99
+ * @stability experimental
100
+ */
101
+ function scoreSensitiveLeak(answerNone, content, capturedPrompts) {
102
+ if (answerNone.length === 0)
103
+ return false;
104
+ const haystacks = [content, ...capturedPrompts];
105
+ return answerNone.some((needle) => haystacks.some((h) => includesCI(h, needle)));
106
+ }
107
+ /**
108
+ * Every `answerAll` group matched, no `answerNone` present, and (for a `notFound` question) no valid citation plus a not-found phrasing.
109
+ *
110
+ * @stability experimental
111
+ */
112
+ function scoreAnswerMatch(expected, content, citations) {
113
+ const reasons = [];
114
+ for (const group of expected.answerAll) {
115
+ if (!groupMatched(content, group))
116
+ reasons.push(`missing required phrase (one of: ${group.join(' | ')})`);
117
+ }
118
+ for (const forbidden of expected.answerNone) {
119
+ if (includesCI(content, forbidden))
120
+ reasons.push(`answer contains forbidden phrase "${forbidden}"`);
121
+ }
122
+ if (expected.notFound) {
123
+ if (citations.some((c) => c.valid))
124
+ reasons.push('a `notFound` question cited something valid');
125
+ if (!NOT_FOUND_PATTERN.test(content))
126
+ reasons.push('answer does not read as "could not find it"');
127
+ }
128
+ return { matched: reasons.length === 0, reasons };
129
+ }
130
+ /**
131
+ * The full score for one question's answered turn.
132
+ *
133
+ * @stability experimental
134
+ */
135
+ function scoreAnswer(question, message, capturedPrompts, evidenceSource, timing = {}) {
136
+ const citations = message.citations;
137
+ const { matched, reasons } = scoreAnswerMatch(question.expected, message.content, citations);
138
+ const citeFromHit = scoreCiteFromHit(question.expected.citeFrom, citations, evidenceSource);
139
+ const notes = [...reasons];
140
+ if (!citeFromHit && question.expected.citeFrom.length > 0) {
141
+ notes.push(`no valid citation resolved to one of: ${question.expected.citeFrom.join(', ')}`);
142
+ }
143
+ return {
144
+ questionId: question.id,
145
+ category: question.category,
146
+ citationValidity: scoreCitationValidity(citations),
147
+ cited: scoreCited(citations),
148
+ citeFromHit,
149
+ answerMatch: matched && (question.expected.citeFrom.length === 0 || citeFromHit),
150
+ entityRecall: scoreEntityRecall(question.expected.entities, citations),
151
+ sensitiveLeak: scoreSensitiveLeak(question.expected.answerNone, message.content, capturedPrompts),
152
+ // The wire `AskToolCall[]` carries a flat `index` across the whole turn,
153
+ // never which loop iteration ("step") produced it — so the caller passes
154
+ // exactly THIS question's captured provider requests, and one request is
155
+ // one loop iteration by construction (`ask-respond.handler.ts`'s `loop`
156
+ // calls `provider.chat()` exactly once per iteration).
157
+ steps: capturedPrompts.length,
158
+ toolCalls: message.toolCalls.length,
159
+ promptTokens: message.promptTokens ?? 0,
160
+ completionTokens: message.completionTokens ?? 0,
161
+ ms: timing.ms ?? null,
162
+ notes,
163
+ };
164
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Where the ask command writes, and the environment it reads (the same shape as
3
+ * the kg command's `AskCommandIo`).
4
+ *
5
+ * @stability experimental
6
+ */
7
+ export interface AskCommandIo {
8
+ /** One line to standard output. */
9
+ out: (line: string) => void;
10
+ /** One line to standard error. */
11
+ err: (line: string) => void;
12
+ /** The environment (`process.env`, or a test's own). */
13
+ env: Readonly<Record<string, string | undefined>>;
14
+ }
15
+ /**
16
+ * The exit codes of `kvox-eval ask` (and of the web app's `npm run ask:eval`).
17
+ *
18
+ * @stability experimental
19
+ */
20
+ export declare const ASK_EVAL_EXIT: Readonly<{
21
+ readonly ok: 0;
22
+ readonly failed: 1;
23
+ readonly config: 2;
24
+ }>;
25
+ /**
26
+ * The usage text of `kvox-eval ask`.
27
+ *
28
+ * @stability experimental
29
+ */
30
+ export declare const ASK_COMMAND_USAGE = "usage: kvox-eval ask [--provider openai|openai-compatible] [--model <id>] [--base-url <url>]\n [--only q01,q07] [--category <c>] [--out <dir>] [--min-match <0..1>]\n kvox-eval ask --replay the scripted self-test (no model, no key)\n reads the API key from KVOX_EVAL_OPENAI_API_KEY (never a flag); default model gpt-4o-mini";
31
+ /**
32
+ * How a host runs the ask command under its own name.
33
+ *
34
+ * @stability experimental
35
+ */
36
+ export interface AskCommandOptions {
37
+ /** The prefix of every message (`kvox-eval ask`, `ask:eval`). */
38
+ name?: string;
39
+ /** The usage text printed with a usage error and for `--help`. */
40
+ usage?: string;
41
+ /**
42
+ * The default `--out` root: a run writes `<dir>/runs/<stamp>/` (the web app's
43
+ * git-ignored `apps/api/.ask-eval/`). Default: none, and the output goes under
44
+ * the system temporary directory.
45
+ */
46
+ syntheticRunsDir?: string | null;
47
+ }
48
+ /**
49
+ * Runs `kvox-eval ask` with the given arguments (everything after `ask`).
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const code = await runAskCommand(['--replay'], { out: console.log, err: console.error, env: process.env });
54
+ * ```
55
+ *
56
+ * @param argv - the arguments.
57
+ * @param io - output streams and environment.
58
+ * @param options - the host's name, usage text and runs directory.
59
+ * @returns the exit code ({@link ASK_EVAL_EXIT}).
60
+ * @stability experimental
61
+ */
62
+ export declare function runAskCommand(argv: string[], io: AskCommandIo, options?: AskCommandOptions): Promise<number>;