chaffjs 0.32.0 → 0.34.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 (535) hide show
  1. package/README.md +7 -0
  2. package/dist/api.d.ts +7 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/cli-text.d.ts.map +1 -1
  6. package/dist/cli-text.js +6 -4
  7. package/dist/cli-text.js.map +1 -1
  8. package/dist/commands/grade.d.ts.map +1 -1
  9. package/dist/commands/grade.js +66 -21
  10. package/dist/commands/grade.js.map +1 -1
  11. package/dist/company-kana-spelling.d.ts +6 -1
  12. package/dist/company-kana-spelling.d.ts.map +1 -1
  13. package/dist/company-kana-spelling.js +19 -2
  14. package/dist/company-kana-spelling.js.map +1 -1
  15. package/dist/company-name-ending.d.ts +7 -0
  16. package/dist/company-name-ending.d.ts.map +1 -0
  17. package/dist/company-name-ending.js +44 -0
  18. package/dist/company-name-ending.js.map +1 -0
  19. package/dist/company-names.d.ts +11 -3
  20. package/dist/company-names.d.ts.map +1 -1
  21. package/dist/company-names.js +27 -14
  22. package/dist/company-names.js.map +1 -1
  23. package/dist/cross-run.d.ts.map +1 -1
  24. package/dist/cross-run.js +1 -0
  25. package/dist/cross-run.js.map +1 -1
  26. package/dist/custom/no-rewrite.d.ts +3 -0
  27. package/dist/custom/no-rewrite.d.ts.map +1 -0
  28. package/dist/custom/no-rewrite.js +3 -0
  29. package/dist/custom/no-rewrite.js.map +1 -0
  30. package/dist/custom/parse.d.ts.map +1 -1
  31. package/dist/custom/parse.js +14 -2
  32. package/dist/custom/parse.js.map +1 -1
  33. package/dist/detectors/age-words.d.ts +5 -0
  34. package/dist/detectors/age-words.d.ts.map +1 -0
  35. package/dist/detectors/age-words.js +8 -0
  36. package/dist/detectors/age-words.js.map +1 -0
  37. package/dist/detectors/agreement-slip.d.ts.map +1 -1
  38. package/dist/detectors/agreement-slip.js +4 -0
  39. package/dist/detectors/agreement-slip.js.map +1 -1
  40. package/dist/detectors/annual-pay-mismatch.d.ts.map +1 -1
  41. package/dist/detectors/annual-pay-mismatch.js +2 -12
  42. package/dist/detectors/annual-pay-mismatch.js.map +1 -1
  43. package/dist/detectors/blank-structure.d.ts.map +1 -1
  44. package/dist/detectors/blank-structure.js +4 -3
  45. package/dist/detectors/blank-structure.js.map +1 -1
  46. package/dist/detectors/closing-balance-mismatch.d.ts.map +1 -1
  47. package/dist/detectors/closing-balance-mismatch.js +1 -0
  48. package/dist/detectors/closing-balance-mismatch.js.map +1 -1
  49. package/dist/detectors/code-fences.d.ts +5 -0
  50. package/dist/detectors/code-fences.d.ts.map +1 -1
  51. package/dist/detectors/code-fences.js +9 -0
  52. package/dist/detectors/code-fences.js.map +1 -1
  53. package/dist/detectors/condition-toki.d.ts +16 -0
  54. package/dist/detectors/condition-toki.d.ts.map +1 -0
  55. package/dist/detectors/condition-toki.js +42 -0
  56. package/dist/detectors/condition-toki.js.map +1 -0
  57. package/dist/detectors/conjugated-entry.d.ts +17 -0
  58. package/dist/detectors/conjugated-entry.d.ts.map +1 -0
  59. package/dist/detectors/conjugated-entry.js +50 -0
  60. package/dist/detectors/conjugated-entry.js.map +1 -0
  61. package/dist/detectors/content-morphemes.d.ts +2 -0
  62. package/dist/detectors/content-morphemes.d.ts.map +1 -1
  63. package/dist/detectors/content-morphemes.js +1 -1
  64. package/dist/detectors/content-morphemes.js.map +1 -1
  65. package/dist/detectors/cross-facts.d.ts.map +1 -1
  66. package/dist/detectors/cross-facts.js +2 -1
  67. package/dist/detectors/cross-facts.js.map +1 -1
  68. package/dist/detectors/custom.d.ts.map +1 -1
  69. package/dist/detectors/custom.js +8 -2
  70. package/dist/detectors/custom.js.map +1 -1
  71. package/dist/detectors/document-shape.d.ts.map +1 -1
  72. package/dist/detectors/document-shape.js +6 -0
  73. package/dist/detectors/document-shape.js.map +1 -1
  74. package/dist/detectors/due-date.d.ts.map +1 -1
  75. package/dist/detectors/due-date.js +12 -2
  76. package/dist/detectors/due-date.js.map +1 -1
  77. package/dist/detectors/fact-consistency.d.ts.map +1 -1
  78. package/dist/detectors/fact-consistency.js +12 -13
  79. package/dist/detectors/fact-consistency.js.map +1 -1
  80. package/dist/detectors/form-line.d.ts +9 -0
  81. package/dist/detectors/form-line.d.ts.map +1 -0
  82. package/dist/detectors/form-line.js +36 -0
  83. package/dist/detectors/form-line.js.map +1 -0
  84. package/dist/detectors/formula-amount.d.ts +3 -0
  85. package/dist/detectors/formula-amount.d.ts.map +1 -0
  86. package/dist/detectors/formula-amount.js +31 -0
  87. package/dist/detectors/formula-amount.js.map +1 -0
  88. package/dist/detectors/heading-claim.d.ts +14 -0
  89. package/dist/detectors/heading-claim.d.ts.map +1 -0
  90. package/dist/detectors/heading-claim.js +49 -0
  91. package/dist/detectors/heading-claim.js.map +1 -0
  92. package/dist/detectors/heading-echo.d.ts.map +1 -1
  93. package/dist/detectors/heading-echo.js +29 -1
  94. package/dist/detectors/heading-echo.js.map +1 -1
  95. package/dist/detectors/installment-total-mismatch.d.ts.map +1 -1
  96. package/dist/detectors/installment-total-mismatch.js +28 -19
  97. package/dist/detectors/installment-total-mismatch.js.map +1 -1
  98. package/dist/detectors/list-parallel.d.ts +5 -2
  99. package/dist/detectors/list-parallel.d.ts.map +1 -1
  100. package/dist/detectors/list-parallel.js +45 -7
  101. package/dist/detectors/list-parallel.js.map +1 -1
  102. package/dist/detectors/name-variant.d.ts.map +1 -1
  103. package/dist/detectors/name-variant.js +34 -7
  104. package/dist/detectors/name-variant.js.map +1 -1
  105. package/dist/detectors/number-unit.d.ts +10 -0
  106. package/dist/detectors/number-unit.d.ts.map +1 -1
  107. package/dist/detectors/number-unit.js +28 -8
  108. package/dist/detectors/number-unit.js.map +1 -1
  109. package/dist/detectors/orthographic-variant.d.ts.map +1 -1
  110. package/dist/detectors/orthographic-variant.js +4 -3
  111. package/dist/detectors/orthographic-variant.js.map +1 -1
  112. package/dist/detectors/orthography.d.ts.map +1 -1
  113. package/dist/detectors/orthography.js +16 -7
  114. package/dist/detectors/orthography.js.map +1 -1
  115. package/dist/detectors/per-unit-words.d.ts +7 -0
  116. package/dist/detectors/per-unit-words.d.ts.map +1 -0
  117. package/dist/detectors/per-unit-words.js +17 -0
  118. package/dist/detectors/per-unit-words.js.map +1 -0
  119. package/dist/detectors/period-multiple-mismatch.d.ts.map +1 -1
  120. package/dist/detectors/period-multiple-mismatch.js +2 -12
  121. package/dist/detectors/period-multiple-mismatch.js.map +1 -1
  122. package/dist/detectors/reference-number-variant.d.ts.map +1 -1
  123. package/dist/detectors/reference-number-variant.js +21 -3
  124. package/dist/detectors/reference-number-variant.js.map +1 -1
  125. package/dist/detectors/registry/condition-toki.d.ts +3 -0
  126. package/dist/detectors/registry/condition-toki.d.ts.map +1 -0
  127. package/dist/detectors/registry/condition-toki.js +3 -0
  128. package/dist/detectors/registry/condition-toki.js.map +1 -0
  129. package/dist/detectors/registry/formula-amount.d.ts +3 -0
  130. package/dist/detectors/registry/formula-amount.d.ts.map +1 -0
  131. package/dist/detectors/registry/formula-amount.js +3 -0
  132. package/dist/detectors/registry/formula-amount.js.map +1 -0
  133. package/dist/detectors/rent-multiple-mismatch.d.ts.map +1 -1
  134. package/dist/detectors/rent-multiple-mismatch.js +2 -12
  135. package/dist/detectors/rent-multiple-mismatch.js.map +1 -1
  136. package/dist/detectors/row-sentence-amounts.d.ts.map +1 -1
  137. package/dist/detectors/row-sentence-amounts.js +5 -10
  138. package/dist/detectors/row-sentence-amounts.js.map +1 -1
  139. package/dist/detectors/stated-amounts.d.ts +4 -0
  140. package/dist/detectors/stated-amounts.d.ts.map +1 -0
  141. package/dist/detectors/stated-amounts.js +12 -0
  142. package/dist/detectors/stated-amounts.js.map +1 -0
  143. package/dist/detectors/step-time-sum.d.ts.map +1 -1
  144. package/dist/detectors/step-time-sum.js +2 -1
  145. package/dist/detectors/step-time-sum.js.map +1 -1
  146. package/dist/detectors/structure-tree.d.ts +1 -1
  147. package/dist/detectors/structure-tree.d.ts.map +1 -1
  148. package/dist/detectors/structure-tree.js +7 -2
  149. package/dist/detectors/structure-tree.js.map +1 -1
  150. package/dist/detectors/team.d.ts.map +1 -1
  151. package/dist/detectors/team.js +10 -6
  152. package/dist/detectors/team.js.map +1 -1
  153. package/dist/detectors/time-order.d.ts.map +1 -1
  154. package/dist/detectors/time-order.js +17 -13
  155. package/dist/detectors/time-order.js.map +1 -1
  156. package/dist/detectors/unknown-word.d.ts +4 -1
  157. package/dist/detectors/unknown-word.d.ts.map +1 -1
  158. package/dist/detectors/unknown-word.js +18 -2
  159. package/dist/detectors/unknown-word.js.map +1 -1
  160. package/dist/detectors/use-interval.d.ts +1 -0
  161. package/dist/detectors/use-interval.d.ts.map +1 -1
  162. package/dist/detectors/use-interval.js +15 -10
  163. package/dist/detectors/use-interval.js.map +1 -1
  164. package/dist/detectors/use-maximum.d.ts.map +1 -1
  165. package/dist/detectors/use-maximum.js +3 -1
  166. package/dist/detectors/use-maximum.js.map +1 -1
  167. package/dist/eval.js +3 -3
  168. package/dist/eval.js.map +1 -1
  169. package/dist/extension/document-view.d.ts +2 -2
  170. package/dist/extension/document-view.d.ts.map +1 -1
  171. package/dist/extension/document-view.js +17 -8
  172. package/dist/extension/document-view.js.map +1 -1
  173. package/dist/extension/entry-genres.d.ts +5 -0
  174. package/dist/extension/entry-genres.d.ts.map +1 -0
  175. package/dist/extension/entry-genres.js +13 -0
  176. package/dist/extension/entry-genres.js.map +1 -0
  177. package/dist/extension/module-detector.d.ts.map +1 -1
  178. package/dist/extension/module-detector.js +4 -4
  179. package/dist/extension/module-detector.js.map +1 -1
  180. package/dist/extension/plugin-parse.d.ts +1 -1
  181. package/dist/extension/plugin-parse.d.ts.map +1 -1
  182. package/dist/extension/plugin-parse.js +24 -7
  183. package/dist/extension/plugin-parse.js.map +1 -1
  184. package/dist/extension/problem-text.d.ts.map +1 -1
  185. package/dist/extension/problem-text.js +2 -0
  186. package/dist/extension/problem-text.js.map +1 -1
  187. package/dist/facts/age-values.d.ts.map +1 -1
  188. package/dist/facts/age-values.js +20 -2
  189. package/dist/facts/age-values.js.map +1 -1
  190. package/dist/facts/column-units.d.ts +31 -0
  191. package/dist/facts/column-units.d.ts.map +1 -0
  192. package/dist/facts/column-units.js +37 -0
  193. package/dist/facts/column-units.js.map +1 -0
  194. package/dist/facts/fact-values.d.ts +4 -0
  195. package/dist/facts/fact-values.d.ts.map +1 -1
  196. package/dist/facts/fact-values.js +3 -1
  197. package/dist/facts/fact-values.js.map +1 -1
  198. package/dist/facts/labelled-facts.d.ts +10 -0
  199. package/dist/facts/labelled-facts.d.ts.map +1 -1
  200. package/dist/facts/labelled-facts.js +6 -1
  201. package/dist/facts/labelled-facts.js.map +1 -1
  202. package/dist/facts/per-unit.d.ts +36 -0
  203. package/dist/facts/per-unit.d.ts.map +1 -0
  204. package/dist/facts/per-unit.js +56 -0
  205. package/dist/facts/per-unit.js.map +1 -0
  206. package/dist/facts/row-sentence-amounts.d.ts +3 -12
  207. package/dist/facts/row-sentence-amounts.d.ts.map +1 -1
  208. package/dist/facts/row-sentence-amounts.js +1 -15
  209. package/dist/facts/row-sentence-amounts.js.map +1 -1
  210. package/dist/facts/table-facts.d.ts +1 -1
  211. package/dist/facts/table-facts.d.ts.map +1 -1
  212. package/dist/facts/table-facts.js +13 -9
  213. package/dist/facts/table-facts.js.map +1 -1
  214. package/dist/facts/term-values.d.ts +13 -1
  215. package/dist/facts/term-values.d.ts.map +1 -1
  216. package/dist/facts/term-values.js +71 -12
  217. package/dist/facts/term-values.js.map +1 -1
  218. package/dist/fix-plan/text.js +2 -2
  219. package/dist/fix-plan/text.js.map +1 -1
  220. package/dist/grade/baseline.d.ts +4 -2
  221. package/dist/grade/baseline.d.ts.map +1 -1
  222. package/dist/grade/baseline.js +13 -11
  223. package/dist/grade/baseline.js.map +1 -1
  224. package/dist/grade/expect-read.d.ts +16 -0
  225. package/dist/grade/expect-read.d.ts.map +1 -0
  226. package/dist/grade/expect-read.js +52 -0
  227. package/dist/grade/expect-read.js.map +1 -0
  228. package/dist/grade/expect.d.ts +50 -0
  229. package/dist/grade/expect.d.ts.map +1 -0
  230. package/dist/grade/expect.js +53 -0
  231. package/dist/grade/expect.js.map +1 -0
  232. package/dist/grade/grade-item.d.ts +1 -1
  233. package/dist/grade/grade-item.d.ts.map +1 -1
  234. package/dist/grade/grade-item.js +13 -3
  235. package/dist/grade/grade-item.js.map +1 -1
  236. package/dist/grade/item.d.ts +9 -1
  237. package/dist/grade/item.d.ts.map +1 -1
  238. package/dist/grade/item.js +32 -7
  239. package/dist/grade/item.js.map +1 -1
  240. package/dist/grade/result.d.ts +5 -0
  241. package/dist/grade/result.d.ts.map +1 -1
  242. package/dist/grade/results-read.d.ts.map +1 -1
  243. package/dist/grade/results-read.js +4 -2
  244. package/dist/grade/results-read.js.map +1 -1
  245. package/dist/grade/rubric-file.d.ts +23 -0
  246. package/dist/grade/rubric-file.d.ts.map +1 -0
  247. package/dist/grade/rubric-file.js +35 -0
  248. package/dist/grade/rubric-file.js.map +1 -0
  249. package/dist/grade/rubric.d.ts +2 -0
  250. package/dist/grade/rubric.d.ts.map +1 -1
  251. package/dist/grade/rubric.js +75 -47
  252. package/dist/grade/rubric.js.map +1 -1
  253. package/dist/grade/setup.d.ts +2 -1
  254. package/dist/grade/setup.d.ts.map +1 -1
  255. package/dist/grade/setup.js +2 -2
  256. package/dist/grade/setup.js.map +1 -1
  257. package/dist/grade/stamp.d.ts +2 -0
  258. package/dist/grade/stamp.d.ts.map +1 -1
  259. package/dist/grade/stamp.js +8 -2
  260. package/dist/grade/stamp.js.map +1 -1
  261. package/dist/grade/task-rubric.d.ts +16 -0
  262. package/dist/grade/task-rubric.d.ts.map +1 -0
  263. package/dist/grade/task-rubric.js +34 -0
  264. package/dist/grade/task-rubric.js.map +1 -0
  265. package/dist/grade/text.d.ts +5 -1
  266. package/dist/grade/text.d.ts.map +1 -1
  267. package/dist/grade/text.js +16 -4
  268. package/dist/grade/text.js.map +1 -1
  269. package/dist/lexicon-genres.d.ts +7 -0
  270. package/dist/lexicon-genres.d.ts.map +1 -0
  271. package/dist/lexicon-genres.js +8 -0
  272. package/dist/lexicon-genres.js.map +1 -0
  273. package/dist/name-char-reading.d.ts +5 -0
  274. package/dist/name-char-reading.d.ts.map +1 -1
  275. package/dist/name-char-reading.js +15 -0
  276. package/dist/name-char-reading.js.map +1 -1
  277. package/dist/name-end-letter.d.ts +19 -0
  278. package/dist/name-end-letter.d.ts.map +1 -0
  279. package/dist/name-end-letter.js +37 -0
  280. package/dist/name-end-letter.js.map +1 -0
  281. package/dist/name-neighbours.d.ts.map +1 -1
  282. package/dist/name-neighbours.js +2 -1
  283. package/dist/name-neighbours.js.map +1 -1
  284. package/dist/name-separator.d.ts +6 -0
  285. package/dist/name-separator.d.ts.map +1 -1
  286. package/dist/name-separator.js +29 -0
  287. package/dist/name-separator.js.map +1 -1
  288. package/dist/name-variants.d.ts +6 -3
  289. package/dist/name-variants.d.ts.map +1 -1
  290. package/dist/name-variants.js +37 -10
  291. package/dist/name-variants.js.map +1 -1
  292. package/dist/number-name.d.ts.map +1 -1
  293. package/dist/number-name.js +11 -2
  294. package/dist/number-name.js.map +1 -1
  295. package/dist/plugin.d.ts +6 -0
  296. package/dist/plugin.d.ts.map +1 -1
  297. package/dist/profile/cue-lines.d.ts +6 -0
  298. package/dist/profile/cue-lines.d.ts.map +1 -0
  299. package/dist/profile/cue-lines.js +13 -0
  300. package/dist/profile/cue-lines.js.map +1 -0
  301. package/dist/profile/parse.d.ts +1 -1
  302. package/dist/profile/parse.d.ts.map +1 -1
  303. package/dist/profile/parse.js +8 -2
  304. package/dist/profile/parse.js.map +1 -1
  305. package/dist/reference-numbers.d.ts +2 -0
  306. package/dist/reference-numbers.d.ts.map +1 -1
  307. package/dist/reference-numbers.js +1 -1
  308. package/dist/reference-numbers.js.map +1 -1
  309. package/dist/run.d.ts.map +1 -1
  310. package/dist/run.js +1 -0
  311. package/dist/run.js.map +1 -1
  312. package/dist/spaced-person-name.d.ts.map +1 -1
  313. package/dist/spaced-person-name.js +26 -1
  314. package/dist/spaced-person-name.js.map +1 -1
  315. package/dist/spacing-code.d.ts.map +1 -1
  316. package/dist/spacing-code.js +10 -1
  317. package/dist/spacing-code.js.map +1 -1
  318. package/dist/spacing-minority.d.ts +7 -0
  319. package/dist/spacing-minority.d.ts.map +1 -1
  320. package/dist/spacing-minority.js +14 -0
  321. package/dist/spacing-minority.js.map +1 -1
  322. package/dist/structure/closing-balance.d.ts +9 -0
  323. package/dist/structure/closing-balance.d.ts.map +1 -1
  324. package/dist/structure/closing-balance.js +34 -7
  325. package/dist/structure/closing-balance.js.map +1 -1
  326. package/dist/structure/date-range.d.ts +3 -2
  327. package/dist/structure/date-range.d.ts.map +1 -1
  328. package/dist/structure/date-range.js +4 -4
  329. package/dist/structure/date-range.js.map +1 -1
  330. package/dist/structure/definition-use.d.ts.map +1 -1
  331. package/dist/structure/definition-use.js +20 -1
  332. package/dist/structure/definition-use.js.map +1 -1
  333. package/dist/structure/document-context.d.ts +2 -0
  334. package/dist/structure/document-context.d.ts.map +1 -0
  335. package/dist/structure/document-context.js +42 -0
  336. package/dist/structure/document-context.js.map +1 -0
  337. package/dist/structure/fenced-lines.d.ts +7 -0
  338. package/dist/structure/fenced-lines.d.ts.map +1 -0
  339. package/dist/structure/fenced-lines.js +27 -0
  340. package/dist/structure/fenced-lines.js.map +1 -0
  341. package/dist/structure/formula-amount.d.ts +38 -0
  342. package/dist/structure/formula-amount.d.ts.map +1 -0
  343. package/dist/structure/formula-amount.js +187 -0
  344. package/dist/structure/formula-amount.js.map +1 -0
  345. package/dist/structure/installment-fee.d.ts +52 -0
  346. package/dist/structure/installment-fee.d.ts.map +1 -0
  347. package/dist/structure/installment-fee.js +110 -0
  348. package/dist/structure/installment-fee.js.map +1 -0
  349. package/dist/structure/installment-total.d.ts +7 -0
  350. package/dist/structure/installment-total.d.ts.map +1 -1
  351. package/dist/structure/installment-total.js +6 -6
  352. package/dist/structure/installment-total.js.map +1 -1
  353. package/dist/structure/labelled-date-order.d.ts +31 -9
  354. package/dist/structure/labelled-date-order.d.ts.map +1 -1
  355. package/dist/structure/labelled-date-order.js +60 -15
  356. package/dist/structure/labelled-date-order.js.map +1 -1
  357. package/dist/structure/labelled-order.d.ts +7 -1
  358. package/dist/structure/labelled-order.d.ts.map +1 -1
  359. package/dist/structure/labelled-order.js +10 -3
  360. package/dist/structure/labelled-order.js.map +1 -1
  361. package/dist/structure/line-amount-label.d.ts.map +1 -1
  362. package/dist/structure/line-amount-label.js +2 -1
  363. package/dist/structure/line-amount-label.js.map +1 -1
  364. package/dist/structure/line-amount.d.ts +0 -5
  365. package/dist/structure/line-amount.d.ts.map +1 -1
  366. package/dist/structure/line-amount.js +1 -10
  367. package/dist/structure/line-amount.js.map +1 -1
  368. package/dist/structure/prose-total.d.ts +8 -0
  369. package/dist/structure/prose-total.d.ts.map +1 -1
  370. package/dist/structure/prose-total.js +70 -2
  371. package/dist/structure/prose-total.js.map +1 -1
  372. package/dist/structure/runs.d.ts.map +1 -1
  373. package/dist/structure/runs.js +9 -1
  374. package/dist/structure/runs.js.map +1 -1
  375. package/dist/structure/signed-date-line.d.ts +15 -0
  376. package/dist/structure/signed-date-line.d.ts.map +1 -0
  377. package/dist/structure/signed-date-line.js +68 -0
  378. package/dist/structure/signed-date-line.js.map +1 -0
  379. package/dist/structure/start-end-range.d.ts +30 -0
  380. package/dist/structure/start-end-range.d.ts.map +1 -0
  381. package/dist/structure/start-end-range.js +79 -0
  382. package/dist/structure/start-end-range.js.map +1 -0
  383. package/dist/structure/tax.d.ts.map +1 -1
  384. package/dist/structure/tax.js +5 -3
  385. package/dist/structure/tax.js.map +1 -1
  386. package/dist/structure/total-line.d.ts.map +1 -1
  387. package/dist/structure/total-line.js +10 -6
  388. package/dist/structure/total-line.js.map +1 -1
  389. package/dist/structure/total.d.ts +2 -0
  390. package/dist/structure/total.d.ts.map +1 -1
  391. package/dist/structure/total.js +34 -3
  392. package/dist/structure/total.js.map +1 -1
  393. package/dist/subject-codes.d.ts +21 -0
  394. package/dist/subject-codes.d.ts.map +1 -0
  395. package/dist/subject-codes.js +79 -0
  396. package/dist/subject-codes.js.map +1 -0
  397. package/dist/unlabelled-references.d.ts +6 -2
  398. package/dist/unlabelled-references.d.ts.map +1 -1
  399. package/dist/unlabelled-references.js +11 -10
  400. package/dist/unlabelled-references.js.map +1 -1
  401. package/genres.yaml +17 -0
  402. package/package.json +4 -4
  403. package/profiles/listing.yaml +1 -1
  404. package/profiles/property-sale.yaml +21 -0
  405. package/rules/closing-balance-mismatch.yaml +3 -3
  406. package/rules/date-range-reversed.yaml +4 -4
  407. package/rules/due-before-issue.yaml +9 -5
  408. package/rules/fact-conflict.yaml +2 -2
  409. package/rules/formula-amount-mismatch.yaml +67 -0
  410. package/rules/heading-echo.yaml +2 -2
  411. package/rules/installment-total-mismatch.yaml +9 -4
  412. package/rules/internal-jargon.yaml +2 -2
  413. package/rules/latin-spacing.yaml +4 -4
  414. package/rules/list-item-form-mix.yaml +1 -1
  415. package/rules/name-variant.yaml +5 -2
  416. package/rules/no-lead.yaml +2 -2
  417. package/rules/number-without-unit.yaml +2 -2
  418. package/rules/orthographic-variant.yaml +3 -3
  419. package/rules/reference-number-variant.yaml +3 -3
  420. package/rules/toki-as-condition.yaml +71 -0
  421. package/rules/total-mismatch.yaml +4 -4
  422. package/rules/unknown-word.yaml +2 -2
  423. package/rules/use-before-definition.yaml +2 -2
  424. package/src/api.ts +11 -2
  425. package/src/cli-text.ts +6 -4
  426. package/src/commands/grade.ts +75 -23
  427. package/src/company-kana-spelling.ts +22 -2
  428. package/src/company-name-ending.ts +50 -0
  429. package/src/company-names.ts +52 -17
  430. package/src/cross-run.ts +1 -0
  431. package/src/custom/no-rewrite.ts +2 -0
  432. package/src/custom/parse.ts +18 -2
  433. package/src/detectors/age-words.ts +12 -0
  434. package/src/detectors/agreement-slip.ts +3 -0
  435. package/src/detectors/annual-pay-mismatch.ts +3 -18
  436. package/src/detectors/blank-structure.ts +4 -3
  437. package/src/detectors/closing-balance-mismatch.ts +1 -0
  438. package/src/detectors/code-fences.ts +10 -0
  439. package/src/detectors/condition-toki.ts +60 -0
  440. package/src/detectors/conjugated-entry.ts +53 -0
  441. package/src/detectors/content-morphemes.ts +2 -1
  442. package/src/detectors/cross-facts.ts +2 -1
  443. package/src/detectors/custom.ts +9 -2
  444. package/src/detectors/document-shape.ts +8 -0
  445. package/src/detectors/due-date.ts +17 -7
  446. package/src/detectors/fact-consistency.ts +13 -17
  447. package/src/detectors/form-line.ts +47 -0
  448. package/src/detectors/formula-amount.ts +43 -0
  449. package/src/detectors/heading-claim.ts +76 -0
  450. package/src/detectors/heading-echo.ts +32 -1
  451. package/src/detectors/installment-total-mismatch.ts +35 -24
  452. package/src/detectors/list-parallel.ts +46 -7
  453. package/src/detectors/name-variant.ts +45 -8
  454. package/src/detectors/number-unit.ts +35 -9
  455. package/src/detectors/orthographic-variant.ts +5 -3
  456. package/src/detectors/orthography.ts +17 -8
  457. package/src/detectors/per-unit-words.ts +24 -0
  458. package/src/detectors/period-multiple-mismatch.ts +2 -18
  459. package/src/detectors/reference-number-variant.ts +27 -5
  460. package/src/detectors/registry/condition-toki.ts +4 -0
  461. package/src/detectors/registry/formula-amount.ts +4 -0
  462. package/src/detectors/rent-multiple-mismatch.ts +2 -18
  463. package/src/detectors/row-sentence-amounts.ts +5 -20
  464. package/src/detectors/stated-amounts.ts +20 -0
  465. package/src/detectors/step-time-sum.ts +2 -1
  466. package/src/detectors/structure-tree.ts +7 -2
  467. package/src/detectors/team.ts +13 -7
  468. package/src/detectors/time-order.ts +6 -3
  469. package/src/detectors/unknown-word.ts +20 -2
  470. package/src/detectors/use-interval.ts +8 -4
  471. package/src/detectors/use-maximum.ts +3 -1
  472. package/src/eval.ts +3 -3
  473. package/src/extension/document-view.ts +18 -9
  474. package/src/extension/entry-genres.ts +15 -0
  475. package/src/extension/module-detector.ts +9 -4
  476. package/src/extension/plugin-parse.ts +29 -7
  477. package/src/extension/problem-text.ts +4 -0
  478. package/src/facts/age-values.ts +24 -2
  479. package/src/facts/column-units.ts +67 -0
  480. package/src/facts/fact-values.ts +11 -1
  481. package/src/facts/labelled-facts.ts +16 -2
  482. package/src/facts/per-unit.ts +105 -0
  483. package/src/facts/row-sentence-amounts.ts +4 -26
  484. package/src/facts/table-facts.ts +7 -4
  485. package/src/facts/term-values.ts +94 -13
  486. package/src/fix-plan/text.ts +2 -2
  487. package/src/grade/baseline.ts +20 -12
  488. package/src/grade/expect-read.ts +65 -0
  489. package/src/grade/expect.ts +74 -0
  490. package/src/grade/grade-item.ts +14 -3
  491. package/src/grade/item.ts +48 -8
  492. package/src/grade/result.ts +5 -0
  493. package/src/grade/results-read.ts +7 -2
  494. package/src/grade/rubric-file.ts +46 -0
  495. package/src/grade/rubric.ts +77 -47
  496. package/src/grade/setup.ts +3 -3
  497. package/src/grade/stamp.ts +10 -2
  498. package/src/grade/task-rubric.ts +39 -0
  499. package/src/grade/text.ts +27 -5
  500. package/src/lexicon-genres.ts +11 -0
  501. package/src/name-char-reading.ts +16 -0
  502. package/src/name-end-letter.ts +52 -0
  503. package/src/name-neighbours.ts +2 -1
  504. package/src/name-separator.ts +32 -0
  505. package/src/name-variants.ts +54 -13
  506. package/src/number-name.ts +12 -3
  507. package/src/plugin.ts +6 -0
  508. package/src/profile/cue-lines.ts +14 -0
  509. package/src/profile/parse.ts +11 -2
  510. package/src/reference-numbers.ts +1 -1
  511. package/src/run.ts +1 -0
  512. package/src/spaced-person-name.ts +27 -2
  513. package/src/spacing-code.ts +13 -2
  514. package/src/spacing-minority.ts +17 -0
  515. package/src/structure/closing-balance.ts +46 -8
  516. package/src/structure/date-range.ts +4 -3
  517. package/src/structure/definition-use.ts +24 -1
  518. package/src/structure/document-context.ts +46 -0
  519. package/src/structure/fenced-lines.ts +28 -0
  520. package/src/structure/formula-amount.ts +236 -0
  521. package/src/structure/installment-fee.ts +201 -0
  522. package/src/structure/installment-total.ts +6 -6
  523. package/src/structure/labelled-date-order.ts +105 -21
  524. package/src/structure/labelled-order.ts +21 -10
  525. package/src/structure/line-amount-label.ts +2 -1
  526. package/src/structure/line-amount.ts +1 -11
  527. package/src/structure/prose-total.ts +81 -2
  528. package/src/structure/runs.ts +10 -1
  529. package/src/structure/signed-date-line.ts +78 -0
  530. package/src/structure/start-end-range.ts +120 -0
  531. package/src/structure/tax.ts +6 -3
  532. package/src/structure/total-line.ts +12 -6
  533. package/src/structure/total.ts +42 -3
  534. package/src/subject-codes.ts +105 -0
  535. package/src/unlabelled-references.ts +24 -16
