///|
/// The field-name index behind A-11.3 (AD section 4, spec section 4.2).
///
/// AD-4 is a **negative** commitment: the authoritative field contract lives
/// in PRD-A 10.3 / PRD-B 10.3, and this repository must not open a second
/// source of truth. This file is therefore not a field table in the
/// documentation sense — it is the code-side index the gate uses to look up
/// expected values **by field name**, and every row is bound to the real
/// `TextStats` field through a typed probe:
///
/// - the name is the key the frozen L3 cells are indexed by (`cell.shape`,
/// minus a trailing `[...]` parameter list);
/// - the kind is the comparison shape AD section 2 fixes for that field
/// (`counter` by value, `score` by IEEE-754 bit pattern, `word_list` as a
/// set);
/// - the probe reads the field off a `TextStats`, so renaming or retyping a
/// field in `types.mbt` stops this package from building. A renamed field
/// therefore can never be silently indexed under a stale name: the build
/// fails, which AD section 3.2 classifies as an environment/build failure
/// (EXIT=2), exactly the outcome AD section 4 predicts ("no expected value
/// found for that field name").
///
/// Nothing here is compared against a hand-written field document: the
/// runtime assertions walk the **delivered** 366-cell table and check that
/// the names and kinds observed there are exactly the ones declared here, in
/// both directions. A cell whose name is outside the index, a missing field,
/// a 20th name, or a kind mismatch (a `Double` formula compared as `Int`, the
/// word list compared as anything but a list) is a reported problem, and the
/// test in `field_index_wbtest.mbt` turns any problem into a hard failure.
///|
/// The comparison shape AD section 2 fixes for one `[ALIGN]` field.
///
/// `counter` values are compared by integer value; `score` values by their
/// IEEE-754 bit pattern (never by printed text); `word_list` values by set
/// equality (order and multiplicity are not contractual).
enum FieldKind {
Counter
Score
WordList
} derive(Eq)
///|
/// The canonical spelling of one kind, used in diagnostics and in the
/// evidence line.
fn field_kind_name(kind : FieldKind) -> String {
match kind {
Counter => "counter"
Score => "score"
WordList => "word_list"
}
}
///|
/// The value of one field, normalized to the shape its kind implies.
///
/// A `score` carries the `UInt64` bit pattern, so `-0.0` and `0.0` stay
/// distinguishable and no decimal text is ever compared (AD section 2.2).
enum FieldValue {
VCounter(Int32)
VScore(UInt64)
VWords(Array[String])
}
///|
/// One row of the index: the field name, its comparison kind, and the probe
/// that reads it off a `TextStats`.
///
/// The probe is what keeps the row honest: it names a real struct field, so
/// the row cannot survive a rename or a retype in `types.mbt`.
struct FieldEntry {
name : String
kind : FieldKind
probe : (@moon_textstat.TextStats) -> FieldValue
}
///|
/// A counter row (`Int32`, compared by value).
fn counter_field(
name : String,
probe : (@moon_textstat.TextStats) -> Int32,
) -> FieldEntry {
{ name, kind: Counter, probe: fn(stats) { VCounter(probe(stats)) }, }
}
///|
/// A score row (`Double`, compared by IEEE-754 bit pattern).
fn score_field(
name : String,
probe : (@moon_textstat.TextStats) -> Double,
) -> FieldEntry {
{
name,
kind: Score,
probe: fn(stats) { VScore(probe(stats).reinterpret_as_uint64()) },
}
}
///|
/// A word-list row (`Array[String]`, compared as a set).
fn word_list_field(
name : String,
probe : (@moon_textstat.TextStats) -> Array[String],
) -> FieldEntry {
{ name, kind: WordList, probe: fn(stats) { VWords(probe(stats)) }, }
}
///|
/// The 19 `[ALIGN]` fields of `TextStats`, in the spec section 4.2 order.
///
/// Nine `Int32` counters, one word list, then nine `Double` scores
/// (`reading_time` plus the eight formula fields). The order is documentation
/// only: every lookup below is by name.
let align_field_index : Array[FieldEntry] = [
counter_field("char_count", fn(stats) { stats.char_count }),
counter_field("letter_count", fn(stats) { stats.letter_count }),
counter_field("lexicon_count", fn(stats) { stats.lexicon_count }),
counter_field("sentence_count", fn(stats) { stats.sentence_count }),
counter_field("syllable_count", fn(stats) { stats.syllable_count }),
counter_field("polysyllabcount", fn(stats) { stats.polysyllabcount }),
counter_field("monosyllabcount", fn(stats) { stats.monosyllabcount }),
counter_field("difficult_words", fn(stats) { stats.difficult_words }),
word_list_field("difficult_words_list", fn(stats) {
stats.difficult_words_list
}),
counter_field("long_word_count", fn(stats) { stats.long_word_count }),
score_field("reading_time", fn(stats) { stats.reading_time }),
score_field("flesch_reading_ease", fn(stats) { stats.flesch_reading_ease }),
score_field("flesch_kincaid_grade", fn(stats) { stats.flesch_kincaid_grade }),
score_field("gunning_fog", fn(stats) { stats.gunning_fog }),
score_field("smog_index", fn(stats) { stats.smog_index }),
score_field("automated_readability_index", fn(stats) {
stats.automated_readability_index
}),
score_field("coleman_liau_index", fn(stats) { stats.coleman_liau_index }),
score_field("linsear_write_formula", fn(stats) { stats.linsear_write_formula }),
score_field("dale_chall_readability_score", fn(stats) {
stats.dale_chall_readability_score
}),
]
///|
/// The frozen number of `[ALIGN]` fields (A-11.3).
const ALIGN_FIELD_COUNT : Int32 = 19
///|
/// The provenance prefix AD section 8 requires on every frozen cell.
const FIELD_INDEX_PROVENANCE : String = "golden:oracle_smoke.json#"
///|
/// Look one row up by field name; `None` means the name is outside the index.
fn declared_field(name : String) -> FieldEntry? {
for entry in align_field_index {
if entry.name == name {
return Some(entry)
}
}
None
}
///|
/// Strip the trailing `[...]` parameter list of a cell shape.
///
/// `lexicon_count[removepunct=False]` indexes the field `lexicon_count`; a
/// bare name is returned unchanged. A malformed shape (empty, an empty
/// parameter list, a missing `]`, a second bracket) yields `""`, which the
/// assertions report as an unusable shape rather than folding it into some
/// neighbouring field.
fn shape_field_name(shape : String) -> String {
if shape.length() == 0 {
return ""
}
match shape.find("[") {
None => shape
Some(index) => {
if index == 0 || !shape.has_suffix("]") {
return ""
}
if shape.find("]") != Some(shape.length() - 1) {
return ""
}
shape[:index].to_owned()
}
}
}
///|
/// The comparison kind implied by one expected value; `None` for an error
/// cell, which carries no value kind because the call must raise (A-5.2).
fn expected_field_kind(expected : @gate_data.Expected) -> FieldKind? {
match expected {
@gate_data.EInt(_) => Some(Counter)
@gate_data.EDouble(_) => Some(Score)
@gate_data.EList(_) => Some(WordList)
@gate_data.EError(_) => None
}
}
///|
/// A `TextStats` whose 19 fields carry 19 pairwise-distinct sentinels: the
/// counters `1..9`, the word list `["sentinel_alpha", "sentinel_beta"]`, and
/// the scores `10.5..19.5`.
///
/// The probe witness below uses it to prove that each index row reads the
/// field it claims: a row wired to the wrong field (or to a duplicate) would
/// return a sentinel already claimed by another row, and the witness reports
/// it. No name-to-value table is involved, so this check cannot drift from
/// the index.
fn field_index_sentinel() -> @moon_textstat.TextStats {
{
char_count: 1,
letter_count: 2,
lexicon_count: 3,
sentence_count: 4,
syllable_count: 5,
polysyllabcount: 6,
monosyllabcount: 7,
difficult_words: 8,
difficult_words_list: ["sentinel_alpha", "sentinel_beta"],
long_word_count: 9,
reading_time: 10.5,
flesch_reading_ease: 11.5,
flesch_kincaid_grade: 12.5,
gunning_fog: 13.5,
smog_index: 14.5,
automated_readability_index: 15.5,
coleman_liau_index: 16.5,
linsear_write_formula: 17.5,
dale_chall_readability_score: 18.5,
}
}
///|
/// A stable text key of one probed value, used only to detect duplicates in
/// the sentinel witness. Bit patterns and integers are exact; the word list
/// joins its elements with the ASCII unit separator, which no sentinel word
/// contains.
fn field_value_key(value : FieldValue) -> String {
match value {
VCounter(count) => "i:" + count.to_string()
VScore(bits) => "d:" + bits.to_string()
VWords(words) => "w:" + words.join("\u{1F}")
}
}
///|
/// Check that every index row's probe reads a distinct field of the real
/// `TextStats`.
///
/// The `FieldValue` variant of a probe result is fixed by the `*_field`
/// constructor, so it cannot disagree with the row's `kind`; the check that
/// *does* carry information is the uniqueness of the values, because a row
/// wired to the wrong struct field returns a sentinel another row already
/// claimed.
fn witness_problems(problems : Array[String]) -> Unit {
let sentinel = field_index_sentinel()
let keys : Array[String] = []
for entry in align_field_index {
let value = (entry.probe)(sentinel)
let key = field_value_key(value)
if keys.contains(key) {
problems.push(
"field index: " +
entry.name +
" reads the same field as an earlier row (duplicate probe)",
)
}
keys.push(key)
}
if keys.length() != ALIGN_FIELD_COUNT {
problems.push(
"field index: " +
keys.length().to_string() +
" probes for " +
ALIGN_FIELD_COUNT.to_string() +
" fields",
)
}
}
///|
/// Every assertion of A-11.3 over one cell table; an empty result means the
/// index and the table agree.
///
/// The checks are deliberately two-directional so that neither side can pass
/// vacuously:
///
/// 1. the index has exactly 19 rows, with 19 distinct names;
/// 2. each row's probe reads a distinct real field of the declared kind
/// (sentinel witness — a rename in `types.mbt` fails the build instead);
/// 3. every cell has a non-empty `shape`, `group`, `sample_id`, `lang`, and a
/// `source` with the AD section 8 provenance prefix;
/// 4. every cell's shape derives a field name that is inside the index
/// (a 20th name fails here);
/// 5. every non-error cell's expected-value constructor matches the declared
/// kind (a `Double` formula compared as `Int`, or the word list compared
/// as anything but a list, fails here); `EError` cells are only allowed in
/// the `errors` group;
/// 6. every declared field is exercised at least once, and the number of
/// distinct observed names is exactly 19 (a missing field fails here).
pub fn field_index_problems(cells : Array[@gate_data.Cell]) -> Array[String] {
let problems : Array[String] = []
if align_field_index.length() != ALIGN_FIELD_COUNT {
problems.push(
"field index: " +
align_field_index.length().to_string() +
" rows, expected " +
ALIGN_FIELD_COUNT.to_string(),
)
}
let declared_names : Array[String] = []
for entry in align_field_index {
if entry.name.length() == 0 {
problems.push("field index: an empty field name is not allowed")
}
if declared_names.contains(entry.name) {
problems.push("field index: duplicate field name " + entry.name)
}
declared_names.push(entry.name)
}
witness_problems(problems)
if cells.length() == 0 {
problems.push("field index: the delivered cell table is empty")
return problems
}
let seen : Array[String] = []
for cell in cells {
if cell.shape.length() == 0 {
problems.push(
"field index: cell " + cell.sample_id + " has an empty shape",
)
}
if cell.group.length() == 0 {
problems.push(
"field index: cell " +
cell.sample_id +
"/" +
cell.shape +
" has an empty group",
)
}
if cell.sample_id.length() == 0 {
problems.push("field index: a cell has an empty sample_id")
}
if cell.lang.length() == 0 {
problems.push(
"field index: cell " +
cell.sample_id +
"/" +
cell.shape +
" has an empty lang",
)
}
if !cell.source.has_prefix(FIELD_INDEX_PROVENANCE) {
problems.push(
"field index: cell " +
cell.sample_id +
"/" +
cell.shape +
" lacks the provenance prefix " +
FIELD_INDEX_PROVENANCE,
)
}
let name = shape_field_name(cell.shape)
if name.length() == 0 {
problems.push(
"field index: cell " +
cell.sample_id +
" has a malformed shape " +
cell.shape,
)
continue
}
match declared_field(name) {
None =>
problems.push(
"field index: cell " +
cell.sample_id +
" uses the unknown field " +
name +
" (shape " +
cell.shape +
")",
)
Some(entry) =>
match expected_field_kind(cell.expected) {
Some(kind) =>
if kind != entry.kind {
problems.push(
"field index: cell " +
cell.sample_id +
"/" +
cell.shape +
" compares " +
name +
" as " +
field_kind_name(kind) +
", declared " +
field_kind_name(entry.kind),
)
}
None =>
if cell.group != "errors" {
problems.push(
"field index: cell " +
cell.sample_id +
"/" +
cell.shape +
" carries an error expectation outside the errors group",
)
}
}
}
if !seen.contains(name) {
seen.push(name)
}
}
for entry in align_field_index {
if !seen.contains(entry.name) {
problems.push("field index: no cell exercises the field " + entry.name)
}
}
if seen.length() != ALIGN_FIELD_COUNT {
problems.push(
"field index: " +
seen.length().to_string() +
" distinct field names observed, expected " +
ALIGN_FIELD_COUNT.to_string(),
)
}
problems
}
///|
/// Enforce the field index on the gate's execution path (AD section 4: the
/// gate looks expected values up by field name, and a name the index does not
/// know is an environment failure).
///
/// The check always reads the **delivered** table (`@gate_data.all_cells()`),
/// never the selected or injected cells: `--inject=` rewrites one
/// expectation in memory, and a perturbed expectation must not be able to
/// turn a healthy index into a reported problem.
///
/// On success nothing is printed, so the two-line stdout contract of AD
/// section 6 is untouched. On failure every problem is printed with the
/// `gate: ` diagnostic prefix used by the other environment failures, and the
/// process exits with 2 (AD section 3.2: a malformed table is an environment
/// problem, not a cell mismatch, so no failure record is written).
pub fn enforce_field_index() -> Unit {
let cells = @gate_data.all_cells()
let problems = field_index_problems(cells)
if problems.length() > 0 {
for problem in problems {
println("gate: " + problem)
}
exit_with_code(2)
}
}
///|
/// The A-11.3 evidence line, derived from the delivered table.
///
/// `fields=/19` is the headline: it is the number of declared fields
/// observed in the table, so a full run prints `fields=19/19`.
pub fn field_index_evidence(cells : Array[@gate_data.Cell]) -> String {
let seen : Array[String] = []
let mut counters = 0
let mut scores = 0
let mut lists = 0
for cell in cells {
let name = shape_field_name(cell.shape)
if name.length() == 0 || seen.contains(name) {
continue
}
seen.push(name)
match declared_field(name) {
Some(entry) =>
match entry.kind {
Counter => counters = counters + 1
Score => scores = scores + 1
WordList => lists = lists + 1
}
None => ()
}
}
"field index: fields=" +
seen.length().to_string() +
"/" +
ALIGN_FIELD_COUNT.to_string() +
" cells=" +
cells.length().to_string() +
" counter=" +
counters.to_string() +
" score=" +
scores.to_string() +
" word_list=" +
lists.to_string()
}