Skip to main content

max / makenotwork

Seal the hand-written hx-confirm count, and assert the two marks Task b279b9eb. Asking before an act and destroying something are marked separately: Act::confirm says the first, Tone::Danger the second, and the vocabulary already carries both, so the ruling adds no member. The converted side was already right and was not asserted. Every described act now says so in its own screen's tests: the SSH key Remove and the token Revoke carry both marks and the three forms beside them carry neither, the library-contacts Revoke carries both, and forum-memberships' Browse Communities carries neither. That last one is the check a blanket pass would have broken, since it would toll every act dangerous. The other half of the done condition, no template writing the attribute by hand, is the conversion program's to finish: 50 sites across 33 files, and three of those back screens that are already described and stay live while QUASI_SCREENS serves nobody. tests/handwritten_confirm.rs makes the number monotone instead, so a new hand-written confirm fails and every converted screen lowers the seal. It counts sites rather than files and names each one on failure, and it does not guess at destructiveness: only 29 of the 50 carry a danger class nearby and the class disagrees in both directions, so each converting change reads its own sites.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-20 19:12 UTC
Signed with PGP, not checked
Commit: 1bf5272e8c53b04485775e5261c8a0a88210e50a
Parent: 6584f46
4 files changed, +190 insertions, -0 deletions
@@ -369,6 +369,12 @@
369 369 // the sentence that survives verbatim rather than re-encoding it here.
370 370 assert!(configured.contains("joined any forum communities yet."));
371 371 assert!(configured.contains("Browse Communities"), "{configured}");
372 + // `b279b9eb`: the way out destroys nothing and interrupts nobody, so it
373 + // takes neither mark. Asserted rather than assumed because a blanket
374 + // pass over the acts in this tree would toll it dangerous, and the
375 + // separation is only real if the un-marked case is checked too.
376 + assert!(!configured.contains("data-tone"), "{configured}");
377 + assert!(!configured.contains("hx-confirm"), "{configured}");
372 378
373 379 // Multithreaded not configured at all: the same sentence, and no button
374 380 // pointing at an empty address.
@@ -372,6 +372,15 @@
372 372 html.contains("Revoke contact sharing with grace?"),
373 373 "{html}"
374 374 );
375 + // `b279b9eb`: asking first and being destructive are marked separately,
376 + // so a row that asks is not thereby dangerous and this act has to say
377 + // both. The tone is what a terminal host colours by; without it the
378 + // renderer would be back to reading the prompt string for a hint.
379 + assert_eq!(
380 + html.matches(r#"data-tone="danger""#).count(),
381 + 2,
382 + "revoking is destructive and says so: {html}"
383 + );
375 384 // Per row rather than one shared endpoint, which is the mistake a loop
376 385 // over rows makes when the id is read outside it.
377 386 assert!(
@@ -483,6 +483,41 @@
483 483 assert!(html.contains(&format!("hx-delete=\"{PATH}/keys/k2\"")));
484 484 }
485 485
486 + #[test]
487 + fn what_destroys_something_is_marked_destructive_and_nothing_else_is() {
488 + // `b279b9eb`: destructiveness is marked separately from asking first.
489 + // Both acts on this screen are destructive and both ask, so the screen
490 + // is the reference pair; what it also proves is the other side, that
491 + // three forms which write without destroying take no tone at all.
492 + let html = render(&pane(
493 + "max",
494 + &[key("k1", "SHA256:aaa")],
495 + &[TokenView {
496 + id: "t1".into(),
497 + name: "laptop".into(),
498 + scope: "read",
499 + expires: "Never".into(),
500 + last_used: "Never".into(),
501 + }],
502 + &[],
503 + ));
504 +
505 + assert_eq!(
506 + html.matches(r#"data-tone="danger""#).count(),
507 + 2,
508 + "removing a key and revoking a token, and nothing else: {html}"
509 + );
510 + // The pairing is the point rather than the count: every tone here sits
511 + // on an act that also asks.
512 + assert_eq!(
513 + html.matches("hx-confirm").count(),
514 + 2,
515 + "and both of them ask first: {html}"
516 + );
517 + assert!(html.contains("Remove this SSH key?"), "{html}");
518 + assert!(html.contains("Revoke this token?"), "{html}");
519 + }
520 +
486 521 #[test]
487 522 fn the_theme_picker_offers_every_theme_and_marks_the_chosen_one() {
488 523 let themes = crate::theming::console_theme_options(Some("nord"));
@@ -1,0 +1,140 @@
1 + //! Forward fence, the hand-written `hx-confirm` ratchet.
2 + //!
3 + //! Task `b279b9eb`, and the half of its done condition no single change can
4 + //! reach: "no template writes `hx-confirm` by hand". Asking before a destructive
5 + //! act is a property of the act, so it belongs in the description
6 + //! (`quasi_router::Act::confirm`, `screen.rs:2787` with its builder at `:2932`),
7 + //! where a terminal host asks in its own way and no renderer can forget to ask.
8 + //! An attribute typed into Askama says it to one host only.
9 + //!
10 + //! There are 50 of them across 33 files and every one leaves as its screen is
11 + //! described, so the condition is met by the conversion program rather than by
12 + //! an edit. What this seal does is make that monotone: a new template writing
13 + //! the attribute by hand fails here, and every conversion lowers the number.
14 + //!
15 + //! # What this seal is not
16 + //!
17 + //! It does not, and cannot, say which of the 50 acts destroy something. Only 29
18 + //! of the 50 carry a danger class within the six preceding lines, and the
19 + //! disagreement runs both ways: `templates/partials/tabs/item_details.html` has
20 + //! four confirms and no danger class at all while three of its four acts are
21 + //! destructive, and `templates/partials/admin_user_entries.html` has four danger
22 + //! classes for four confirms while two of those acts (Unlock at `:60`, Unsuspend
23 + //! at `:80`) restore rather than destroy. Ten of the fifty are non-destructive
24 + //! and keep their confirm without taking `Tone::Danger`. So a converting change
25 + //! reads its own sites; the markup is not evidence in either direction.
26 + //!
27 + //! The converted side is asserted where it is written, not here: each described
28 + //! screen's own tests check that its destructive acts carry both marks and that
29 + //! its non-destructive ones carry neither (`src/quasi/ssh_keys.rs`,
30 + //! `library_contacts.rs`, `forum_memberships.rs`).
31 +
32 + use std::fs;
33 + use std::path::{Path, PathBuf};
34 +
35 + /// Ratchets down only, never up.
36 + ///
37 + /// 50 across 33 files on 2026-08-20, the measurement the task was written
38 + /// against and unchanged since.
39 + ///
40 + /// Three of the fifty are not this program's to remove and will be the last to
41 + /// go: `partials/ssh_keys_list.html`, `partials/git_tokens_list.html` and
42 + /// `partials/tabs/library_contacts.html` back screens that are already
43 + /// described, and are still live Askama structs because the conversions are
44 + /// dual-serve while `QUASI_SCREENS` serves nobody. They leave with the templates
45 + /// when the switch ships, which is task `64b33b26`.
46 + const HIGH_WATER: usize = 50;
47 +
48 + /// Every template, recursively. Walked rather than listed for the reason the
49 + /// frontend-globals seal walks `frontend/src`: a fence that has to be told about
50 + /// each new file silently stops covering the tree.
51 + fn templates() -> Vec<PathBuf> {
52 + let mut files = Vec::new();
53 + collect(
54 + &Path::new(env!("CARGO_MANIFEST_DIR")).join("templates"),
55 + &mut files,
56 + );
57 + files.sort();
58 + files
59 + }
60 +
61 + fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
62 + let Ok(entries) = fs::read_dir(dir) else {
63 + return;
64 + };
65 + for entry in entries {
66 + let path = entry.expect("dir entry").path();
67 + if path.is_dir() {
68 + collect(&path, out);
69 + } else if path.extension().and_then(|e| e.to_str()) == Some("html") {
70 + out.push(path);
71 + }
72 + }
73 + }
74 +
75 + /// Where the attribute is written, one line per site, sorted by path.
76 + ///
77 + /// The failure message carries the list rather than a bare number, because a
78 + /// ratchet that only says "it rose" leaves the reader to find the site.
79 + fn sites() -> Vec<String> {
80 + let root = Path::new(env!("CARGO_MANIFEST_DIR"));
81 + let mut found = Vec::new();
82 +
83 + for path in templates() {
84 + let src = fs::read_to_string(&path).unwrap_or_default();
85 + let shown = path
86 + .strip_prefix(root)
87 + .unwrap_or(&path)
88 + .display()
89 + .to_string();
90 + for (index, line) in src.lines().enumerate() {
91 + for _ in 0..line.matches("hx-confirm").count() {
92 + found.push(format!("{shown}:{}", index + 1));
93 + }
94 + }
95 + }
96 +
97 + found
98 + }
99 +
100 + #[test]
101 + fn hand_written_confirms_do_not_grow() {
102 + let found = sites();
103 + let count = found.len();
104 +
105 + assert!(
106 + count <= HIGH_WATER,
107 + "hand-written hx-confirm rose to {count} (HIGH_WATER {HIGH_WATER}).\n\
108 + Asking before an act is the act's own property: say it with \
109 + Act::confirm in the described screen, not as an attribute in Askama. \
110 + If you CONVERTED sites, lower HIGH_WATER to {count}.\n{}",
111 + found.join("\n")
112 + );
113 +
114 + assert_eq!(
115 + count, HIGH_WATER,
116 + "hand-written hx-confirm fell to {count}. Lower HIGH_WATER to {count}, \
117 + and say in its doc comment which screen took them.",
118 + );
119 + }
120 +
121 + #[test]
122 + fn the_seal_reads_every_site_and_not_every_file() {
123 + // Four files hold more than one, so a fence counting files rather than
124 + // occurrences would report a conversion that removed two of four as
125 + // progress on neither. The count is of sites.
126 + let found = sites();
127 +
128 + assert!(
129 + found.iter().any(|site| site.contains("item_details.html")),
130 + "item_details holds four of them: {found:?}"
131 + );
132 + assert_eq!(
133 + found
134 + .iter()
135 + .filter(|site| site.contains("item_details.html"))
136 + .count(),
137 + 4,
138 + "all four, not the file once: {found:?}"
139 + );
140 + }