agentpaas-acceptance.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { asRecord, canonicalJSON } from "../../.opencode/lib/agentpaas"
  2. export type AcceptanceCase = {
  3. id: string
  4. mode: "chat" | "iterate"
  5. capability: string
  6. files: string[]
  7. prompt: string
  8. artifact: string
  9. expected: Record<string, unknown>
  10. required_artifacts: string[]
  11. review: string[]
  12. }
  13. // These are observable contract checks, not an LLM judgment of scientific quality.
  14. export function evaluateAcceptance(
  15. sample: AcceptanceCase,
  16. artifacts: string[],
  17. content?: string,
  18. files: Record<string, string> = {},
  19. inputs: Record<string, string> = {},
  20. ) {
  21. const checks = sample.required_artifacts.map((name) => ({
  22. name: `artifact:${name}`,
  23. passed: artifacts.includes(name),
  24. }))
  25. const parsed =
  26. content === undefined
  27. ? undefined
  28. : (() => {
  29. try {
  30. return asRecord(JSON.parse(content))
  31. } catch {
  32. return undefined
  33. }
  34. })()
  35. checks.push({ name: "valid_json_object", passed: parsed !== undefined })
  36. for (const [name, expected] of Object.entries(sample.expected))
  37. checks.push({
  38. name: `value:${name}`,
  39. passed: parsed !== undefined && canonicalJSON(parsed[name]) === canonicalJSON(expected),
  40. })
  41. // Compare downloaded outputs to supplied inputs, never trust the model's own scores.
  42. if (sample.id === "data") {
  43. const rows = csv(inputs["measurements.csv"])
  44. const actual = csv(files["cleaned.csv"])
  45. const unique = rows?.filter((row, index, all) => all.findIndex((other) => other.id === row.id) === index)
  46. const clean = unique?.filter((row) => row.value.trim() !== "")
  47. checks.push({
  48. name: "independent:cleaned_rows",
  49. passed: !!clean && !!actual && canonicalJSON(actual) === canonicalJSON(clean),
  50. })
  51. const values = clean?.map((row) => Number(row.value))
  52. checks.push({
  53. name: "independent:recomputed_statistics",
  54. passed:
  55. !!rows &&
  56. !!unique &&
  57. !!clean &&
  58. !!values?.length &&
  59. values.every(Number.isFinite) &&
  60. !!parsed &&
  61. parsed.valid_rows === clean.length &&
  62. parsed.excluded_missing === unique.length - clean.length &&
  63. parsed.duplicates_removed === rows.length - unique.length &&
  64. parsed.overall_mean === values.reduce((a, b) => a + b, 0) / values.length &&
  65. ["A", "B"].every((group) => {
  66. const selected = clean.filter((row) => row.group === group).map((row) => Number(row.value))
  67. return (
  68. selected.length > 0 &&
  69. asRecord(parsed.group_means)?.[group] === selected.reduce((a, b) => a + b, 0) / selected.length
  70. )
  71. }),
  72. })
  73. }
  74. if (["qa", "research"].includes(sample.id)) {
  75. const source = inputs[sample.id === "qa" ? "policy.txt" : "evidence.md"] ?? ""
  76. const ids = sample.id === "qa" ? ["P1", "P2", "P3"] : ["E1", "E2", "E3"]
  77. const quotes = asRecord(parsed?.evidence_quotes)
  78. checks.push({
  79. name: "independent:source_quotes",
  80. passed:
  81. !!quotes &&
  82. !!source &&
  83. canonicalJSON(Object.keys(quotes).sort()) === canonicalJSON(ids) &&
  84. ids.every((id) => {
  85. const quote = quotes[id]
  86. const line = source.split(/\r?\n/).find((line) => line.startsWith(`[${id}]`) || line.startsWith(`- ${id}:`))
  87. return (
  88. typeof quote === "string" &&
  89. quote.trim().length >= 12 &&
  90. !!line?.includes(quote) &&
  91. !!files["report.md"]?.includes(quote)
  92. )
  93. }),
  94. })
  95. checks.push({
  96. name: "independent:evidence_ids",
  97. passed:
  98. Array.isArray(parsed?.evidence_ids) && canonicalJSON([...parsed.evidence_ids].sort()) === canonicalJSON(ids),
  99. })
  100. }
  101. return {
  102. checks,
  103. passed: checks.filter((check) => check.passed).length,
  104. total: checks.length,
  105. human_review: sample.review.map((criterion) => ({ criterion, result: "not_reviewed" })),
  106. }
  107. }
  108. // The fixed data fixture has three unquoted columns; reject ambiguous formats.
  109. function csv(content?: string) {
  110. if (!content) return undefined
  111. const lines = content.trim().split(/\r?\n/)
  112. if (lines.shift()?.replace(/^\uFEFF/, "") !== "id,group,value") return undefined
  113. const rows = lines.map((line) => line.split(","))
  114. if (rows.some((row) => row.length !== 3)) return undefined
  115. return rows.map(([id, group, value]) => ({ id, group, value }))
  116. }