@@ -2,6 +2,7 @@ import { sameValue, type FactValue } from "./fact-values.ts";
2
2
  import type { Fact } from "./labelled-facts.ts";
3
3
  import type { TermWord } from "./document-terms.ts";
4
4
  import { withoutEdgeMarks } from "./trim-marks.ts";
5
+ import { qualifiedKeyOf, type QualifierWords } from "./qualified-labels.ts";
5
6
 
6
7
  /**
7
8
  * 文書全体で一つの値を持つ項目(保証期間、warranty period)を主語にした文の期間。名前付きの値の読み(labelled-facts)は、区切りの
@@ -24,6 +25,14 @@ export type TermValueWords = {
24
25
  /** 起算の句の印(から は値の前の句の終わり、from は値の後ろの句の頭)。 */
25
26
  readonly starts: readonly PlacedWord[];
26
27
  readonly valueEnds: readonly string[];
28
+ /** 名前のすぐ後ろで、その値を何に使ったかを言う句の頭(used for the)。後ろの句ごと落としても同じ項目。 */
29
+ readonly restating?: readonly string[];
30
+ /** 名前と値を囲む型(を*として)。* が値。型の値は、期間でなくても読む。 */
31
+ readonly frames?: readonly string[];
32
+ /** 文の値に条件を付ける語(場合、if)。型の前の句にあれば読まない。 */
33
+ readonly conditions?: readonly string[];
34
+ /** 名前に条件を付ける型(*の、for *)。 */
35
+ readonly qualifiers?: QualifierWords;
27
36
  };
