Extract the gesture vocabulary from the code that implements it
Every gesture the application has was documented in the comment beside the `TouchArea` that implements it. Excellent comments, and unreachable by anyone not reading the source — which is the FR-UI-4 failure in a different costume: a gesture nobody can find is a feature only its author knows about. Writing them out again in a hand-kept help page is the failure this avoids. Two descriptions of one gesture drift, and it is always the prose that drifts: the code is exercised every time somebody uses the application and the page is exercised never. A help screen confidently describing a double tap the grid stopped honouring last week is worse than no help screen — and the grid did stop honouring one, in the commit before this. So the comment beside the implementation stays the only copy, and a `GESTURE:` block beside it is scanned into two artefacts: `docs/gestures.md` for a reader, and a Rust table for the application to draw a help sheet from. Both committed, both gated, so neither can quietly stop describing the code. It lives in the traceability crate because it is the same operation on the same input — walk the tree, pull structured tags out of comments, render, fail if the committed artefact has moved. Only the vocabulary is new. It scans `ui` and `apps` alone: a gesture needs an interface to be performed on, and excluding `tools` is also what stops the scanner extracting its own worked examples as broken gestures. Fifteen gestures so far, across the library grid and the People screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,13 @@
|
||||
//! traces report # write docs/traceability.md
|
||||
//! traces json # machine-readable, to stdout
|
||||
//! traces check # gate: non-zero exit on failure
|
||||
//! traces gestures # write docs/gestures.md and the app's gesture table
|
||||
//! traces gestures-check # gate: non-zero exit if either has drifted
|
||||
//! ```
|
||||
//!
|
||||
//! The gesture half scans a different tag out of the same files — see
|
||||
//! [`traceability::gestures`] for what it is and why it lives here.
|
||||
//!
|
||||
//! The gate fails hard on a *misconfigured run* — zero requirements parsed, or
|
||||
//! zero source files scanned — rather than reporting a plausible-looking 0%.
|
||||
//! A gate that cannot distinguish "nothing is tagged" from "I read nothing" is
|
||||
@@ -32,10 +37,36 @@ const SOURCE_ROOTS: &[&str] = &["core", "ui", "platform", "apps", "tools"];
|
||||
/// Structural failures below are unconditional and do not depend on this.
|
||||
const MIN_COVERAGE: f64 = 0.0;
|
||||
|
||||
/// Where the extracted gesture vocabulary is written.
|
||||
///
|
||||
/// Two artefacts from one scan: the document a person reads, and the table the
|
||||
/// application draws its help sheet from. Both committed, both gated, so
|
||||
/// neither can quietly stop describing the code.
|
||||
const GESTURE_DOC: &str = "docs/gestures.md";
|
||||
const GESTURE_TABLE: &str = "ui/dr-ui/src/gesture_book.rs";
|
||||
|
||||
/// Directories scanned for gestures.
|
||||
///
|
||||
/// Narrower than `SOURCE_ROOTS`, and not merely as an optimisation. A gesture
|
||||
/// is something a *user* performs, so it can only be declared where there is an
|
||||
/// interface to perform it on: `ui` and the two application shells. `core` has
|
||||
/// no pointer and no finger.
|
||||
///
|
||||
/// Excluding `tools` is what stops this scanner reading its own documentation.
|
||||
/// The tag has to appear in this crate — in the doc comment that teaches the
|
||||
/// format, and in the fixtures that test the parser — and every one of those
|
||||
/// appearances was being extracted as a broken gesture. A scanner that indexes
|
||||
/// its own examples is a scanner nobody can document.
|
||||
const GESTURE_ROOTS: &[&str] = &["ui", "apps"];
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let base = repo_root()?;
|
||||
let mode = std::env::args().nth(1).unwrap_or_else(|| "report".into());
|
||||
|
||||
if mode.starts_with("gestures") {
|
||||
return run_gestures(&base, mode == "gestures-check");
|
||||
}
|
||||
|
||||
let req_path = base.join("docs/requirements.md");
|
||||
let markdown = std::fs::read_to_string(&req_path)
|
||||
.with_context(|| format!("reading {}", req_path.display()))?;
|
||||
@@ -86,6 +117,80 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Scan the gesture vocabulary, and either write it or check it has not moved.
|
||||
///
|
||||
/// Writing and checking share every step but the last, so they are one function
|
||||
/// with a flag rather than two that could come to disagree about what the
|
||||
/// artefact should contain — which is the only way a gate like this fails
|
||||
/// dishonestly.
|
||||
fn run_gestures(base: &Path, check: bool) -> Result<()> {
|
||||
let files = collect_sources(base, GESTURE_ROOTS);
|
||||
if files.is_empty() {
|
||||
bail!("no source files scanned — misconfigured, not zero gestures");
|
||||
}
|
||||
|
||||
let mut found = Vec::new();
|
||||
let mut problems = Vec::new();
|
||||
for file in &files {
|
||||
let text = std::fs::read_to_string(file).unwrap_or_default();
|
||||
let rel = file
|
||||
.strip_prefix(base)
|
||||
.unwrap_or(file)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
// Never the table this command writes. It quotes the tag in its own
|
||||
// header, so scanning it fed every generated gesture back in as a
|
||||
// malformed one — and a generator that consumes its own output cannot
|
||||
// converge.
|
||||
if rel == GESTURE_TABLE {
|
||||
continue;
|
||||
}
|
||||
let (g, p) = gestures::extract_from_text(&text, &rel);
|
||||
found.extend(g);
|
||||
problems.extend(p);
|
||||
}
|
||||
|
||||
println!("files scanned {}", files.len());
|
||||
println!("gestures found {}", found.len());
|
||||
println!("places {}", gestures::by_section(&found).len());
|
||||
|
||||
if !problems.is_empty() {
|
||||
for p in &problems {
|
||||
println!(" {p}");
|
||||
}
|
||||
bail!("{} malformed gesture tag(s)", problems.len());
|
||||
}
|
||||
|
||||
let doc = gestures::render_markdown(&found);
|
||||
let table = gestures::render_rust(&found);
|
||||
|
||||
if check {
|
||||
// Read back rather than trusting a timestamp: the artefacts are
|
||||
// committed, and what matters is whether the file in the tree says what
|
||||
// the source says, however it got there.
|
||||
let mut stale = Vec::new();
|
||||
for (path, want) in [(GESTURE_DOC, &doc), (GESTURE_TABLE, &table)] {
|
||||
let have = std::fs::read_to_string(base.join(path)).unwrap_or_default();
|
||||
if have != *want {
|
||||
stale.push(path);
|
||||
}
|
||||
}
|
||||
if !stale.is_empty() {
|
||||
bail!(
|
||||
"{} is stale — run: cargo run -p traceability -- gestures",
|
||||
stale.join(" and ")
|
||||
);
|
||||
}
|
||||
println!("\ngesture gate: PASS");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
std::fs::write(base.join(GESTURE_DOC), &doc)?;
|
||||
std::fs::write(base.join(GESTURE_TABLE), &table)?;
|
||||
println!("\nwrote {GESTURE_DOC} and {GESTURE_TABLE}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Structural checks that fail regardless of the coverage threshold.
|
||||
fn gate(files: &[PathBuf], defined: &DefinedRequirements, cov: &Coverage) -> Result<()> {
|
||||
// A run that parsed nothing is misconfigured, not passing.
|
||||
|
||||
Reference in New Issue
Block a user