max / makenotwork
| 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 fs; |
| 33 | use ; |
| 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 | |
| 73 | let mut files = Vecnew; |
| 74 | collect |
| 75 | &new.join, |
| 76 | &mut files, |
| 77 | ; |
| 78 | files.sort; |
| 79 | files |
| 80 | |
| 81 | |
| 82 | |
| 83 | let Ok = read_dir else |
| 84 | return; |
| 85 | ; |
| 86 | for entry in entries |
| 87 | let path = entry.expect.path; |
| 88 | if path.is_dir |
| 89 | collect; |
| 90 | else if path.extension.and_then == Some |
| 91 | out.push; |
| 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 | |
| 101 | let root = new; |
| 102 | let mut found = Vecnew; |
| 103 | |
| 104 | for path in templates |
| 105 | let src = read_to_string.unwrap_or_default; |
| 106 | let shown = path |
| 107 | .strip_prefix |
| 108 | .unwrap_or |
| 109 | .display |
| 110 | .to_string; |
| 111 | for in src.lines.enumerate |
| 112 | for _ in 0..line.matches.count |
| 113 | found.push; |
| 114 | |
| 115 | |
| 116 | |
| 117 | |
| 118 | found |
| 119 | |
| 120 | |
| 121 | |
| 122 | |
| 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 |
| 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 | |
| 143 | |
| 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, |
| 151 | "item_details holds four of them: {found:?}" |
| 152 | ; |
| 153 | assert_eq! |
| 154 | found |
| 155 | .iter |
| 156 | .filter |
| 157 | .count, |
| 158 | 4, |
| 159 | "all four, not the file once: {found:?}" |
| 160 | ; |
| 161 | |
| 162 |