epiphany/spikes/editor-toolkit/round2-textkit/src/bin/generate.rs

107 lines
4.3 KiB
Rust

//! `generate` — Packet 2A-i's entry point: resolves the declared faces,
//! shapes the five committed fixtures, asserts every W3 §5 invariant and
//! every recipe §4 precommitted expectation, then writes
//! `round2-textkit/fixtures.json` and `round2-textkit/FIXTURES_SUMMARY.md`.
//!
//! Exit behavior:
//! * A declared face **missing from disk** is an environment absence (pin
//! 14): prints `NOT RUN: <path>` for each missing face and exits `0`
//! without writing anything.
//! * A declared face **present but hash-mismatched**, or any invariant/
//! recipe-expectation disagreement, is a hard failure: `faces::resolve_one`
//! or `fixtures::build_fixture` panics, and this binary exits non-zero.
use round2_textkit::faces::{self, FaceResolution, LoadedFace};
use round2_textkit::fixtures::{self, FIXTURES};
use round2_textkit::output;
fn main() {
let resolved = faces::resolve_declared_chain();
let missing: Vec<&std::path::PathBuf> = resolved
.iter()
.filter_map(|r| match r {
FaceResolution::Missing { path } => Some(path),
FaceResolution::Loaded(_) => None,
})
.collect();
if !missing.is_empty() {
for path in &missing {
println!("NOT RUN: {}", path.display());
}
println!(
"{} of {} declared faces are absent from this machine (pin 14: environment absence, \
not a failure). No fixtures.json was written.",
missing.len(),
resolved.len()
);
return;
}
let loaded: Vec<LoadedFace> = resolved
.into_iter()
.map(|r| match r {
FaceResolution::Loaded(lf) => lf,
FaceResolution::Missing { .. } => unreachable!("handled above"),
})
.collect();
println!("Resolved {} declared faces:", loaded.len());
for (i, f) in loaded.iter().enumerate() {
println!(
" [{i}] {} — family {:?}, version {:?}, {} bytes",
f.path.display(),
f.identity.family,
f.identity.version,
f.bytes.len()
);
}
let mut built = Vec::with_capacity(FIXTURES.len());
for (ordinal, def) in FIXTURES.iter().enumerate() {
println!("Shaping {} ({}) ...", def.id, def.purpose);
let rt = fixtures::build_fixture(def, &loaded, ordinal as u64);
let total_glyphs: usize = rt.segments.iter().map(|s| s.glyphs.len()).sum();
println!(
" {} codepoints, {} bytes, {} segments, {} glyphs, {} clusters — OK",
rt.text.chars().count(),
rt.text.len(),
rt.segments.len(),
total_glyphs,
rt.clusters.clusters.len()
);
built.push((def.id.to_string(), def.purpose.to_string(), rt));
}
let file = output::build_fixture_file(&loaded, built)
.expect("every fixture must have a precommitted accessibility note");
// Printed BEFORE validation, and unconditionally: when the digest is what
// disagrees, the number needed to fix it must be on screen even though
// `validate()` is about to fail. Re-record it only after establishing why
// it changed — see `EXPECTED_ARTIFACT_DIGEST_HEX`'s doc comment.
let digest = output::artifact_digest(&file);
println!("artifact digest (sha256 of canonical JSON): {digest}");
if digest != output::expected_artifact_digest() {
println!(
" compiled-in expectation: {}",
output::expected_artifact_digest()
);
println!(" ^ these differ — validation below will fail until the constant is re-recorded");
}
file.validate()
.expect("freshly generated fixtures.json must pass its own validator");
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let json_path = manifest_dir.join("fixtures.json");
let summary_path = manifest_dir.join("FIXTURES_SUMMARY.md");
let json = serde_json::to_string_pretty(&file).expect("fixtures.json must serialize");
std::fs::write(&json_path, json).expect("write fixtures.json");
println!("wrote {}", json_path.display());
let summary = output::render_summary_markdown(&file);
std::fs::write(&summary_path, summary).expect("write FIXTURES_SUMMARY.md");
println!("wrote {}", summary_path.display());
}