Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 14 additions & 11 deletions apps/jbotci-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2846,21 +2846,21 @@ mod tests {
.expect("tersmu XML text");
// With definitions on (the default), tersmu returns one well-formed
// XML document: structured WORD cards live in the WORDS section
// following KEY, not in a prepended text block.
// (the root's first child, after the KEY comment; jbotci#719), not in
// a prepended text block.
assert!(
tersmu_text.starts_with("<SFN "),
"the default MCP tersmu document is a single XML document"
tersmu_text.starts_with("<!--\n"),
"the default MCP tersmu document opens with the KEY comment"
);
roxmltree::Document::parse(tersmu_text).expect("default MCP XML parses");
assert!(tersmu_text.contains("<KEY>"));
let after_key = tersmu_text
.split_once("</KEY>")
.expect("KEY closes before WORDS")
let after_root = tersmu_text
.split_once("\n<SFN ")
.expect("root follows the KEY comment")
.1;
assert!(after_key.starts_with("\n <WORDS>"));
let words_section = after_key
assert!(after_root.contains(">\n <WORDS>\n"));
let words_section = after_root
.split_once("</WORDS>")
.expect("WORDS closes before WAIVERS")
.expect("WORDS section closes")
.0;
assert!(words_section.contains("<WORD ID=\"klama\">"));
assert!(words_section.contains("<GLOSS>"));
Expand Down Expand Up @@ -2922,7 +2922,10 @@ mod tests {
let tersmu_xml_text = tersmu_xml["result"]["content"][0]["text"]
.as_str()
.expect("tersmu XML text");
assert!(tersmu_xml_text.starts_with("<SFN VERSION=\"0\" DOC=\"&lt;input&gt;\">"));
assert!(tersmu_xml_text.starts_with("<!--\n"));
assert!(
tersmu_xml_text.contains("\n<SFN VERSION=\"0\" DOC=\"&lt;input&gt;\">")
);
assert!(tersmu_xml_text.contains("<UTTERANCE FORCE=\"ASSERT\" GROUND=\"g1\">"));

let tersmu_json = post_json(
Expand Down
20 changes: 12 additions & 8 deletions apps/jbotci/src/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1980,24 +1980,28 @@ mod tests {
assert_eq!(grounded.status, ToolStatus::Success);
let grounded = grounded.stdout_text().expect("UTF-8 grounded XML");
let ungrounded = ungrounded.stdout_text().expect("UTF-8 ungrounded XML");
// One well-formed document: no prepended text cards.
assert!(grounded.starts_with("<SFN "), "{grounded}");
// One well-formed document: no prepended text cards; the KEY teaching
// text is a single comment before the root element (jbotci#719).
assert!(grounded.starts_with("<!--\n"), "{grounded}");
assert!(!grounded.contains("1. klama | by: officialdata"));
assert!(grounded.ends_with("</SFN>\n"));
roxmltree::Document::parse(grounded).expect("grounded output is one XML document");
// The WORDS section follows the KEY and carries the klama card with
// place markup inside DEF.
// The WORDS section is the first child of the root and carries the
// klama card with place markup inside DEF.
assert!(
grounded.contains("</KEY>\n <WORDS>\n"),
"WORDS must immediately follow KEY: {grounded}"
grounded.contains("\n<SFN VERSION=\"0\" DOC=\"&lt;input&gt;\">\n <WORDS>\n"),
"WORDS must be the first child of the root: {grounded}"
);
assert!(grounded.contains("<WORD ID=\"klama\">"), "{grounded}");
assert!(grounded.contains("<GLOSS>"), "{grounded}");
assert!(grounded.contains("<ARG INDEX=\"1\"/>"), "{grounded}");
// The body after the section is byte-identical to the ungrounded body.
let grounded_body = grounded.split_once("</WORDS>").expect("WORDS section").1;
let ungrounded_body = ungrounded.split_once("</KEY>").expect("KEY").1;
assert_eq!(grounded_body, ungrounded_body);
let ungrounded_body = ungrounded
.split_once("\n<SFN VERSION=\"0\" DOC=\"&lt;input&gt;\">\n")
.expect("ungrounded root after comment")
.1;
assert_eq!(grounded_body, format!("\n{ungrounded_body}"));
}

#[test]
Expand Down
20 changes: 12 additions & 8 deletions apps/jbotci/tests/support/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -906,9 +906,12 @@ fn tersmu_smusni_cli_output_has_a_single_trailing_newline() {
fn tersmu_xml_cli_output_is_one_canonical_document() {
let run = run_cli_capture(&["jbotci", "tersmu", "--format", "xml", "mi klama"], false);
assert_eq!(run.status, CliStatus::Success);
// jbotci#719: the document opens with the KEY teaching comment.
assert!(run.stdout.starts_with("<!--
"));
assert!(
run.stdout
.starts_with("<SFN VERSION=\"0\" DOC=\"&lt;input&gt;\">")
.contains("\n<SFN VERSION=\"0\" DOC=\"&lt;input&gt;\">")
);
assert!(
run.stdout
Expand All @@ -933,19 +936,20 @@ fn tersmu_show_defs_embeds_a_words_section_in_the_xml_document() {
"cu",
"barda",
]);
// One well-formed document: no prepended text cards.
// One well-formed document: no prepended text cards; the KEY teaching
// text is a single comment before the root element (jbotci#719).
assert!(
output.starts_with("<SFN VERSION=\"0\" DOC=\"&lt;input&gt;\">"),
"output must be a single SFN document: {}",
output.starts_with("<!--\n"),
"output must open with the KEY comment: {}",
&output[..output.len().min(200)]
);
assert!(!output.contains("1. barda | by: officialdata"));
assert!(!output.contains("cmavo:"));
// The WORDS section follows the KEY and carries the barda card with
// glosses and place markup inside DEF.
// The WORDS section is the first child of the root and carries the barda
// card with glosses and place markup inside DEF.
assert!(
output.contains("</KEY>\n <WORDS>\n"),
"WORDS must immediately follow KEY: {output}"
output.contains("\n<SFN VERSION=\"0\" DOC=\"&lt;input&gt;\">\n <WORDS>\n"),
"WORDS must be the first child of the root: {output}"
);
assert!(output.contains("<WORD ID=\"barda\">"), "{output}");
assert!(output.contains("<GLOSS>big</GLOSS>"), "{output}");
Expand Down
1 change: 1 addition & 0 deletions crates/jbotci-semantics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ bityzba = { workspace = true, features = ["contract_scanner"] }
[dev-dependencies]
jbotci-dictionary-data.workspace = true
jbotci-dialect.workspace = true
roxmltree.workspace = true
sha2.workspace = true

[lints]
Expand Down
182 changes: 182 additions & 0 deletions crates/jbotci-semantics/examples/regen_goldens.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//! Regenerate the SFN golden files after an intentional output-shape change
//! (jbotci#719): the 48 xml_corpus `*.xml.txt` documents (frozen JSON plus
//! injected binder universes), the 50 phaseb `*.smusni.txt` /
//! `*.smusni-prov.txt` quartets (rebuilt from `.lojban` through the product
//! pipeline), and the four xml_focused_regressions `*.xml.txt` documents
//! (public typed path, DOC name swapped to the document id).
//!
//! Usage: `regen_goldens <xml-corpus|smusni|focused>`; prints one line per
//! written file. The mechanical-diff verifier
//! (`scripts/verify_issue_719_output_migration.py`) then proves the golden
//! delta is exactly the intended shape change.

use std::path::PathBuf;

#[allow(unused_imports)]
use bityzba::{data, ensures, invariant, new, requires};
use jbotci_semantics::notation::{SmusniConfig, render_smusni, render_xml_value_for_tooling};
use jbotci_semantics::{
SemanticBuildOptions, SemanticGraph, build_generated_semantic_graph_with_dictionary_and_options,
render_xml,
};
use jbotci_syntax::{ParseOptions, parse_syntax_tree_generated_model_with_source_and_options};
use serde_json::Value;

#[requires(!text.is_empty())]
#[ensures(true)]
fn graph_for_text(text: &str) -> SemanticGraph {
let words = jbotci_morphology::segment_words_with_modifiers(text).expect("morphology");
let parsed = parse_syntax_tree_generated_model_with_source_and_options(
&words,
text,
&ParseOptions::default(),
)
.expect("syntax");
build_generated_semantic_graph_with_dictionary_and_options(
&parsed,
SemanticBuildOptions {
source_text: Some(text),
story_time: false,
},
jbotci_dictionary_data::english(),
)
.expect("semantics")
}

#[requires(true)]
#[ensures(true)]
fn manifest() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

#[requires(true)]
#[ensures(true)]
fn regen_xml_corpus() {
let corpus = manifest().join("tests/xml_corpus");
let binder_universes: Value = serde_json::from_slice(
&std::fs::read(corpus.join("BINDER_UNIVERSES.json")).expect("binder universes read"),
)
.expect("binder universes parse");
let mut documents: Vec<PathBuf> = std::fs::read_dir(&corpus)
.expect("corpus directory")
.filter_map(|entry| {
let path = entry.expect("corpus entry").path();
path.file_name()
.map(|name| name.to_string_lossy().into_owned())
.filter(|name| name.ends_with(".frozen.json"))
.map(|_| path)
})
.collect();
documents.sort();
assert_eq!(documents.len(), 48, "the xml corpus must stay at 48 documents");
for path in documents {
let document = path
.file_name()
.expect("document name")
.to_string_lossy()
.into_owned()
.replace(".frozen.json", "");
let mut graph: Value = serde_json::from_slice(
&std::fs::read(&path).unwrap_or_else(|error| panic!("read {}: {error}", path.display())),
)
.expect("frozen graph parses");
graph["scopeDependenceBinderUniverses"] = binder_universes[document.as_str()].clone();
let rendered = render_xml_value_for_tooling(graph, &document);
let target = corpus.join(format!("{document}.xml.txt"));
std::fs::write(&target, &rendered.output)
.unwrap_or_else(|error| panic!("write {}: {error}", target.display()));
println!("xml-corpus {document}");
}
}

#[requires(true)]
#[ensures(true)]
fn regen_smusni() {
let corpus = manifest().join("tests/phaseb_corpus");
let mut documents: Vec<PathBuf> = std::fs::read_dir(&corpus)
.expect("corpus directory")
.filter_map(|entry| {
let path = entry.expect("corpus entry").path();
path.file_name()
.map(|name| name.to_string_lossy().into_owned())
.filter(|name| name.ends_with(".lojban"))
.map(|_| path)
})
.collect();
documents.sort();
assert_eq!(
documents.len(),
50,
"the phaseb corpus must stay at 50 documents"
);
for path in documents {
let document = path
.file_name()
.expect("document name")
.to_string_lossy()
.into_owned()
.replace(".lojban", "");
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
let graph = graph_for_text(text.trim_end_matches('\n'));
let plain = render_smusni(&graph, SmusniConfig { provenance: false });
let provenance = render_smusni(&graph, SmusniConfig { provenance: true });
std::fs::write(corpus.join(format!("{document}.smusni.txt")), plain)
.expect("smusni golden write");
std::fs::write(
corpus.join(format!("{document}.smusni-prov.txt")),
provenance,
)
.expect("smusni provenance golden write");
println!("smusni {document}");
}
}

#[requires(true)]
#[ensures(true)]
fn regen_focused() {
let focused = manifest().join("tests/xml_focused_regressions");
let cases = [
(
"b59",
"content-first-question-scope",
"mi djuno lo ka ce'u klama makau",
),
(
"b60",
"content-first-question-scope",
"mi djica lo nu makau klama",
),
("b61", "referent-sort-abstraction", "mi facki lo ni ma kau clani"),
("b62", "sign-quotation", "mi cusku lu ro da klama li'u"),
];
for (document, group, text) in cases {
let graph = graph_for_text(text);
// Match the byte-parity test: render with the scope-dependence-first-
// visit label, then swap the DOC name to the document id.
let rendered = render_xml(&graph, "<scope-dependence-first-visit>");
let output = rendered.output.replacen(
"DOC=\"&lt;scope-dependence-first-visit&gt;\"",
&format!("DOC=\"{document}\""),
1,
);
let target = focused.join(group).join(format!("{document}.xml.txt"));
std::fs::write(&target, output)
.unwrap_or_else(|error| panic!("write {}: {error}", target.display()));
println!("focused {document}");
}
}

#[requires(true)]
#[ensures(true)]
fn main() {
let mode = std::env::args()
.nth(1)
.unwrap_or_else(|| panic!("usage: regen_goldens <xml-corpus|smusni|focused>"));
match mode.as_str() {
"xml-corpus" => regen_xml_corpus(),
"smusni" => regen_smusni(),
"focused" => regen_focused(),
other => panic!("unknown mode: {other}"),
}
}
Loading
Loading