Skip to main content

max / makenotwork

6.6 KB · 162 lines History Blame Raw
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 ///
47 /// 48 on 2026-08-22, and the first two that left without their screens. The
48 /// identical "Delete this blog post?" button in `tabs/project_content.html` and
49 /// `tabs/project_blog.html` is described once in
50 /// `crate::quasi::blog_delete_act` and called from both, which is what the
51 /// glue-module ruling (`27d5e5b8`) bought: a shared act converts at its own
52 /// granularity rather than once per screen. Both take `Tone::Danger`, which the
53 /// templates were saying as `class="danger-text"` to the stylesheet alone.
54 ///
55 /// The two Remove buttons in `partials/link_row.html` and `partials/tag.html`
56 /// were candidates in the same pass and are deliberately not converted: both
57 /// target `hx-target="closest .<class>"`, and a relative selector is not
58 /// something `Action::replaces` can say. Filed rather than approximated.
59 ///
60 /// 47 on 2026-08-23, and NOT a conversion: `tabs/user_profile.html` stopped
61 /// respelling `partials/link_row.html` and includes it instead, so the second
62 /// hand-written copy of "Remove this link?" is gone while the rendered page
63 /// still draws exactly one per row. Worth knowing when reading this number as
64 /// progress: the seal counts template source, so deduplicating two templates
65 /// lowers it without describing anything. The surviving site is still blocked
66 /// on the `closest .link-row` target above.
67 const HIGH_WATER: usize = 47;
68
69 /// Every template, recursively. Walked rather than listed for the reason the
70 /// frontend-globals seal walks `frontend/src`: a fence that has to be told about
71 /// each new file silently stops covering the tree.
72 fn templates() -> Vec<PathBuf> {
73 let mut files = Vec::new();
74 collect(
75 &Path::new(env!("CARGO_MANIFEST_DIR")).join("templates"),
76 &mut files,
77 );
78 files.sort();
79 files
80 }
81
82 fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
83 let Ok(entries) = fs::read_dir(dir) else {
84 return;
85 };
86 for entry in entries {
87 let path = entry.expect("dir entry").path();
88 if path.is_dir() {
89 collect(&path, out);
90 } else if path.extension().and_then(|e| e.to_str()) == Some("html") {
91 out.push(path);
92 }
93 }
94 }
95
96 /// Where the attribute is written, one line per site, sorted by path.
97 ///
98 /// The failure message carries the list rather than a bare number, because a
99 /// ratchet that only says "it rose" leaves the reader to find the site.
100 fn sites() -> Vec<String> {
101 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
102 let mut found = Vec::new();
103
104 for path in templates() {
105 let src = fs::read_to_string(&path).unwrap_or_default();
106 let shown = path
107 .strip_prefix(root)
108 .unwrap_or(&path)
109 .display()
110 .to_string();
111 for (index, line) in src.lines().enumerate() {
112 for _ in 0..line.matches("hx-confirm").count() {
113 found.push(format!("{shown}:{}", index + 1));
114 }
115 }
116 }
117
118 found
119 }
120
121 #[test]
122 fn hand_written_confirms_do_not_grow() {
123 let found = sites();
124 let count = found.len();
125
126 assert!(
127 count <= HIGH_WATER,
128 "hand-written hx-confirm rose to {count} (HIGH_WATER {HIGH_WATER}).\n\
129 Asking before an act is the act's own property: say it with \
130 Act::confirm in the described screen, not as an attribute in Askama. \
131 If you CONVERTED sites, lower HIGH_WATER to {count}.\n{}",
132 found.join("\n")
133 );
134
135 assert_eq!(
136 count, HIGH_WATER,
137 "hand-written hx-confirm fell to {count}. Lower HIGH_WATER to {count}, \
138 and say in its doc comment which screen took them.",
139 );
140 }
141
142 #[test]
143 fn the_seal_reads_every_site_and_not_every_file() {
144 // Four files hold more than one, so a fence counting files rather than
145 // occurrences would report a conversion that removed two of four as
146 // progress on neither. The count is of sites.
147 let found = sites();
148
149 assert!(
150 found.iter().any(|site| site.contains("item_details.html")),
151 "item_details holds four of them: {found:?}"
152 );
153 assert_eq!(
154 found
155 .iter()
156 .filter(|site| site.contains("item_details.html"))
157 .count(),
158 4,
159 "all four, not the file once: {found:?}"
160 );
161 }
162