max / makenotwork
| 1 | //! Forward fence, the hand-written `hx-confirm` ratchet. |
| 2 | //! |
| 3 | //! No template writes `hx-confirm` by hand. Asking before a destructive |
| 4 | //! act is a property of the act, so it belongs in the description |
| 5 | //! (`quasi_router::Act::confirm`, `screen.rs:2787` with its builder at `:2932`), |
| 6 | //! where a terminal host asks in its own way and no renderer can forget to ask. |
| 7 | //! An attribute typed into Askama says it to one host only. |
| 8 | //! |
| 9 | //! Each remaining site leaves as its screen is described, so the condition is |
| 10 | //! met by the conversion program rather than by an edit. What this seal does is |
| 11 | //! make that monotone: a new template writing the attribute by hand fails here, |
| 12 | //! and every conversion lowers the number. |
| 13 | //! |
| 14 | //! # What this seal is not |
| 15 | //! |
| 16 | //! It does not, and cannot, say which acts destroy something. A danger class |
| 17 | //! near a confirm proves nothing in either direction: |
| 18 | //! `templates/partials/tabs/item_details.html` carries four confirms and no |
| 19 | //! danger class while three of its four acts are destructive, and |
| 20 | //! `templates/partials/admin_user_entries.html` carries four danger classes for |
| 21 | //! four confirms while two of those acts restore rather than destroy. Some |
| 22 | //! confirms are non-destructive and keep their confirm without taking |
| 23 | //! `Tone::Danger`. So a converting change reads its own sites; the markup is not |
| 24 | //! evidence. |
| 25 | //! |
| 26 | //! The converted side is asserted where it is written, not here: each described |
| 27 | //! screen's own tests check that its destructive acts carry both marks and that |
| 28 | //! its non-destructive ones carry neither (`src/quasi/ssh_keys.rs`, |
| 29 | //! `library_contacts.rs`, `forum_memberships.rs`). |
| 30 | |
| 31 | use fs; |
| 32 | use ; |
| 33 | |
| 34 | /// Ratchets down only, never up. |
| 35 | /// |
| 36 | /// The seal counts template source, so deduplicating two templates lowers it |
| 37 | /// without describing anything. Read it against what moved before treating it |
| 38 | /// as a progress figure. |
| 39 | /// |
| 40 | /// 43 to 30: thirteen sites left through glue modules, which is the whole of |
| 41 | /// what a described act can say against a route the description layer does not |
| 42 | /// serve. `session_acts` took the three in `tabs/user_sessions.html`, |
| 43 | /// `promo_code_acts` the two in `partials/promo_codes_list.html`, `repo_acts` |
| 44 | /// the two in `tabs/project_code.html`, `library_acts` the two in |
| 45 | /// `tabs/library_collections.html`, and `clip_acts`, `license_key_act`, |
| 46 | /// `cart_act` and `link_remove_act` one each from `partials/insertion_list.html`, |
| 47 | /// `partials/item_license_keys.html`, `pages/cart.html` and |
| 48 | /// `partials/link_row.html`. `clip_acts` also took the Remove in |
| 49 | /// `partials/placement_list.html`, which the seal never counted: it is |
| 50 | /// destructive with no confirm, and describing it beside its asking twin is |
| 51 | /// where the ruling's separation is proved. |
| 52 | /// |
| 53 | /// `link_row.html` was one of the two the shape recorded as unsayable. |
| 54 | /// `Action::replacing_enclosing` says it now. The other, `partials/tag.html`, |
| 55 | /// stays: its button reads a glyph and takes its name from `aria-label`, and a |
| 56 | /// described act renders its label as text, so converting it would move a |
| 57 | /// prompt and lose a name. |
| 58 | /// |
| 59 | /// What is left splits four ways, none of it a matter of effort. Measured |
| 60 | /// 2026-09-01 by reading the verb, target and swap at each of the 30: |
| 61 | /// |
| 62 | /// - Ten carry no `hx-target` and refresh, reload or navigate through |
| 63 | /// `data-after` glue. `Action::invalidating` emits `data-replaces` and |
| 64 | /// nothing under `frontend/src` performs it, so describing one today would |
| 65 | /// be describing a control that stops working. |
| 66 | /// - Ten answer into a region with `hx-swap="innerHTML"`: the four admin |
| 67 | /// entry partials, both admin dashboards, the broadcast result, and the two |
| 68 | /// dual-serve fragments below. Every described act emits |
| 69 | /// `hx-swap="outerMorph"`, so the container would be replaced by its own |
| 70 | /// contents and the next press would find no target. |
| 71 | /// - Five are submit buttons inside a form that carries the verb. An act is a |
| 72 | /// control with an action of its own, so these convert with their form. |
| 73 | /// - Five are neither: two tag glyphs (above), the two domain rows in |
| 74 | /// `tabs/user_profile.html`, which target `closest .form-section` and also |
| 75 | /// run a `data-after` verb, and the Remove in `tabs/library_purchases.html`, |
| 76 | /// which is a row of a context menu rather than a button standing among |
| 77 | /// others. |
| 78 | /// |
| 79 | /// `partials/ssh_keys_list.html` and `partials/git_tokens_list.html` are not |
| 80 | /// this program's to remove either: they are not tab renderings but the |
| 81 | /// fragments `routes::api::ssh_keys` and the git-token routes answer with, and |
| 82 | /// the described screens still call those. |
| 83 | const HIGH_WATER: usize = 30; |
| 84 | |
| 85 | /// Every template, recursively. Walked rather than listed for the reason the |
| 86 | /// frontend-globals seal walks `frontend/src`: a fence that has to be told about |
| 87 | /// each new file silently stops covering the tree. |
| 88 | |
| 89 | let mut files = Vecnew; |
| 90 | collect |
| 91 | &new.join, |
| 92 | &mut files, |
| 93 | ; |
| 94 | files.sort; |
| 95 | files |
| 96 | |
| 97 | |
| 98 | |
| 99 | let Ok = read_dir else |
| 100 | return; |
| 101 | ; |
| 102 | for entry in entries |
| 103 | let path = entry.expect.path; |
| 104 | if path.is_dir |
| 105 | collect; |
| 106 | else if path.extension.and_then == Some |
| 107 | out.push; |
| 108 | |
| 109 | |
| 110 | |
| 111 | |
| 112 | /// Where the attribute is written, one line per site, sorted by path. |
| 113 | /// |
| 114 | /// The failure message carries the list rather than a bare number, because a |
| 115 | /// ratchet that only says "it rose" leaves the reader to find the site. |
| 116 | |
| 117 | let root = new; |
| 118 | let mut found = Vecnew; |
| 119 | |
| 120 | for path in templates |
| 121 | let src = read_to_string.unwrap_or_default; |
| 122 | let shown = path |
| 123 | .strip_prefix |
| 124 | .unwrap_or |
| 125 | .display |
| 126 | .to_string; |
| 127 | for in src.lines.enumerate |
| 128 | for _ in 0..line.matches.count |
| 129 | found.push; |
| 130 | |
| 131 | |
| 132 | |
| 133 | |
| 134 | found |
| 135 | |
| 136 | |
| 137 | |
| 138 | |
| 139 | let found = sites; |
| 140 | let count = found.len; |
| 141 | |
| 142 | assert! |
| 143 | count <= HIGH_WATER, |
| 144 | "hand-written hx-confirm rose to {count} (HIGH_WATER {HIGH_WATER}).\n\ |
| 145 | Asking before an act is the act's own property: say it with \ |
| 146 | Act::confirm in the described screen, not as an attribute in Askama. \ |
| 147 | If you CONVERTED sites, lower HIGH_WATER to {count}.\n{}", |
| 148 | found.join |
| 149 | ; |
| 150 | |
| 151 | assert_eq! |
| 152 | count, HIGH_WATER, |
| 153 | "hand-written hx-confirm fell to {count}. Lower HIGH_WATER to {count}, \ |
| 154 | and say in its doc comment which screen took them.", |
| 155 | ; |
| 156 | |
| 157 | |
| 158 | |
| 159 | |
| 160 | // Four files hold more than one, so a fence counting files rather than |
| 161 | // occurrences would report a conversion that removed two of four as |
| 162 | // progress on neither. The count is of sites. |
| 163 | let found = sites; |
| 164 | |
| 165 | assert! |
| 166 | found.iter.any, |
| 167 | "item_details holds four of them: {found:?}" |
| 168 | ; |
| 169 | assert_eq! |
| 170 | found |
| 171 | .iter |
| 172 | .filter |
| 173 | .count, |
| 174 | 4, |
| 175 | "all four, not the file once: {found:?}" |
| 176 | ; |
| 177 | |
| 178 |