28
37
 
29
38
  const folded = (text: string): string => text.normalize("NFKC").toLowerCase().replace(/\s+/gu, " ").trim();
@@ -73,12 +82,35 @@ const withoutSelf = (key: string, words: TermValueWords): string => {
73
82
  return (self.position === "before" ? key.slice(length) : key.slice(0, key.length - length)).trim();
74
83
  };
75
84
 
76
- /** 名前が語彙表の項目なら、その語の形。 */
77
- export const termKeyOf = (label: string, words: TermValueWords): string | undefined => {
85
+ /** 語彙表の項目の形。語彙表の語は条件の付いた名前の形(扶養親族の数 は 数 (扶養親族))なので、名前もその形にして比べる。 */
86
+ const termOf = (key: string, words: TermValueWords): string | undefined => {
87
+ const qualified = words.qualifiers === undefined ? key : qualifiedKeyOf(key, words.qualifiers);
88
+ return words.terms.map((word) => folded(word.pattern)).find((pattern) => pattern === key || pattern === qualified);
89
+ };
90
+
91
+ /** 何に使ったかを言う句から後ろを落とした名前(number of dependants used for the allowance の number of dependants)。 */
92
+ const withoutRestating = (key: string, words: TermValueWords): string | undefined =>
93
+ (words.restating ?? [])
94
+ .flatMap((word) => {
95
+ const mark = ` ${folded(word)}`;
96
+ const at = key.indexOf(mark);
97
+ const next = key.charAt(at + mark.length);
98
+ return at > 0 && (next === "" || next === " ") ? [key.slice(0, at)] : [];
99
+ })
100
+ .find((head) => termOf(head, words) !== undefined);
101
+
102
+ /** 名前が語彙表の項目なら、その語の形。restated: 何に使ったかを言う句を落として、項目になった。 */
103
+ const termKeyWith = (label: string, words: TermValueWords): { readonly key: string; readonly restated: boolean } | undefined => {
78
104
  const key = withoutSelf(withoutDeterminer(folded(withoutEdgeMarks(label)), words), words);
79
- return words.terms.map((word) => folded(word.pattern)).find((pattern) => pattern === key);
105
+ const term = termOf(key, words);
106
+ if (term !== undefined) return { key: term, restated: false };
107
+ const restated = withoutRestating(key, words);
108
+ return restated === undefined ? undefined : { key: restated, restated: true };
80
109
  };
81
110
 
111
+ /** 名前が語彙表の項目なら、その語の形。 */
112
+ export const termKeyOf = (label: string, words: TermValueWords): string | undefined => termKeyWith(label, words)?.key;
113
+
82
114
  type Span = { readonly start: number; readonly end: number };
83
115
 
84
116
  const valuesIn = (values: readonly FactValue[], span: Span): FactValue[] => values.filter((value) => span.start <= value.start && value.end <= span.end);
@@ -91,7 +123,7 @@ const separatorsIn = (text: string, separator: string): number[] =>
91
123
  .map((match) => match.index)
92
124
  .filter((index) => !isLatin(separator) || (text.charAt(index - 1) === " " && [" ", ""].includes(text.charAt(index + separator.length))));
93
125
 
94
- type Subject = { readonly key: string; readonly label: string; readonly lead: Span | undefined; readonly restStart: number };
126
+ type Subject = { readonly key: string; readonly label: string; readonly lead: Span | undefined; readonly restStart: number; readonly restated: boolean };
95
127
 
96
128
  /** 文の頭から区切りまでの主語。読点の前は条件の句。 */
97
129
  const subjectOf = (source: string, from: number, to: number, words: TermValueWords): Subject | undefined => {
@@ -101,9 +133,10 @@ const subjectOf = (source: string, from: number, to: number, words: TermValueWor
101
133
  const head = clause.slice(0, index);
102
134
  const comma = [...head.matchAll(CLAUSE_COMMAS)].at(-1)?.index;
103
135
  const label = withoutEdgeMarks(comma === undefined ? head : head.slice(comma + 1));
104
- const key = termKeyOf(label, words);
105
- if (key === undefined) continue;
106
- return { key, label, lead: comma === undefined ? undefined : { start: from, end: from + comma }, restStart: from + index + separator.length };
136
+ const term = termKeyWith(label, words);
137
+ if (term === undefined) continue;
138
+ const lead = comma === undefined ? undefined : { start: from, end: from + comma };
139
+ return { ...term, label, lead, restStart: from + index + separator.length };
107
140
  }
108
141
  }
109
142
  return undefined;
@@ -170,10 +203,10 @@ const isWorkedExample = (lead: Span, starts: readonly Span[], values: readonly F
170
203
  return starts.some((span) => valuesIn(values, span).some((value) => value.kind === "date" && sameValue(date, value)));
171
204
  };
172
205
 
173
- /** 主語と区切りの後ろの期間(保証期間は、お買い上げ日から24か月です)。 */
174
- const subjectFact = (source: string, value: FactValue, values: readonly FactValue[], words: TermValueWords): Fact | undefined => {
206
+ /** 主語と区切りの後ろの期間(保証期間は、お買い上げ日から24か月です)。restatedOnly: 何に使ったかを言う句の付いた主語だけ。 */
207
+ const subjectFact = (source: string, value: FactValue, values: readonly FactValue[], words: TermValueWords, restatedOnly = false): Fact | undefined => {
175
208
  const subject = subjectOf(source, clauseStartOf(source, value.start), value.start, words);
176
- if (subject === undefined) return undefined;
209
+ if (subject === undefined || (restatedOnly && !subject.restated)) return undefined;
177
210
  const before = startBeforeValue(source, { start: subject.restStart, end: value.start }, values, words);
178
211
  if (before === undefined) return undefined;
179
212
  const after = startAfterValue(source, value.end, values, words);
@@ -226,11 +259,59 @@ const unitNamedFact = (source: string, value: FactValue, words: TermValueWords):
226
259
 
227
260
  const DURATION_UNITS = new Set(["day", "week", "month", "year"]);
228
261
 
229
- /** 語彙表の項目を主語にした文の、期間の値。values は文書の中の順の値(日付を含む)。 */
262
+ /** 型の前と後ろの字(を*として の を と として)。 */
263
+ const frameParts = (frame: string): readonly [string, string] | undefined => {
264
+ const at = frame.indexOf("*");
265
+ return at <= 0 || at === frame.length - 1 ? undefined : [frame.slice(0, at), frame.slice(at + 1)];
266
+ };
267
+
268
+ /** 型の前の句(扶養控除は、)が、値も条件の語も含まないか。条件の句(登録された場合、)の後ろの値は、その場合の値。 */
269
+ const isPlainLead = (source: string, lead: Span, values: readonly FactValue[], words: TermValueWords): boolean => {
270
+ const written = folded(source.slice(lead.start, lead.end));
271
+ return valuesIn(values, lead).length === 0 && !(words.conditions ?? []).some((word) => written.includes(folded(word)));
272
+ };
273
+
274
+ /** 型の名前(扶養親族の数を2人として の 扶養親族の数)。名前は読点の後ろから。 */
275
+ const framedFact = (
276
+ source: string,
277
+ value: FactValue,
278
+ values: readonly FactValue[],
279
+ words: TermValueWords,
280
+ [open, close]: readonly [string, string],
281
+ ): Fact | undefined => {
282
+ const from = clauseStartOf(source, value.start);
283
+ const labelEnd = value.start - open.length;
284
+ if (labelEnd <= from || source.slice(labelEnd, value.start) !== open || !source.startsWith(close, value.end)) return undefined;
285
+ const head = source.slice(from, labelEnd);
286
+ const comma = [...head.matchAll(CLAUSE_COMMAS)].at(-1)?.index ?? -1;
287
+ const label = withoutEdgeMarks(head.slice(comma + 1));
288
+ const key = termKeyOf(label, words);
289
+ if (key === undefined || !isPlainLead(source, { start: from, end: from + Math.max(comma, 0) }, values, words)) return undefined;
290
+ return { label, key, value };
291
+ };
292
+
293
+ /** 名前と値を囲む型の値(扶養控除は、扶養親族の数を2人として計算しています)。 */
294
+ const frameFact = (source: string, value: FactValue, values: readonly FactValue[], words: TermValueWords): Fact | undefined =>
295
+ (words.frames ?? [])
296
+ .flatMap((frame) => {
297
+ const parts = frameParts(frame);
298
+ const fact = parts === undefined ? undefined : framedFact(source, value, values, words, parts);
299
+ return fact === undefined ? [] : [fact];
300
+ })
301
+ .at(0);
302
+
303
+ const durationFact = (source: string, value: FactValue, values: readonly FactValue[], words: TermValueWords): Fact | undefined =>
304
+ bracketFact(source, value, words) ?? subjectFact(source, value, values, words) ?? unitNamedFact(source, value, words);
305
+
306
+ /**
307
+ * 語彙表の項目を主語にした文の、期間の値。期間でない数は、何に使ったかを言う句の付いた主語(the number of dependants used for the
308
+ * allowance is 2)か、名前と値を囲む型(扶養親族の数を2人として)のときだけ。values は文書の中の順の値(日付を含む)。
309
+ */
230
310
  export const termValues = (source: string, values: readonly FactValue[], words: TermValueWords): Fact[] =>
231
311
  values
232
- .filter((value) => value.kind === "quantity" && DURATION_UNITS.has(value.unit))
312
+ .filter((value) => value.kind === "quantity")
233
313
  .flatMap((value) => {
234
- const fact = bracketFact(source, value, words) ?? subjectFact(source, value, values, words) ?? unitNamedFact(source, value, words);
314
+ const read = DURATION_UNITS.has(value.unit) ? durationFact(source, value, values, words) : subjectFact(source, value, values, words, true);
315
+ const fact = read ?? frameFact(source, value, values, words);
235
316
  return fact === undefined ? [] : [fact];
236
317
  });
@@ -109,7 +109,7 @@ export const FIX_PLAN_TEXT: Texts<FixPlanText> = {
109
109
  spots: "見つけた箇所",
110
110
  at: (line) => `${String(line)} 行目`,
111
111
  notRunHeading: "動かなかったルール",
112
- notExperimental: "試験中のルールは動かしていません。AIっぽさのルールの多くは試験中です。--experimental を付けると見ます。",
112
+ notExperimental: "試験中のルールは動かしていません。--experimental を付けると見ます。",
113
113
  checksHeading: "直したあとの確かめ",
114
114
  saveAs: (path) => `書き直したものを ${path} に保存して、次を実行します。`,
115
115
  checksNote:
@@ -174,7 +174,7 @@ export const FIX_PLAN_TEXT: Texts<FixPlanText> = {
174
174
  spots: "Spots",
175
175
  at: (line) => `line ${String(line)}`,
176
176
  notRunHeading: "Rules that did not run",
177
- notExperimental: "The experimental rules did not run, and most rules for generated text are experimental. Add --experimental to run them.",
177
+ notExperimental: "The experimental rules did not run. Add --experimental to run them.",
178
178
  checksHeading: "Check after rewriting",
179
179
  saveAs: (path) => `Save the rewrite as ${path} and run:`,
180
180
  checksNote:
@@ -60,13 +60,22 @@ type Pair = { readonly before: GradeResult; readonly after: GradeResult };
60
60
 
61
61
  const countIn = (result: GradeResult, rule: string): number => result.findings.filter((finding) => finding.rule === rule).length;
62
62
 
63
- const movementsOf = (pairs: readonly Pair[], rubricRules: ReadonlySet<string>): RuleMovement[] =>
63
+ /** The rules the rubric names for one output: with rubric tasks, its task's rules over the top level's. */
64
+ export type RubricRulesOf = (result: GradeResult) => ReadonlySet<string>;
65
+
66
+ /** A rule's movement, and the outputs it rose in whose own rubric names it: only those are a regression. */
67
+ type Moved = { readonly movement: RuleMovement; readonly regressedIn: readonly string[] };
68
+
69
+ const movementsOf = (pairs: readonly Pair[], rubricRulesOf: RubricRulesOf): Moved[] =>
64
70
  rateRows([pairs.map((pair) => pair.before), pairs.map((pair) => pair.after)]).flatMap(({ rule, unit, rates: [before = 0, after = 0] }) => {
65
71
  const inUnit = pairs.filter((pair) => pair.after.size.unit === unit);
66
- const increasedIn = inUnit.filter((pair) => countIn(pair.after, rule) > countIn(pair.before, rule)).map((pair) => resultName(pair.after));
72
+ const increased = inUnit.filter((pair) => countIn(pair.after, rule) > countIn(pair.before, rule));
73
+ const increasedIn = increased.map((pair) => resultName(pair.after));
67
74
  const decreasedIn = inUnit.filter((pair) => countIn(pair.after, rule) < countIn(pair.before, rule)).map((pair) => resultName(pair.after));
68
75
  if (increasedIn.length === 0 && decreasedIn.length === 0 && before === after) return [];
69
- return [{ rule, unit, before, after, increasedIn, decreasedIn, inRubric: rubricRules.has(rule) }];
76
+ const graded = (pair: Pair): boolean => rubricRulesOf(pair.after).has(rule);
77
+ const regressedIn = increased.filter(graded).map((pair) => resultName(pair.after));
78
+ return [{ movement: { rule, unit, before, after, increasedIn, decreasedIn, inRubric: inUnit.some(graded) }, regressedIn }];
70
79
  });
71
80
 
72
81
  const factKey = (fact: GradeFact): string => `${fact.kind}\n${fact.key}`;
@@ -100,11 +109,9 @@ const penaltyOf = (results: readonly GradeResult[]): number => results.reduce((s
100
109
 
101
110
  const scored = (pairs: readonly Pair[]): boolean => pairs.some((pair) => pair.after.score !== undefined);
102
111
 
103
- const regressionsOf = (newlyFailed: readonly string[], movements: readonly RuleMovement[], penalty: Comparison["penalty"]): string[] => [
112
+ const regressionsOf = (newlyFailed: readonly string[], moved: readonly Moved[], penalty: Comparison["penalty"]): string[] => [
104
113
  ...newlyFailed.map((id) => `${id}: passed, now fails`),
105
- ...movements
106
- .filter((movement) => movement.inRubric && movement.increasedIn.length > 0)
107
- .map((movement) => `rules.${movement.rule}: more findings in ${movement.increasedIn.join(", ")}`),
114
+ ...moved.filter((entry) => entry.regressedIn.length > 0).map((entry) => `rules.${entry.movement.rule}: more findings in ${entry.regressedIn.join(", ")}`),
108
115
  ...(penalty !== undefined && penalty.after > penalty.before ? [`score.penalty ${String(penalty.before)} → ${String(penalty.after)}`] : []),
109
116
  ];
110
117
 
@@ -112,9 +119,10 @@ const pairRead = (pair: Pair): boolean => sameReading([pair.before, pair.after])
112
119
 
113
120
  /**
114
121
  * `after` compared with `before`, output by output: an output is paired with the earlier one of its id and variant.
115
- * `rubricRules`: the rules the rubric names, whose increase is a regression.
122
+ * `rubricRules`: the rules the rubric names, whose increase is a regression; per output when the rubric has tasks.
116
123
  */
117
- export const compareRuns = (before: readonly GradeResult[], after: readonly GradeResult[], rubricRules: ReadonlySet<string>): Comparison => {
124
+ export const compareRuns = (before: readonly GradeResult[], after: readonly GradeResult[], rubricRules: ReadonlySet<string> | RubricRulesOf): Comparison => {
125
+ const rubricRulesOf: RubricRulesOf = typeof rubricRules === "function" ? rubricRules : () => rubricRules;
118
126
  const earlier = new Map(before.map((result) => [resultKey(result), result]));
119
127
  const keys = new Set(after.map(resultKey));
120
128
  const matched = after.flatMap((result) => {
@@ -122,7 +130,7 @@ export const compareRuns = (before: readonly GradeResult[], after: readonly Grad
122
130
  return match === undefined ? [] : [{ before: match, after: result }];
123
131
  });
124
132
  const pairs = matched.filter(pairRead);
125
- const rules = movementsOf(pairs, rubricRules);
133
+ const moved = movementsOf(pairs, rubricRulesOf);
126
134
  const newlyFailed = pairs.filter((pair) => pair.before.pass && !pair.after.pass).map((pair) => resultName(pair.after));
127
135
  const penalty = scored(pairs) ? { before: penaltyOf(pairs.map((pair) => pair.before)), after: penaltyOf(pairs.map((pair) => pair.after)) } : undefined;
128
136
  return {
@@ -130,11 +138,11 @@ export const compareRuns = (before: readonly GradeResult[], after: readonly Grad
130
138
  onlyBefore: before.filter((result) => !keys.has(resultKey(result))).map(resultName),
131
139
  onlyAfter: after.filter((result) => !earlier.has(resultKey(result))).map(resultName),
132
140
  readOtherwise: matched.filter((pair) => !pairRead(pair)).map((pair) => resultName(pair.after)),
133
- rules,
141
+ rules: moved.map((entry) => entry.movement),
134
142
  newlyFailed,
135
143
  newlyPassed: pairs.filter((pair) => !pair.before.pass && pair.after.pass).map((pair) => resultName(pair.after)),
136
144
  items: pairs.map(itemChange).filter((change) => change.dropped.length + change.added.length + change.citations.length > 0),
137
145
  ...(penalty === undefined ? {} : { penalty }),
138
- regressions: regressionsOf(newlyFailed, rules, penalty),
146
+ regressions: regressionsOf(newlyFailed, moved, penalty),
139
147
  };
140
148
  };
@@ -0,0 +1,65 @@
1
+ import { documentLanguage } from "../check-source.ts";
2
+ import { factsOf } from "../commands/compare.ts";
3
+ import { documentFromSource } from "../commands/read-document.ts";
4
+ import type { Compared } from "../compare/outcome.ts";
5
+ import { resolveGenre } from "../resolve-genre.ts";
6
+ import { OUTPUT_PATH, type ItemReading } from "./checks.ts";
7
+ import { EXPECT_LISTS, entryPath, expectAgainst, factOfEntry, type GradeExpect, type GradeExpectResult, type ReadEntry } from "./expect.ts";
8
+ import type { GradeItem, ItemProblem } from "./item.ts";
9
+
10
+ // An item's `expect` entries read as compare reads a document, in the output's language and genre, so an entry and
11
+ // the output spell one fact alike.
12
+
13
+ const ENTRY_PATH = "expect.md";
14
+
15
+ type EntryRead = { readonly entry: ReadEntry } | { readonly problem: Omit<ItemProblem, "line"> };
16
+
17
+ const readEntry = async (list: ReadEntry["list"], index: number, entry: string, reading: ItemReading & { readonly language: string }): Promise<EntryRead> => {
18
+ const prose = await documentFromSource(ENTRY_PATH, entry, { language: reading.language, genre: reading.genre }, reading.config, true);
19
+ const facts = factsOf(ENTRY_PATH, prose, reading.config.names ?? []);
20
+ const found = factOfEntry(entry, facts.extraction.atoms);
21
+ if ("unread" in found)
22
+ return { problem: { kind: found.unread === "no-fact" ? "expect-no-fact" : "expect-several-facts", detail: `${entryPath(list, index)} ${entry}` } };
23
+ return { entry: { list, index, entry, read: { path: ENTRY_PATH, extraction: { ...facts.extraction, atoms: [found.fact] } } } };
24
+ };
25
+
26
+ const readEntries = (expect: GradeExpect, reading: ItemReading & { readonly language: string }): Promise<EntryRead[]> =>
27
+ Promise.all(EXPECT_LISTS.flatMap((list) => expect[list].map((entry, index) => readEntry(list, index, entry, reading))));
28
+
29
+ /** The output's own facts, read as `chaff compare` reads the document after. */
30
+ const outputFacts = async (output: string, reading: ItemReading & { readonly language: string }): Promise<Compared> => {
31
+ const prose = await documentFromSource(OUTPUT_PATH, output, { language: reading.language, genre: reading.genre }, reading.config, true);
32
+ return factsOf(OUTPUT_PATH, prose, reading.config.names ?? []);
33
+ };
34
+
35
+ /** The language and genre the output is graded in, as checkSource chooses them. */
36
+ const itemReading = (item: GradeItem, setup: { readonly config: ItemReading["config"]; readonly genre?: string | undefined }) => ({
37
+ config: setup.config,
38
+ language: item.language ?? documentLanguage(OUTPUT_PATH, item.output, setup.config),
39
+ genre: resolveGenre(OUTPUT_PATH, item.output, setup.config, item.genre ?? setup.genre).genre,
40
+ });
41
+
42
+ /** Each entry chaff cannot read as exactly one fact, on the item's line: the run stops before grading (exit 2). */
43
+ export const expectProblems = async (
44
+ item: GradeItem,
45
+ line: number,
46
+ setup: { readonly config: ItemReading["config"]; readonly genre?: string | undefined },
47
+ ): Promise<ItemProblem[]> => {
48
+ if (item.expect === undefined) return [];
49
+ const read = await readEntries(item.expect, itemReading(item, setup));
50
+ return read.flatMap((entry) => ("problem" in entry ? [{ ...entry.problem, line }] : []));
51
+ };
52
+
53
+ /**
54
+ * The output against the item's expect, read in the language and genre it was graded in. Throws for an entry that is
55
+ * not one fact: `chaff grade` refuses such an item before grading (expectProblems), so none reaches here.
56
+ */
57
+ export const expectOfItem = async (item: GradeItem, reading: ItemReading & { readonly language: string }): Promise<GradeExpectResult | undefined> => {
58
+ if (item.expect === undefined) return undefined;
59
+ const read = await readEntries(item.expect, reading);
60
+ const entries = read.flatMap((entry) => {
61
+ if ("problem" in entry) throw new Error(`${item.id}: ${entry.problem.detail ?? ""} is not one fact`);
62
+ return [entry.entry];
63
+ });
64
+ return expectAgainst(entries, await outputFacts(item.output, reading));
65
+ };
@@ -0,0 +1,74 @@
1
+ import type { Atom } from "../compare/atom.ts";
2
+ import { outcomeOf, type Compared } from "../compare/outcome.ts";
3
+
4
+ // An item's known answer (`expect`): facts the output must state and facts it must not. Pure: the shape of the field,
5
+ // which one fact an entry states, and whether the output states it, decided as `chaff compare --distinct` decides it.
6
+
7
+ export type GradeExpect = { readonly facts: readonly string[]; readonly absent: readonly string[] };
8
+
9
+ export const EXPECT_LISTS = ["facts", "absent"] as const;
10
+
11
+ export type ExpectList = (typeof EXPECT_LISTS)[number];
12
+
13
+ const isExpectList = (key: string): key is ExpectList => EXPECT_LISTS.some((list) => list === key);
14
+
15
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
16
+
17
+ const isEntryList = (value: unknown): value is string[] => Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.trim() !== "");
18
+
19
+ /**
20
+ * The field as written, or the path of its first wrong part. A key other than facts and absent is wrong, not ignored:
21
+ * a misspelt list would check nothing and pass every output. A missing list is an empty one.
22
+ */
23
+ export const expectOf = (raw: unknown): { readonly expect: GradeExpect | undefined } | { readonly wrong: string } => {
24
+ if (raw === undefined) return { expect: undefined };
25
+ if (!isRecord(raw)) return { wrong: "expect" };
26
+ const unknown = Object.keys(raw).find((key) => !isExpectList(key));
27
+ if (unknown !== undefined) return { wrong: `expect.${unknown}` };
28
+ const wrongList = EXPECT_LISTS.find((list) => raw[list] !== undefined && !isEntryList(raw[list]));
29
+ if (wrongList !== undefined) return { wrong: `expect.${wrongList}` };
30
+ const listOf = (list: ExpectList): readonly string[] => {
31
+ const value = raw[list];
32
+ return isEntryList(value) ? value : [];
33
+ };
34
+ return { expect: { facts: listOf("facts"), absent: listOf("absent") } };
35
+ };
36
+
37
+ /** The one fact an entry states, or why it states none or several. */
38
+ export type EntryFact = { readonly fact: Atom } | { readonly unread: "no-fact" | "several-facts" };
39
+
40
+ /**
41
+ * The fact an entry states. Several facts are one when exactly one of them is the whole entry: a quotation 「12,000円」
42
+ * is read as the quotation and the number inside it, and the entry means the quotation.
43
+ */
44
+ export const factOfEntry = (entry: string, atoms: readonly Atom[]): EntryFact => {
45
+ const [only] = atoms;
46
+ if (atoms.length === 1 && only !== undefined) return { fact: only };
47
+ if (atoms.length === 0) return { unread: "no-fact" };
48
+ const whole = atoms.filter((atom) => atom.text === entry.trim());
49
+ const [wholeOnly] = whole;
50
+ return whole.length === 1 && wholeOnly !== undefined ? { fact: wholeOnly } : { unread: "several-facts" };
51
+ };
52
+
53
+ /** Where an entry is in the item: `expect.facts[0]`. */
54
+ export const entryPath = (list: ExpectList, index: number): string => `expect.${list}[${String(index)}]`;
55
+
56
+ /** One entry as read: its list, its place, its text, and its document's facts with only the entry's fact kept. */
57
+ export type ReadEntry = { readonly list: ExpectList; readonly index: number; readonly entry: string; readonly read: Compared };
58
+
59
+ /** An entry that states one fact, compared as compare compares two documents: the output states it at least once. */
60
+ export const statesFact = (entry: Compared, output: Compared): boolean => outcomeOf(entry, output, undefined, "distinct").dropped.length === 0;
61
+
62
+ /** What the output was checked against: how many entries, the facts it does not state, and the absent ones it does. */
63
+ export type GradeExpectResult = { readonly checked: number; readonly missing: readonly string[]; readonly present: readonly string[] };
64
+
65
+ /** Each entry against the output, by the entry's text as written in the item. */
66
+ export const expectAgainst = (entries: readonly ReadEntry[], output: Compared): GradeExpectResult => ({
67
+ checked: entries.length,
68
+ missing: entries.filter((entry) => entry.list === "facts" && !statesFact(entry.read, output)).map((entry) => entry.entry),
69
+ present: entries.filter((entry) => entry.list === "absent" && statesFact(entry.read, output)).map((entry) => entry.entry),
70
+ });
71
+
72
+ /** The failure reasons: `expect.missing 12,000円`, `expect.present 15,000円`. Absent when the item had no expect. */
73
+ export const expectFailures = (result: GradeExpectResult | undefined): string[] =>
74
+ result === undefined ? [] : [...result.missing.map((entry) => `expect.missing ${entry}`), ...result.present.map((entry) => `expect.present ${entry}`)];
@@ -10,11 +10,14 @@ import { ratesOf, sizeOf, type OutputSize } from "./rates.ts";
10
10
  import type { GradeCitations, GradeFacts, GradeFinding, GradeResult, GradeScore, NotRunEntry, Stamp } from "./result.ts";
11
11
  import type { Rubric } from "./rubric.ts";
12
12
  import { rubricVerdict } from "./rubric-verdict.ts";
13
+ import { rubricForTask } from "./task-rubric.ts";
13
14
  import type { GradeSettings } from "./stamp.ts";
14
15
  import { GRADE_TEXT } from "./text.ts";
15
16
  import { aiScoreOfDocument } from "../ai-score/of-document.ts";
16
17
  import { gradeAiScoreOf } from "./ai-score.ts";
17
- import { defaultVerdict, type Verdict } from "./verdict.ts";
18
+ import { defaultVerdict, verdictOf, type Verdict } from "./verdict.ts";
19
+ import { expectFailures, type GradeExpectResult } from "./expect.ts";
20
+ import { expectOfItem } from "./expect-read.ts";
18
21
 
19
22
  /** One run's settings, its `grade:` rubric when chaff.yaml has one, and its stamp, which every result carries. */
20
23
  export type GradeSetup = GradeSettings & { readonly stamp: Stamp; readonly rubric?: Rubric | undefined };
@@ -34,6 +37,7 @@ type Checked = {
34
37
  readonly facts: GradeFacts | null;
35
38
  readonly cited: CitationsRead | undefined;
36
39
  readonly contexts: ContextsRead | undefined;
40
+ readonly expect: GradeExpectResult | undefined;
37
41
  };
38
42
 
39
43
  const checkItem = async (item: GradeItem, setup: GradeSetup): Promise<Checked> => {
@@ -50,6 +54,7 @@ const checkItem = async (item: GradeItem, setup: GradeSetup): Promise<Checked> =
50
54
  facts: item.reference === undefined ? null : await factsAgainst(texts, reading, allowedBy(setup.rubric)),
51
55
  cited: item.citations === undefined ? undefined : await citationsAgainst(item, item.citations, reading, ui),
52
56
  contexts: await contextsOf(item, check.language, reading, setup.rubric),
57
+ expect: await expectOfItem(item, { ...reading, language: check.language }),
53
58
  };
54
59
  };
55
60
 
@@ -97,12 +102,17 @@ const judge = (item: GradeItem, checked: Checked, setup: GradeSetup): { readonly
97
102
  };
98
103
 
99
104
  /** Grades one output (spec §29.3): its findings and their rates, the facts against its reference, its quotations, and pass or fail. */
100
- export const gradeItem = async (item: GradeItem, setup: GradeSetup): Promise<GradeResult> => {
105
+ export const gradeItem = async (item: GradeItem, runSetup: GradeSetup): Promise<GradeResult> => {
106
+ const setup = item.task === undefined ? runSetup : { ...runSetup, rubric: rubricForTask(runSetup.rubric, item.task) };
101
107
  const checked = await checkItem(item, setup);
102
- const { verdict, score } = judge(item, checked, setup);
108
+ const judged = judge(item, checked, setup);
109
+ const { score } = judged;
110
+ // expect is the item's own answer, so it fails the output with or without a `grade:` rubric.
111
+ const verdict = verdictOf([...judged.verdict.failedBecause, ...expectFailures(checked.expect)]);
103
112
  return {
104
113
  id: item.id,
105
114
  ...(item.variant === undefined ? {} : { variant: item.variant }),
115
+ ...(item.task === undefined ? {} : { task: item.task }),
106
116
  language: checked.check.language,
107
117
  genre: checked.check.genre.genre,
108
118
  size: checked.size,
@@ -115,6 +125,7 @@ export const gradeItem = async (item: GradeItem, setup: GradeSetup): Promise<Gra
115
125
  facts: checked.facts,
116
126
  citations: citationsOf(checked),
117
127
  ...(checked.contexts === undefined ? {} : { contexts: checked.contexts.contexts }),
128
+ ...(checked.expect === undefined ? {} : { expect: checked.expect }),
118
129
  ...(score === undefined ? {} : { score }),
119
130
  aiScore: gradeAiScoreOf(aiScoreOfDocument(checked.check.doc, checked.check.rules, checked.check.genre.genre)),
120
131
  pass: verdict.pass,
package/src/grade/item.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // One line of `chaff grade`'s input: a model's output and what to check it against. Pure: reads text, returns items
2
2
  // or what is wrong with them, so a harness that writes the file can be told which line to fix.
3
+ import { expectOf, type GradeExpect } from "./expect.ts";
3
4
  import { resultKey } from "./result-name.ts";
4
5
 
5
6
  export type GradeCitation = { readonly source: string; readonly address: string; readonly quote: string };
@@ -18,10 +19,14 @@ export type GradeItem = {
18
19
  readonly contexts?: readonly string[] | undefined;
19
20
  /** The prompt, model or setting that produced this output. Outputs of several variants are compared on the same ids. */
20
21
  readonly variant?: string | undefined;
22
+ /** The rubric task this item is graded under (`grade.tasks.<name>`). Undefined: the top-level rubric. */
23
+ readonly task?: string | undefined;
24
+ /** Facts the output must state and facts it must not: a question's known answer. Undefined when not given. */
25
+ readonly expect?: GradeExpect | undefined;
21
26
  };
22
27
 
23
28
  /** The fields an item already has a meaning for: none of them can name a variant. */
24
- export const ITEM_FIELDS: readonly string[] = ["id", "output", "reference", "sources", "citations", "contexts", "language", "genre"];
29
+ export const ITEM_FIELDS: readonly string[] = ["id", "output", "reference", "sources", "citations", "contexts", "language", "genre", "expect", "task"];
25
30
 
26
31
  /** Which field names an output's variant, and whether every line must have it (the key was given on the command line). */
27
32
  export type VariantField = { readonly key: string; readonly required: boolean };
@@ -44,13 +49,22 @@ export type ItemProblemKind =
44
49
  | "which-source"
45
50
  | "unknown-language"
46
51
  | "unknown-genre"
52
+ | "not-expect"
53
+ | "expect-no-fact"
54
+ | "expect-several-facts"
55
+ | "unknown-task"
47
56
  | "empty";
48
57
 
49
58
  /** Why a line cannot be graded. `detail`: the field, the value or the parser's message; `first`: the line an id was first on. */
50
59
  export type ItemProblem = { readonly kind: ItemProblemKind; readonly line: number; readonly detail?: string; readonly first?: number };
51
60
 
52
61
  /** What an item may name: the languages chaff can load and the genres it knows. */
53
- export type ItemVocabulary = { readonly isLanguage: (language: string) => boolean; readonly genres: readonly string[] };
62
+ export type ItemVocabulary = {
63
+ readonly isLanguage: (language: string) => boolean;
64
+ readonly genres: readonly string[];
65
+ /** The tasks the rubric defines. None given: an item naming a task is refused, as there is no rubric for it. */
66
+ readonly tasks?: readonly string[] | undefined;
67
+ };
54
68
 
55
69
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
56
70
 
@@ -129,6 +143,27 @@ const variantOf = (raw: Record<string, unknown>, key: string, line: number): Par
129
143
  return typeof value === "string" && value !== "" ? { value } : { problem: { kind: "not-string", line, detail: key } };
130
144
  };
131
145
 
146
+ /** The item's task: a non-empty string the rubric defines, or none. Never a silent fall back to the top-level rubric. */
147
+ const taskOf = (raw: unknown, vocabulary: ItemVocabulary, line: number): Parsed<string | undefined> => {
148
+ if (raw === undefined) return { value: undefined };
149
+ if (typeof raw !== "string" || raw === "") return { problem: { kind: "not-string", line, detail: "task" } };
150
+ return (vocabulary.tasks ?? []).includes(raw) ? { value: raw } : { problem: { kind: "unknown-task", line, detail: raw } };
151
+ };
152
+
153
+ /** The item's variant and task, each written only when given, checked in that order. */
154
+ const labelsOf = (
155
+ raw: Record<string, unknown>,
156
+ line: number,
157
+ vocabulary: ItemVocabulary,
158
+ variantKey: string,
159
+ ): Parsed<{ readonly variant?: string; readonly task?: string }> => {
160
+ const variant = variantOf(raw, variantKey, line);
161
+ if ("problem" in variant) return variant;
162
+ const task = taskOf(raw["task"], vocabulary, line);
163
+ if ("problem" in task) return task;
164
+ return { value: { ...(variant.value === undefined ? {} : { variant: variant.value }), ...(task.value === undefined ? {} : { task: task.value }) } };
165
+ };
166
+
132
167
  const itemOf = (raw: Record<string, unknown>, line: number, vocabulary: ItemVocabulary, variantKey: string): Parsed<GradeItem> => {
133
168
  const id = raw["id"];
134
169
  if (typeof id !== "string" || id === "") return { problem: { kind: "no-id", line } };
@@ -138,17 +173,22 @@ const itemOf = (raw: Record<string, unknown>, line: number, vocabulary: ItemVoca
138
173
  if ("problem" in fields) return fields;
139
174
  const unknown = vocabularyProblem(fields.value, vocabulary, line);
140
175
  if (unknown !== undefined) return { problem: unknown };
141
- const variant = variantOf(raw, variantKey, line);
142
- if ("problem" in variant) return variant;
176
+ const labels = labelsOf(raw, line, vocabulary, variantKey);
177
+ if ("problem" in labels) return labels;
143
178
  const sources = sourcesOf(raw["sources"], line);
144
179
  if ("problem" in sources) return sources;
145
180
  const citations = citationsOf(raw["citations"], sources.value, line);
146
181
  if ("problem" in citations) return citations;
147
182
  const contexts = contextsOf(raw["contexts"], line);
148
183
  if ("problem" in contexts) return contexts;
184
+ const expect = expectOf(raw["expect"]);
185
+ if ("wrong" in expect) return { problem: { kind: "not-expect", line, detail: expect.wrong } };
149
186
  const passages = contexts.value === undefined ? {} : { contexts: contexts.value };
150
- const labelled = variant.value === undefined ? {} : { variant: variant.value };
151
- return { value: { id, output, ...fields.value, sources: sources.value, citations: citations.value, ...passages, ...labelled } };
187
+ const expected = expect.expect === undefined ? {} : { expect: expect.expect };
188
+ const { variant, task } = labels.value;
189
+ const labelled = variant === undefined ? {} : { variant };
190
+ const tasked = task === undefined ? {} : { task };
191
+ return { value: { id, output, ...fields.value, sources: sources.value, citations: citations.value, ...passages, ...labelled, ...expected, ...tasked } };
152
192
  };
153
193
 
154
194
  /** One item from a value already parsed, as `grade()` receives it; `line` is 0 when it came from no file. */
@@ -192,7 +232,7 @@ export const parseItems = (
192
232
  text: string,
193
233
  vocabulary: ItemVocabulary,
194
234
  variantField: VariantField = DEFAULT_VARIANT_FIELD,
195
- ): { readonly items: readonly GradeItem[] } | { readonly problems: readonly ItemProblem[] } => {
235
+ ): { readonly items: readonly GradeItem[]; readonly lines: readonly number[] } | { readonly problems: readonly ItemProblem[] } => {
196
236
  const lines = text.split(/\r?\n/u).map((body, index) => ({ body, line: index + 1 }));
197
237
  const parsed = lines
198
238
  .filter(({ body }) => body.trim() !== "")
@@ -204,5 +244,5 @@ export const parseItems = (
204
244
  ...unlabelled(read, variantField),
205
245
  ];
206
246
  if (problems.length > 0) return { problems: problems.toSorted((left, right) => left.line - right.line) };
207
- return read.length === 0 ? { problems: [{ kind: "empty", line: 0 }] } : { items: read.map((entry) => entry.item) };
247
+ return read.length === 0 ? { problems: [{ kind: "empty", line: 0 }] } : { items: read.map((entry) => entry.item), lines: read.map((entry) => entry.line) };
208
248
  };
@@ -3,6 +3,7 @@ import type { CitationStatus } from "../structure/cite.ts";
3
3
  import type { Severity } from "../plugin.ts";
4
4
  import type { OutputSize } from "./rates.ts";
5
5
  import type { GradeAiScore } from "./ai-score.ts";
6
+ import type { GradeExpectResult } from "./expect.ts";
6
7
 
7
8
  // The shape of one line of `chaff grade --out`, and of what `grade()` returns. Spec §29.3.
8
9
 
@@ -62,6 +63,8 @@ export type GradeResult = {
62
63
  readonly id: string;
63
64
  /** The item's variant label, when it had one. */
64
65
  readonly variant?: string | undefined;
66
+ /** The rubric task the output was graded under, when the item named one. */
67
+ readonly task?: string | undefined;
65
68
  readonly language: string;
66
69
  readonly genre: string;
67
70
  readonly size: OutputSize;
@@ -74,6 +77,8 @@ export type GradeResult = {
74
77
  readonly citations: GradeCitations | null;
75
78
  /** Only when the item gave contexts, so results of items without them keep their shape. */
76
79
  readonly contexts?: GradeContexts | undefined;
80
+ /** Only when the item gave expect: the entries checked, the facts not stated and the absent ones stated. */
81
+ readonly expect?: GradeExpectResult | undefined;
77
82
  /** Only with a `grade:` rubric. */
78
83
  readonly score?: GradeScore | undefined;
79
84
  /** The AI-likeness quick score. Absent only in results written before it existed. Never part of pass or fail. */
@@ -72,8 +72,13 @@ const isRates = (value: unknown): boolean => isRecord(value) && Object.values(va
72
72
  /** The id, and the variant when the output had one. */
73
73
  const isLabel = (value: Record<string, unknown>): boolean => isString(value["id"]) && (value["variant"] === undefined || isString(value["variant"]));
74
74
 
75
- /** The parts a result carries only sometimes: contexts, a rubric's score, the quick score. Each is checked when present. */
76
- const hasOptionalParts = (value: Record<string, unknown>): boolean => isContexts(value["contexts"]) && isScore(value["score"]) && isAiScore(value["aiScore"]);
75
+ /** Absent when the item gave no expect. */
76
+ const isExpect = (value: unknown): boolean =>
77
+ value === undefined || (isRecord(value) && isNumber(value["checked"]) && isArrayOf(value["missing"], isString) && isArrayOf(value["present"], isString));
78
+
79
+ /** The parts a result carries only sometimes: contexts, expect, a rubric's score, the quick score. Each is checked when present. */
80
+ const hasOptionalParts = (value: Record<string, unknown>): boolean =>
81
+ isContexts(value["contexts"]) && isExpect(value["expect"]) && isScore(value["score"]) && isAiScore(value["aiScore"]);
77
82
 
78
83
  /** A line `--out` writes, every field checked: a line that only looks like one would be compared as if it had been graded. */
79
84
  export const isGradeResult = (value: unknown): value is GradeResult =>