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 were called out as not this program's to remove: |
| 41 | /// `partials/ssh_keys_list.html`, `partials/git_tokens_list.html` and |
| 42 | /// `partials/tabs/library_contacts.html`, all backing screens that were already |
| 43 | /// described and dual-serving while the switch served nobody. `64b33b26` shipped |
| 44 | /// the switch's deletion and took `library_contacts.html` with it, which is the |
| 45 | /// 47 to 46 below. The other two stay: they are not tab renderings but the |
| 46 | /// fragments `routes::api::ssh_keys` and the git-token routes answer with, and |
| 47 | /// the described screens still call those. |
| 48 | /// |
| 49 | /// 48 on 2026-08-22, and the first two that left without their screens. The |
| 50 | /// identical "Delete this blog post?" button in `tabs/project_content.html` and |
| 51 | /// `tabs/project_blog.html` is described once in |
| 52 | /// `crate::quasi::blog_delete_act` and called from both, which is what the |
| 53 | /// glue-module ruling (`27d5e5b8`) bought: a shared act converts at its own |
| 54 | /// granularity rather than once per screen. Both take `Tone::Danger`, which the |
| 55 | /// templates were saying as `class="danger-text"` to the stylesheet alone. |
| 56 | /// |
| 57 | /// The two Remove buttons in `partials/link_row.html` and `partials/tag.html` |
| 58 | /// were candidates in the same pass and are deliberately not converted: both |
| 59 | /// target `hx-target="closest .<class>"`, and a relative selector is not |
| 60 | /// something `Action::replaces` can say. Filed rather than approximated. |
| 61 | /// |
| 62 | /// 47 on 2026-08-23, and NOT a conversion: `tabs/user_profile.html` stopped |
| 63 | /// respelling `partials/link_row.html` and includes it instead, so the second |
| 64 | /// hand-written copy of "Remove this link?" is gone while the rendered page |
| 65 | /// still draws exactly one per row. Worth knowing when reading this number as |
| 66 | /// progress: the seal counts template source, so deduplicating two templates |
| 67 | /// lowers it without describing anything. The surviving site is still blocked |
| 68 | /// on the `closest .link-row` target above. |
| 69 | /// 44 on 2026-08-26, and this one was late: the six-panel tier-1 batch |
| 70 | /// (`16d8cac4`..`14254786`) deleted `tabs/project_members.html` and |
| 71 | /// `tabs/user_projects.html`, each carrying one, and nobody lowered the number. |
| 72 | /// The batch ran lib tests only, and this seal is an integration-directory test, |
| 73 | /// so it went unread until the suite ran on astra. Same shape as the globals |
| 74 | /// seal in `8ed9ad79`, and the same lesson: a ratchet nobody runs is not a |
| 75 | /// ratchet. Both are now `Act::confirm` -- "Remove {name} from this project?" in |
| 76 | /// `crate::quasi::project_members` and the delete confirm in |
| 77 | /// `crate::quasi::user_projects`. |
| 78 | /// |
| 79 | /// 43 on 2026-08-26, `b25dd957`: `tabs/item_sales.html` is described as |
| 80 | /// `crate::quasi::item_sales`, and its Refund button's "Issue a full refund for |
| 81 | /// {amount}? This cannot be undone." is `Act::confirm` with `Tone::Danger`. |
| 82 | const HIGH_WATER: usize = 43; |
| 83 | |
| 84 | /// Every template, recursively. Walked rather than listed for the reason the |
| 85 | /// frontend-globals seal walks `frontend/src`: a fence that has to be told about |
| 86 | /// each new file silently stops covering the tree. |
| 87 | |
| 88 | let mut files = Vecnew; |
| 89 | collect |
| 90 | &new.join, |
| 91 | &mut files, |
| 92 | ; |
| 93 | files.sort; |
| 94 | files |
| 95 | |
| 96 | |
| 97 | |
| 98 | let Ok = read_dir else |
| 99 | return; |
| 100 | ; |
| 101 | for entry in entries |
| 102 | let path = entry.expect.path; |
| 103 | if path.is_dir |
| 104 | collect; |
| 105 | else if path.extension.and_then == Some |
| 106 | out.push; |
| 107 | |
| 108 | |
| 109 | |
| 110 | |
| 111 | /// Where the attribute is written, one line per site, sorted by path. |
| 112 | /// |
| 113 | /// The failure message carries the list rather than a bare number, because a |
| 114 | /// ratchet that only says "it rose" leaves the reader to find the site. |
| 115 | |
| 116 | let root = new; |
| 117 | let mut found = Vecnew; |
| 118 | |
| 119 | for path in templates |
| 120 | let src = read_to_string.unwrap_or_default; |
| 121 | let shown = path |
| 122 | .strip_prefix |
| 123 | .unwrap_or |
| 124 | .display |
| 125 | .to_string; |
| 126 | for in src.lines.enumerate |
| 127 | for _ in 0..line.matches.count |
| 128 | found.push; |
| 129 | |
| 130 | |
| 131 | |
| 132 | |
| 133 | found |
| 134 | |
| 135 | |
| 136 | |
| 137 | |
| 138 | let found = sites; |
| 139 | let count = found.len; |
| 140 | |
| 141 | assert! |
| 142 | count <= HIGH_WATER, |
| 143 | "hand-written hx-confirm rose to {count} (HIGH_WATER {HIGH_WATER}).\n\ |
| 144 | Asking before an act is the act's own property: say it with \ |
| 145 | Act::confirm in the described screen, not as an attribute in Askama. \ |
| 146 | If you CONVERTED sites, lower HIGH_WATER to {count}.\n{}", |
| 147 | found.join |
| 148 | ; |
| 149 | |
| 150 | assert_eq! |
| 151 | count, HIGH_WATER, |
| 152 | "hand-written hx-confirm fell to {count}. Lower HIGH_WATER to {count}, \ |
| 153 | and say in its doc comment which screen took them.", |
| 154 | ; |
| 155 | |
| 156 | |
| 157 | |
| 158 | |
| 159 | // Four files hold more than one, so a fence counting files rather than |
| 160 | // occurrences would report a conversion that removed two of four as |
| 161 | // progress on neither. The count is of sites. |
| 162 | let found = sites; |
| 163 | |
| 164 | assert! |
| 165 | found.iter.any, |
| 166 | "item_details holds four of them: {found:?}" |
| 167 | ; |
| 168 | assert_eq! |
| 169 | found |
| 170 | .iter |
| 171 | .filter |
| 172 | .count, |
| 173 | 4, |
| 174 | "all four, not the file once: {found:?}" |
| 175 | ; |
| 176 | |
| 177 |