max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01KD2dJJETtYs7R5kku6YyLk
212 files changed,
+2512 insertions,
-2 deletions
| @@ -2,6 +2,25 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Notable changes to tagtree. Versions follow semver. | |
| 4 | 4 | ||
| 5 | + | ## Unreleased | |
| 6 | + | ||
| 7 | + | ### Added | |
| 8 | + | ||
| 9 | + | - `pub mod oracle`: the property set the astra soak tier asserts, and the same set | |
| 10 | + | `tests/regressions.rs` replays on stable. Structural models for the hierarchy | |
| 11 | + | decomposition, a round-trip model for `escape_like`, an independent model of | |
| 12 | + | `validate_with`'s accept decision, and full reimplementations of `TagIndex::suggest` | |
| 13 | + | and `suggest_fuzzy`. The last of those scores with a naive full-matrix Levenshtein | |
| 14 | + | over bytes, which is a genuine differential against `edit_distance`'s single-row DP. | |
| 15 | + | - `fuzz/`: two libFuzzer targets, `tag` (the string grammar and its decomposition) and | |
| 16 | + | `index` (a line-oriented operation script over one `TagIndex`). Both take plain text, | |
| 17 | + | so the seed corpus is readable in a diff. | |
| 18 | + | - `fuzz/seeds/`: 130 production tag slugs taken from MNW's migrations, nine per-namespace | |
| 19 | + | family files, and 25 adversarial inputs, plus 36 index scripts. | |
| 20 | + | - `tests/regressions.rs`: replays every committed seed and every crash artifact through | |
| 21 | + | the oracle on stable, with a floor on the seed count so a silently emptied corpus | |
| 22 | + | cannot pass. | |
| 23 | + | ||
| 5 | 24 | ## 0.4.1 (2026-08-21) | |
| 6 | 25 | ||
| 7 | 26 | ### Changed |
| @@ -16,11 +16,11 @@ | |||
| 16 | 16 | ||
| 17 | 17 | | App | max_depth | max_length | semantic_depth | Notes | | |
| 18 | 18 | |-----|-----------|------------|----------------|-------| | |
| 19 | - | | AF | 5 | 100 | 1 | Deep hierarchy, namespace-driven (`genre.electronic.house`) | | |
| 19 | + | | AF | 5 | 100 | 0 | Deep hierarchy, namespace-driven (`genre.electronic.house`) | | |
| 20 | 20 | | GO | 3 | 60 | 0 | Shallow free-form tags | | |
| 21 | 21 | | BB | 3 | 80 | 0 | Shallow free-form tags | | |
| 22 | 22 | | MT | 3 | 64 | 0 | Community-scoped thread tags | | |
| 23 | - | | MNW | 5 | 100 | 0 | Content/project tags | | |
| 23 | + | | MNW | 5 | 100 | 2 | Content/project tags. The 2 is what forces the three-segment `type.category.value` slug shape on the platform | | |
| 24 | 24 | ||
| 25 | 25 | - `max_depth`: maximum number of segments allowed | |
| 26 | 26 | - `max_length`: maximum character length of the entire tag string |
| @@ -33,6 +33,8 @@ | |||
| 33 | 33 | ||
| 34 | 34 | use std::fmt; | |
| 35 | 35 | ||
| 36 | + | pub mod oracle; | |
| 37 | + | ||
| 36 | 38 | /// Separator between tag levels. | |
| 37 | 39 | pub const SEPARATOR: char = '.'; | |
| 38 | 40 |
| @@ -1,0 +1,4 @@ | |||
| 1 | + | target | |
| 2 | + | corpus | |
| 3 | + | artifacts | |
| 4 | + | coverage |
| @@ -1,0 +1,207 @@ | |||
| 1 | + | # This file is automatically @generated by Cargo. | |
| 2 | + | # It is not intended for manual editing. | |
| 3 | + | version = 4 | |
| 4 | + | ||
| 5 | + | [[package]] | |
| 6 | + | name = "arbitrary" | |
| 7 | + | version = "1.4.2" | |
| 8 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9 | + | checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" | |
| 10 | + | dependencies = [ | |
| 11 | + | "derive_arbitrary", | |
| 12 | + | ] | |
| 13 | + | ||
| 14 | + | [[package]] | |
| 15 | + | name = "cc" | |
| 16 | + | version = "1.4.4" | |
| 17 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 18 | + | checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" | |
| 19 | + | dependencies = [ | |
| 20 | + | "find-msvc-tools", | |
| 21 | + | "jobserver", | |
| 22 | + | "libc", | |
| 23 | + | "shlex", | |
| 24 | + | ] | |
| 25 | + | ||
| 26 | + | [[package]] | |
| 27 | + | name = "cfg-if" | |
| 28 | + | version = "1.0.4" | |
| 29 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 30 | + | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | |
| 31 | + | ||
| 32 | + | [[package]] | |
| 33 | + | name = "derive_arbitrary" | |
| 34 | + | version = "1.4.2" | |
| 35 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 36 | + | checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" | |
| 37 | + | dependencies = [ | |
| 38 | + | "proc-macro2", | |
| 39 | + | "quote", | |
| 40 | + | "syn", | |
| 41 | + | ] | |
| 42 | + | ||
| 43 | + | [[package]] | |
| 44 | + | name = "find-msvc-tools" | |
| 45 | + | version = "0.1.11" | |
| 46 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 47 | + | checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" | |
| 48 | + | ||
| 49 | + | [[package]] | |
| 50 | + | name = "getrandom" | |
| 51 | + | version = "0.4.3" | |
| 52 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 53 | + | checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" | |
| 54 | + | dependencies = [ | |
| 55 | + | "cfg-if", | |
| 56 | + | "libc", | |
| 57 | + | "r-efi", | |
| 58 | + | ] | |
| 59 | + | ||
| 60 | + | [[package]] | |
| 61 | + | name = "jobserver" | |
| 62 | + | version = "0.1.35" | |
| 63 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 64 | + | checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" | |
| 65 | + | dependencies = [ | |
| 66 | + | "getrandom", | |
| 67 | + | "libc", | |
| 68 | + | ] | |
| 69 | + | ||
| 70 | + | [[package]] | |
| 71 | + | name = "libc" | |
| 72 | + | version = "0.2.189" | |
| 73 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 74 | + | checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" | |
| 75 | + | ||
| 76 | + | [[package]] | |
| 77 | + | name = "libfuzzer-sys" | |
| 78 | + | version = "0.4.13" | |
| 79 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 80 | + | checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" | |
| 81 | + | dependencies = [ | |
| 82 | + | "arbitrary", | |
| 83 | + | "cc", | |
| 84 | + | ] | |
| 85 | + | ||
| 86 | + | [[package]] | |
| 87 | + | name = "proc-macro2" | |
| 88 | + | version = "1.0.107" | |
| 89 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 90 | + | checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" | |
| 91 | + | dependencies = [ | |
| 92 | + | "unicode-ident", | |
| 93 | + | ] | |
| 94 | + | ||
| 95 | + | [[package]] | |
| 96 | + | name = "quote" | |
| 97 | + | version = "1.0.47" | |
| 98 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 99 | + | checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" | |
| 100 | + | dependencies = [ | |
| 101 | + | "proc-macro2", | |
| 102 | + | ] | |
| 103 | + | ||
| 104 | + | [[package]] | |
| 105 | + | name = "r-efi" | |
| 106 | + | version = "6.0.0" | |
| 107 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 108 | + | checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" | |
| 109 | + | ||
| 110 | + | [[package]] | |
| 111 | + | name = "shlex" | |
| 112 | + | version = "2.0.1" | |
| 113 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 114 | + | checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" | |
| 115 | + | ||
| 116 | + | [[package]] | |
| 117 | + | name = "syn" | |
| 118 | + | version = "2.0.119" | |
| 119 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 120 | + | checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" | |
| 121 | + | dependencies = [ | |
| 122 | + | "proc-macro2", | |
| 123 | + | "quote", | |
| 124 | + | "unicode-ident", | |
| 125 | + | ] | |
| 126 | + | ||
| 127 | + | [[package]] | |
| 128 | + | name = "tagtree" | |
| 129 | + | version = "0.4.1" | |
| 130 | + | ||
| 131 | + | [[package]] | |
| 132 | + | name = "tagtree-fuzz" | |
| 133 | + | version = "0.0.0" | |
| 134 | + | dependencies = [ | |
| 135 | + | "libfuzzer-sys", | |
| 136 | + | "tagtree", | |
| 137 | + | ] | |
| 138 | + | ||
| 139 | + | [[package]] | |
| 140 | + | name = "unicode-ident" | |
| 141 | + | version = "1.0.24" | |
| 142 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 143 | + | checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" | |
| 144 | + | ||
| 145 | + | [[patch.unused]] | |
| 146 | + | name = "quasi-type" | |
| 147 | + | version = "0.1.3" | |
| 148 | + | ||
| 149 | + | [[patch.unused]] | |
| 150 | + | name = "kberg" | |
| 151 | + | version = "0.1.0" | |
| 152 | + | ||
| 153 | + | [[patch.unused]] | |
| 154 | + | name = "ops-status" | |
| 155 | + | version = "0.1.0" | |
| 156 | + | ||
| 157 | + | [[patch.unused]] | |
| 158 | + | name = "painhours" | |
| 159 | + | version = "0.1.0" | |
| 160 | + | ||
| 161 | + | [[patch.unused]] | |
| 162 | + | name = "quasi-axum" | |
| 163 | + | version = "0.81.0" | |
| 164 | + | ||
| 165 | + | [[patch.unused]] | |
| 166 | + | name = "quasi-basics" | |
| 167 | + | version = "0.81.0" | |
| 168 | + | ||
| 169 | + | [[patch.unused]] | |
| 170 | + | name = "quasi-http" | |
| 171 | + | version = "0.81.0" | |
| 172 | + | ||
| 173 | + | [[patch.unused]] | |
| 174 | + | name = "quasi-immediate" | |
| 175 | + | version = "0.81.0" | |
| 176 | + | ||
| 177 | + | [[patch.unused]] | |
| 178 | + | name = "quasi-notifs" | |
| 179 | + | version = "0.81.0" | |
| 180 | + | ||
| 181 | + | [[patch.unused]] | |
| 182 | + | name = "quasi-router" | |
| 183 | + | version = "0.81.0" | |
| 184 | + | ||
| 185 | + | [[patch.unused]] | |
| 186 | + | name = "quasi-store" | |
| 187 | + | version = "0.1.0" | |
| 188 | + | ||
| 189 | + | [[patch.unused]] | |
| 190 | + | name = "quasi-tauri" | |
| 191 | + | version = "0.81.0" | |
| 192 | + | ||
| 193 | + | [[patch.unused]] | |
| 194 | + | name = "quasi-webview" | |
| 195 | + | version = "0.81.0" | |
| 196 | + | ||
| 197 | + | [[patch.unused]] | |
| 198 | + | name = "synckit-client" | |
| 199 | + | version = "0.10.0" | |
| 200 | + | ||
| 201 | + | [[patch.unused]] | |
| 202 | + | name = "synckit-config" | |
| 203 | + | version = "0.2.0" | |
| 204 | + | ||
| 205 | + | [[patch.unused]] | |
| 206 | + | name = "docengine" | |
| 207 | + | version = "0.7.0" |
| @@ -1,0 +1,31 @@ | |||
| 1 | + | [package] | |
| 2 | + | name = "tagtree-fuzz" | |
| 3 | + | version = "0.0.0" | |
| 4 | + | publish = false | |
| 5 | + | edition = "2024" | |
| 6 | + | ||
| 7 | + | [package.metadata] | |
| 8 | + | cargo-fuzz = true | |
| 9 | + | ||
| 10 | + | [dependencies] | |
| 11 | + | # `arbitrary` with `derive` is what lets the targets take `&str` instead of | |
| 12 | + | # `&[u8]`. Both grammars are text, and byte input would spend most of the | |
| 13 | + | # fuzzer's budget rediscovering UTF-8 before it reached a dot. | |
| 14 | + | libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] } | |
| 15 | + | ||
| 16 | + | [dependencies.tagtree] | |
| 17 | + | path = ".." | |
| 18 | + | ||
| 19 | + | [[bin]] | |
| 20 | + | name = "tag" | |
| 21 | + | path = "fuzz_targets/tag.rs" | |
| 22 | + | test = false | |
| 23 | + | doc = false | |
| 24 | + | bench = false | |
| 25 | + | ||
| 26 | + | [[bin]] | |
| 27 | + | name = "index" | |
| 28 | + | path = "fuzz_targets/index.rs" | |
| 29 | + | test = false | |
| 30 | + | doc = false | |
| 31 | + | bench = false |
| @@ -1,0 +1,41 @@ | |||
| 1 | + | //! Structured fuzz over `TagIndex`: the mutable half of row 8. | |
| 2 | + | //! | |
| 3 | + | //! Split from the `tag` target because the input shape is different, not | |
| 4 | + | //! because the grammar is. There is one grammar, so this is not the | |
| 5 | + | //! `custom-pages` html/css split. But a tag string and an operation sequence | |
| 6 | + | //! over a mutable index cannot share a corpus without most of its bytes being | |
| 7 | + | //! wasted on the wrong half, and `tagtree` has zero runtime dependencies, so | |
| 8 | + | //! the second target costs a `witchbroom.toml` row and nothing else. | |
| 9 | + | //! | |
| 10 | + | //! The input is a line-oriented script, one operation per line, dispatched on | |
| 11 | + | //! the first character (`+` insert, `-` remove, `=` rebuild, `?` suggest, `~` | |
| 12 | + | //! suggest_fuzzy, `!` rename, `x` remove_subtree, `m` merge). One character of | |
| 13 | + | //! dispatch means a random mutation usually leaves a valid script, and it means | |
| 14 | + | //! the seeds are readable. | |
| 15 | + | //! | |
| 16 | + | //! ## Where the teeth are | |
| 17 | + | //! | |
| 18 | + | //! `suggest` and `suggest_fuzzy` are asserted against full reimplementations, | |
| 19 | + | //! not against invariants. `oracle::model_suggest` filters the whole tag list | |
| 20 | + | //! where the crate binary searches and breaks early. `model_suggest_fuzzy` | |
| 21 | + | //! scores tier 3 with a naive full-matrix Levenshtein where the crate uses a | |
| 22 | + | //! single-row DP with a length short-circuit and a per-row early exit. That | |
| 23 | + | //! last one is the only genuine differential the crate offers, and it aims | |
| 24 | + | //! directly at the mutants that survived in `edit_distance`. | |
| 25 | + | //! | |
| 26 | + | //! The reference Levenshtein works on **bytes**, because `edit_distance` calls | |
| 27 | + | //! `as_bytes`. A `chars()` reference would disagree on every non-ASCII input | |
| 28 | + | //! and would report correct behaviour as a finding. | |
| 29 | + | //! | |
| 30 | + | //! One invariant is deliberately weaker than it looks: the segment index is a | |
| 31 | + | //! *superset* of the segments of the live tags, never an equality. `remove` | |
| 32 | + | //! documents that it does not prune orphaned segments, so an equality assertion | |
| 33 | + | //! would fire on the first removal and every report after it would be noise. | |
| 34 | + | ||
| 35 | + | #![no_main] | |
| 36 | + | ||
| 37 | + | use libfuzzer_sys::fuzz_target; | |
| 38 | + | ||
| 39 | + | fuzz_target!(|text: &str| { | |
| 40 | + | tagtree::oracle::check_index_script(text); | |
| 41 | + | }); |
| @@ -1,0 +1,49 @@ | |||
| 1 | + | //! Structured fuzz over the tag grammar and its hierarchy decomposition. | |
| 2 | + | //! | |
| 3 | + | //! Row 8 of `astra-soak-overview`. The doors were counted before this was | |
| 4 | + | //! written, as infra `8910f917` instructs, and the count decided the shape: | |
| 5 | + | //! twelve files across five repos call `tagtree`, and every one of them reaches | |
| 6 | + | //! the same implementation. There is no second parser to extract, so one target | |
| 7 | + | //! covers the whole string surface. What the count did change is the *input*: | |
| 8 | + | //! five distinct `TagConfig` constants are live in the tree, and | |
| 9 | + | //! `oracle::CONFIGS` enumerates all five plus the degenerate ends. | |
| 10 | + | //! | |
| 11 | + | //! ## Why plain text, and not a derived `Arbitrary` struct | |
| 12 | + | //! | |
| 13 | + | //! The obvious shape is `(u8, u8, u8, &str, &str)`, mapping three bytes onto a | |
| 14 | + | //! config. It was rejected. The config is three small integers whose live | |
| 15 | + | //! values are known, so enumerating them costs one loop and dominates spending | |
| 16 | + | //! fuzzer budget rediscovering them; and taking text means the seed corpus is | |
| 17 | + | //! the 130 production tag slugs committed in MNW's migrations, as files a | |
| 18 | + | //! reviewer can read in a diff, rather than a directory of little-endian blobs | |
| 19 | + | //! with a hand-written decoder in the replay test. | |
| 20 | + | //! | |
| 21 | + | //! The input is one tag per line. Each line is checked against every config, | |
| 22 | + | //! and each adjacent pair of lines is checked through the relational half, so a | |
| 23 | + | //! two-line seed reaches `common_ancestor`, `subtree` and `rename_prefix`. | |
| 24 | + | //! | |
| 25 | + | //! ## The oracle lives in the crate, not here | |
| 26 | + | //! | |
| 27 | + | //! Everything asserted is `tagtree::oracle`. The committed replay in | |
| 28 | + | //! `tests/regressions.rs` calls the same functions on stable, so a crash found | |
| 29 | + | //! here becomes a unit test by copying one file, and neither side can drift | |
| 30 | + | //! into checking less than the other. | |
| 31 | + | //! | |
| 32 | + | //! What it asserts, in short: `depth`, `segment`, `prefix_at_depth`, | |
| 33 | + | //! `ancestors` and `parent`/`leaf` all agree with one segment split; | |
| 34 | + | //! `semantic_prefix` and `free_suffix` rejoin to the input; `escape_like` | |
| 35 | + | //! round-trips under its own documented rule and leaves no unescaped wildcard; | |
| 36 | + | //! and `validate_with`'s accept decision matches an independent model. | |
| 37 | + | //! | |
| 38 | + | //! Not-panicking is the weakest thing a fuzz target can assert. `validate_with` | |
| 39 | + | //! already has a proptest that never panics on arbitrary input, so a target | |
| 40 | + | //! that asserted only that would report clean forever while `escape_like` | |
| 41 | + | //! handed Postgres a bare `%`. | |
| 42 | + | ||
| 43 | + | #![no_main] | |
| 44 | + | ||
| 45 | + | use libfuzzer_sys::fuzz_target; | |
| 46 | + | ||
| 47 | + | fuzz_target!(|text: &str| { | |
| 48 | + | tagtree::oracle::check_tag_text(text); | |
| 49 | + | }); |
| @@ -1,0 +1,7 @@ | |||
| 1 | + | # Regression inputs | |
| 2 | + | ||
| 3 | + | One file per fuzz input that once found a bug. `tests/regressions.rs` replays | |
| 4 | + | every file here through both oracles on stable, so a fixed crash cannot come | |
| 5 | + | back unnoticed and there is no test function to remember to write. | |
| 6 | + | ||
| 7 | + | Empty is the good state. None yet. |
| @@ -1,0 +1,73 @@ | |||
| 1 | + | # Seed corpora | |
| 2 | + | ||
| 3 | + | Hand-written and production-derived starting inputs, one directory per fuzz | |
| 4 | + | target. These are committed; `corpus/` is not. | |
| 5 | + | ||
| 6 | + | The split follows `astra-soak-overview`, which calls a minimized corpus | |
| 7 | + | "accumulated compute, not a build artifact": | |
| 8 | + | ||
| 9 | + | - **These seeds are human intent.** `tag/` holds the 130 distinct tag slugs | |
| 10 | + | committed in MNW's migrations (`002_seed_data.sql`, `056_expand_tags.sql`, | |
| 11 | + | `111_tag_taxonomy_overhaul.sql`) as `real-*`, the nine top-level namespaces as | |
| 12 | + | multi-line `family-*` files so the relational half of the oracle sees real | |
| 13 | + | siblings, and one `adv-*` file per class of thing the grammar has to refuse. | |
| 14 | + | `index/` holds operation scripts, half built from the real taxonomy and half | |
| 15 | + | aimed at the paths that have no unit test. | |
| 16 | + | - **`corpus/` is machine output and lives on astra**, under the soak runner's | |
| 17 | + | persistent directory. Minimize with `cargo +nightly fuzz cmin <target>` and | |
| 18 | + | commit it once it represents real soak hours, not before. | |
| 19 | + | ||
| 20 | + | Run against these on a machine with no corpus: | |
| 21 | + | ||
| 22 | + | mkdir -p fuzz/corpus/tag | |
| 23 | + | cargo +nightly fuzz run tag fuzz/corpus/tag fuzz/seeds/tag | |
| 24 | + | ||
| 25 | + | The `mkdir` is needed once. `cargo fuzz` creates the default corpus directory | |
| 26 | + | for you only when you name no directories at all; pass them explicitly and | |
| 27 | + | libFuzzer requires every one to exist already. | |
| 28 | + | ||
| 29 | + | **Name the corpus directory first and this one second.** libFuzzer writes new | |
| 30 | + | inputs into whichever directory it is given first and treats the rest as | |
| 31 | + | read-only. Passing `fuzz/seeds/tag` alone dumps hundreds of machine-generated | |
| 32 | + | files in here and buries the hand-written ones, which is exactly the split this | |
| 33 | + | directory exists to keep. | |
| 34 | + | ||
| 35 | + | ## The index script grammar | |
| 36 | + | ||
| 37 | + | One operation per line, dispatched on the first character. Anything else is | |
| 38 | + | ignored, which is what makes `# a comment` work. | |
| 39 | + | ||
| 40 | + | | Line | Operation | | |
| 41 | + | |------|-----------| | |
| 42 | + | | `+tag` | `insert` | | |
| 43 | + | | `-tag` | `remove` | | |
| 44 | + | | `=a,b,c` | `rebuild` with that list; `=` alone rebuilds empty | | |
| 45 | + | | `?input limit` | `suggest` | | |
| 46 | + | | `~input limit` | `suggest_fuzzy` | | |
| 47 | + | | `!old>new` | `rename_prefix_bulk` | | |
| 48 | + | | `x prefix` | `remove_subtree` | | |
| 49 | + | | `m old>new` | `merge_tags` | | |
| 50 | + | ||
| 51 | + | ## What the accept/reject seeds are for | |
| 52 | + | ||
| 53 | + | Most of `tag/` is input the grammar refuses under at least one of the ten | |
| 54 | + | configs the oracle enumerates, and that is not wasted budget. The structural | |
| 55 | + | half of the oracle runs on every string, valid or not, because MNW server calls | |
| 56 | + | `parent`, `leaf` and `depth` on strings that never went through `validate_with`. | |
| 57 | + | A rejected seed also sits one mutation away from an accepted one. | |
| 58 | + | ||
| 59 | + | Two are worth knowing. `adv-11-backslash-percent` is the escaping shape the | |
| 60 | + | round-trip oracle exists to catch, and it is reached by unvalidated user text: | |
| 61 | + | `audiofiles-core/src/search.rs` wraps a free-text query in `%...%` after | |
| 62 | + | escaping it. `adv-12-multibyte` is the shape of the 0.4.1 character-versus-byte | |
| 63 | + | `max_length` fix, and it is also what separates the byte-level reference | |
| 64 | + | Levenshtein from a `chars()` one. | |
| 65 | + | ||
| 66 | + | Thirty-two of the 130 production slugs are two-segment category headers | |
| 67 | + | (`audio.format`, `audio.genre`) that MNW's `semantic_depth: 2` config refuses. | |
| 68 | + | They stay: an almost-valid tag is the most useful seed there is. | |
| 69 | + | ||
| 70 | + | ## Crash seeds | |
| 71 | + | ||
| 72 | + | An input that once found a bug stays here forever, and also becomes a file under | |
| 73 | + | `fuzz/regressions/`, which `tests/regressions.rs` replays on stable. None yet. |