///|
/// The `--group=zh` cell run (spec §4.7).
///
/// The Chinese cells live in their own package (`lib/gate/zh/`) with their own
/// `Cell` type, so they can never reach `@gate_data.all_cells()` and therefore
/// never reach the English field index: `enforce_field_index` rejects any
/// shape outside the nineteen `[ALIGN]` names and `--group=all` runs it. The
/// separation is a compile-time fact rather than a review rule (design D-Z6).
///
/// The run reuses the shared three things of the gate — the canonical first
/// line, the exit-code semantics (AD section 3.2) and the failure-record
/// schema (AD section 2.5) — and nothing else: no `select_cells`, no
/// `enforce_field_index`, no L3 cell. `floor=366` is still printed and still
/// enforced against `@gate_data.total_cells()`, because it protects the
/// English table; the Chinese group reports its own 192 cells.
///
/// `--inject=` is accepted here as it is for `selfcheck` (spec §4.7.2): the
/// nth expectation is perturbed in memory so that the run must go red, which
/// is the only way to tell "192 green" from "nothing ran" on the exit code.
///|
/// One mismatched Chinese cell together with the value it returned.
struct ZhFailure {
cell : @gate_zh.Cell
actual : Actual
injected : Bool
}
///|
/// The text of one Chinese cell.
///
/// An unknown sample id is a table defect: EXIT=2, never an empty text that
/// could false-pass.
fn zh_input_text(cell : @gate_zh.Cell) -> String {
match @gate_zh.case_text(cell.sample_id) {
Some(text) => text
None => gate_fail("unknown zh case id: " + cell.sample_id)
}
}
///|
/// Evaluate one Chinese cell against the public entry of the `zh` package.
///
/// The text and the fixture both come from the cell, so the hashed input of a
/// failure record is exactly the evaluated input. A cell whose `fixture` is
/// empty uses the bundled level-1 table; otherwise the fixture characters are
/// handed to `analyze_zh` as the table.
fn evaluate_zh_cell(cell : @gate_zh.Cell) -> Actual {
let text = zh_input_text(cell)
let stats = if cell.fixture == "" {
@zh.analyze_zh(text)
} else {
@zh.analyze_zh(text, common_chars=cell.fixture)
}
match cell.metric {
"zh_sentence_count" => AInt(stats.zh_sentence_count)
"zh_sentence_length_mean" =>
ABits(stats.zh_sentence_length_mean.reinterpret_as_uint64())
"zh_sentence_length_sum" => AInt(stats.zh_sentence_length_sum)
"zh_sentence_bucket_1_10" => AInt(stats.zh_sentence_bucket_1_10)
"zh_sentence_bucket_11_20" => AInt(stats.zh_sentence_bucket_11_20)
"zh_sentence_bucket_21_40" => AInt(stats.zh_sentence_bucket_21_40)
"zh_sentence_bucket_41_plus" => AInt(stats.zh_sentence_bucket_41_plus)
"zh_han_char_total" => AInt(stats.zh_han_char_total)
"zh_han_char_distinct" => AInt(stats.zh_han_char_distinct)
"zh_common_char_matched" => AInt(stats.zh_common_char_matched)
"zh_common_char_coverage" =>
ABits(stats.zh_common_char_coverage.reinterpret_as_uint64())
"zh_uncovered_char_list" => AList(stats.zh_uncovered_char_list)
_ => gate_fail("unknown zh metric in the expectation table: " + cell.metric)
}
}
///|
/// Compare one Chinese expectation with the observed value.
///
/// Unlike the English comparator, a list here is compared **element by
/// element**: `zh_uncovered_char_list` is defined as first-appearance order
/// (PRD-B FR-06), so a reordering is a mismatch even though the set is the
/// same. This is the one place where the Chinese contract and the English
/// `difficult_words_list` contract deliberately differ (spec §4.5.4).
fn zh_cell_matches(expected : @gate_zh.Expected, actual : Actual) -> Bool {
match (expected, actual) {
(@gate_zh.EInt(want), AInt(got)) => want == got
(@gate_zh.EDouble(want), ABits(got)) => want == got
(@gate_zh.EList(want), AList(got)) => {
if want.length() != got.length() {
return false
}
let mut index = 0
while index < want.length() {
if want[index] != got[index] {
return false
}
index = index + 1
}
true
}
_ => false
}
}
///|
/// The `expected` JSON value of one Chinese cell.
fn zh_expected_json(expected : @gate_zh.Expected) -> String {
match expected {
@gate_zh.EInt(value) => value.to_string()
@gate_zh.EDouble(bits) => json_string(double_text(bits))
@gate_zh.EList(words) => json_string_array(words)
}
}
///|
/// One Chinese failure record, in the shared seven-field schema plus
/// `injected` (AD section 2.5). Floats add `expected_bits` / `actual_bits`.
fn zh_failure_record(failure : ZhFailure) -> String {
let cell = failure.cell
let fields = record_fields(
sha256_hex(zh_input_text(cell)),
cell.sample_id,
cell.metric,
cell.lang,
zh_expected_json(cell.expected),
actual_json(failure.actual),
failure.injected,
)
match cell.expected {
@gate_zh.EDouble(bits) =>
fields.push(json_field("expected_bits", json_string(bits_hex(bits))))
_ => ()
}
match failure.actual {
ABits(bits) =>
fields.push(json_field("actual_bits", json_string(bits_hex(bits))))
_ => ()
}
"{" + fields.join(",") + "}"
}
///|
/// A copy of `cell` whose expectation cannot match its actual value.
///
/// The perturbation rules are the ones `inject.mbt` already uses for the
/// English table, minus the `EError` case the Chinese cells do not have: an
/// integer moves by one, a double flips its lowest bit (the smallest possible
/// change, and still a change for `NaN`, `+0.0` and `-0.0`), and a list gains
/// the sentinel word, which breaks the element-wise comparison.
fn injected_zh_cell(cell : @gate_zh.Cell) -> @gate_zh.Cell {
let expected = match cell.expected {
@gate_zh.EInt(value) => @gate_zh.EInt(value + 1)
@gate_zh.EDouble(bits) => @gate_zh.EDouble(bits ^ 1)
@gate_zh.EList(words) => {
let replaced : Array[String] = []
for word in words {
replaced.push(word)
}
replaced.push(INJECTED_LIST_WORD)
@gate_zh.EList(replaced)
}
}
{
sample_id: cell.sample_id,
metric: cell.metric,
lang: cell.lang,
fixture: cell.fixture,
expected,
}
}
///|
/// Run the `--group=zh` cell set and map its outcome to the exit contract.
///
/// The sequence matches the L3 path: report line, then `enforce_floor` on the
/// English table, then the failure records, then the exit code. A table below
/// the floor is an environment failure and leaves no `.jsonl` behind, exactly
/// as on the other paths.
///
/// `passed < cells` exits 1 and promises the records are on disk; a cell count
/// of zero is EXIT=2 (a known group that selects nothing is a defect, not a
/// vacuous pass — AD section 3.3).
fn run_zh_group(config : GateConfig) -> Unit {
let cells = @gate_zh.all_cells()
if cells.length() == 0 {
gate_fail("group zh selected no cells")
}
if cells.length() != @gate_zh.total_cells() {
gate_fail("the zh table returned an inconsistent cell count")
}
let mut inject_target : (Int32, @gate_zh.Cell)? = None
if config.inject != 0 {
let index = config.inject - 1
if index < 0 || index >= cells.length() {
gate_fail(
"--inject=" +
config.inject.to_string() +
" is out of range for " +
cells.length().to_string() +
" selected cells",
)
}
let original = cells[index]
cells[index] = injected_zh_cell(original)
inject_target = Some((index, original))
}
let mut passed = 0
let failures : Array[ZhFailure] = []
let mut index = 0
for cell in cells {
let actual = evaluate_zh_cell(cell)
if zh_cell_matches(cell.expected, actual) {
passed = passed + 1
} else {
match inject_target {
Some((target, original)) =>
if target == index && zh_cell_matches(original.expected, actual) {
// The un-injected expectation would have held, so the mismatch is
// the injection's own doing and the record is an artifact.
failures.push({ cell, actual, injected: true, })
} else {
failures.push({ cell, actual, injected: false, })
}
None => failures.push({ cell, actual, injected: false, })
}
}
index = index + 1
}
println(report_line(cells.length(), passed, config.group, config.inject))
enforce_floor(@gate_data.total_cells())
if failures.length() > 0 {
let records : Array[String] = []
for failure in failures {
records.push(zh_failure_record(failure))
}
write_failure_records(records)
}
if passed < cells.length() {
exit_with_code(1)
} else {
exit_with_code(0)
}
}