diff --git a/Cargo.lock b/Cargo.lock index 0cc7278b..97206bb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,7 +205,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.7", ] [[package]] @@ -243,9 +243,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.7" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b" dependencies = [ "find-msvc-tools", "shlex", @@ -319,7 +319,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.7", ] [[package]] @@ -499,7 +499,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 3.0.6", + "syn 3.0.7", ] [[package]] @@ -510,7 +510,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 3.0.6", + "syn 3.0.7", ] [[package]] @@ -585,9 +585,9 @@ dependencies = [ [[package]] name = "either" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "enum_dispatch" @@ -655,9 +655,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "finl_unicode" @@ -952,6 +952,15 @@ dependencies = [ "improvise-formula", ] +[[package]] +name = "improvise-incremental" +version = "0.1.0" +dependencies = [ + "improvise-core", + "improvise-io", + "proptest", +] + [[package]] name = "improvise-io" version = "0.1.0-rc2" @@ -1043,9 +1052,9 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1" dependencies = [ "rustversion", ] @@ -1060,7 +1069,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.7", ] [[package]] @@ -1114,15 +1123,15 @@ checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libloading" @@ -1187,12 +1196,12 @@ dependencies = [ [[package]] name = "mac_address" -version = "1.1.8" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +checksum = "b367b50a9be9a5d3b5718c1da74e5d6b9c17647f89752ee2562a470cc3c4eb1a" dependencies = [ - "nix", - "winapi", + "nix 0.30.1", + "windows-sys", ] [[package]] @@ -1234,9 +1243,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "log", @@ -1249,6 +1258,18 @@ name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.13.2", "cfg-if", @@ -1423,19 +1444,21 @@ dependencies = [ [[package]] name = "pest" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" +checksum = "b568374ba38b33a6c627141f891faf16902b08d2db26b8ede1bcb0a15b1919fa" dependencies = [ "memchr", + "psm", + "stacker", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" +checksum = "b66e184b924cebaaff20ab2256ca52f12332d528a39aa76553b5d96f92aacf7f" dependencies = [ "pest", "pest_generator", @@ -1443,22 +1466,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" +checksum = "a87478d267e4de54a626af9754f2f0f58e927aac6ed0575fe89bc05ad6851694" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.7", ] [[package]] name = "pest_meta" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" +checksum = "4f986f248b4241ac359b831f6139aaa34e03b08a37b6caf7e201a33f95c869e1" dependencies = [ "pest", ] @@ -1529,9 +1552,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "powerfmt" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" [[package]] name = "ppv-lite86" @@ -1579,6 +1602,15 @@ dependencies = [ "unarray", ] +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1932,7 +1964,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.7", ] [[package]] @@ -2015,9 +2047,9 @@ checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "siphasher" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" [[package]] name = "slab" @@ -2027,9 +2059,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "socket2" @@ -2041,6 +2073,19 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2098,9 +2143,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc" dependencies = [ "proc-macro2", "quote", @@ -2172,7 +2217,7 @@ dependencies = [ "libc", "log", "memmem", - "nix", + "nix 0.29.0", "num-derive", "num-traits", "ordered-float", @@ -2233,7 +2278,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.7", ] [[package]] @@ -2259,9 +2304,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "tokio" -version = "1.53.1" +version = "1.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" dependencies = [ "bytes", "libc", @@ -2280,7 +2325,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.7", ] [[package]] @@ -2361,9 +2406,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-ident" @@ -2402,9 +2447,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.26.1" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" dependencies = [ "atomic", "getrandom 0.4.3", @@ -2494,7 +2539,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.7", "wasm-bindgen-shared", ] @@ -2706,18 +2751,18 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zerocopy" -version = "0.8.58" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.58" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.nix b/Cargo.nix index 17f0f403..4660a79b 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -103,6 +103,16 @@ rec { # File a bug if you depend on any for non-debug work! debug = internal.debugCrate { inherit packageId; }; }; + "improvise-incremental" = rec { + packageId = "improvise-incremental"; + build = internal.buildRustCrateWithFeatures { + packageId = "improvise-incremental"; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; "improvise-io" = rec { packageId = "improvise-io"; build = internal.buildRustCrateWithFeatures { @@ -721,14 +731,14 @@ rec { } { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; features = [ "full" "fold" ]; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; features = [ "full" "fold" "parsing" ]; } ]; @@ -817,9 +827,9 @@ rec { }; "cc" = rec { crateName = "cc"; - version = "1.4.7"; + version = "1.7.0"; edition = "2021"; - sha256 = "04z3q2wqsg4qgx4vsqsd01s27svz0bgdymjiyccgbnn24gg3whal"; + sha256 = "12xvzyiwincbr6djn8ykbdlxv87ilva4pjxhkz156xc2iaplk9jh"; dependencies = [ { name = "find-msvc-tools"; @@ -1062,7 +1072,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; features = [ "full" ]; } ]; @@ -1577,7 +1587,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; features = [ "full" "extra-traits" ]; } ]; @@ -1608,7 +1618,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; } ]; @@ -1855,9 +1865,9 @@ rec { }; "either" = rec { crateName = "either"; - version = "1.18.0"; + version = "1.19.0"; edition = "2021"; - sha256 = "0d7dx31sf8rakcgp63070ngb2vkjynrni866pnx879pawndgnai5"; + sha256 = "1gjq21g0sgk5ylpj85zafcinwhh3jj91i6drhb4278vw2v17370f"; features = { "default" = [ "std" ]; "serde" = [ "dep:serde" ]; @@ -2047,9 +2057,9 @@ rec { }; "find-msvc-tools" = rec { crateName = "find-msvc-tools"; - version = "0.1.13"; + version = "0.1.14"; edition = "2021"; - sha256 = "16ykhz2icc0xx8i3vr8fpp6h3djpik2zw5bcxbff9bxba5g909gg"; + sha256 = "112ljldlv150fpl8xr2jl5czg51k3kdfn6cy5fqdsvkl14sgpp5f"; libName = "find_msvc_tools"; }; @@ -3047,6 +3057,30 @@ rec { } ]; + }; + "improvise-incremental" = rec { + crateName = "improvise-incremental"; + version = "0.1.0"; + edition = "2024"; + src = lib.cleanSourceWith { filter = sourceFilter; src = ./crates/improvise-incremental; }; + libName = "improvise_incremental"; + dependencies = [ + { + name = "improvise-core"; + packageId = "improvise-core"; + } + ]; + devDependencies = [ + { + name = "improvise-io"; + packageId = "improvise-io"; + } + { + name = "proptest"; + packageId = "proptest"; + } + ]; + }; "improvise-io" = rec { crateName = "improvise-io"; @@ -3379,9 +3413,9 @@ rec { }; "indoc" = rec { crateName = "indoc"; - version = "2.0.7"; + version = "2.0.8"; edition = "2021"; - sha256 = "01np60qdq6lvgh8ww2caajn9j4dibx9n58rvzf7cya1jz69mrkvr"; + sha256 = "1lc4agds9bqf3lin6kkz2jgmahaqwrkaq1fkij5fpzvcg68jcyx3"; procMacro = true; authors = [ "David Tolnay " @@ -3429,7 +3463,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; features = [ "derive" "full" ]; } ]; @@ -3557,9 +3591,9 @@ rec { }; "lazy_static" = rec { crateName = "lazy_static"; - version = "1.5.0"; + version = "1.5.1"; edition = "2015"; - sha256 = "1zk6dqqni0193xg6iijh7i3i44sryglwgvx20spdvwk3r6sbrlmv"; + sha256 = "1yqaqmp510xw2ldpw88mx9b5s5qj8flb4rw0wd9ks1zpk9j0z1r0"; authors = [ "Marvin Löbel " ]; @@ -3570,9 +3604,9 @@ rec { }; "libc" = rec { crateName = "libc"; - version = "0.2.189"; + version = "0.2.190"; edition = "2021"; - sha256 = "1whjfs375vlng2q6yrbzs73cvp5lm3w1n2gfqajb2vgf7zg3xbry"; + sha256 = "0y5yap4bfp7rfsldcbk9pb5alcgygca5xn1n2pmh181zdpf3spff"; features = { "default" = [ "std" ]; "rustc-dep-of-std" = [ "align" "rustc-std-workspace-core" ]; @@ -3738,24 +3772,24 @@ rec { }; "mac_address" = rec { crateName = "mac_address"; - version = "1.1.8"; + version = "1.2.0"; edition = "2018"; - sha256 = "00r3n18mxglq1dzshnm0vxk1fgsp3c2hd08w6hfcqdp8ymmv5bn0"; + sha256 = "06pbqk1hqiraavi2wxc9gxj1g73bbm7af7ccf6sx79g9kc5barxk"; authors = [ "rep-nop " ]; dependencies = [ { name = "nix"; - packageId = "nix"; + packageId = "nix 0.30.1"; target = { target, features }: (("linux" == target."os" or null) || ("macos" == target."os" or null) || ("freebsd" == target."os" or null) || ("netbsd" == target."os" or null) || ("openbsd" == target."os" or null) || ("android" == target."os" or null) || ("illumos" == target."os" or null)); features = [ "net" ]; } { - name = "winapi"; - packageId = "winapi"; + name = "windows-sys"; + packageId = "windows-sys"; target = { target, features }: (target."windows" or false); - features = [ "winerror" "ws2def" "iphlpapi" ]; + features = [ "Win32_Networking_WinSock" "Win32_NetworkManagement_IpHelper" "Win32_NetworkManagement_Ndis" ]; } ]; features = { @@ -3860,9 +3894,9 @@ rec { }; "mio" = rec { crateName = "mio"; - version = "1.2.3"; + version = "1.2.4"; edition = "2021"; - sha256 = "1n5ryp7j5fga38z7php5yy9k7ia24rp6cl9gm27zwar6khz4862b"; + sha256 = "1vl6px9zy0pwhlwnw3cknsqyzkg8bcpiwiq4cgicf2fwgywfv20p"; authors = [ "Carl Lerche " "Thomas de Zeeuw " @@ -3898,7 +3932,7 @@ rec { }; resolvedDefaultFeatures = [ "default" "log" "net" "os-ext" "os-poll" ]; }; - "nix" = rec { + "nix 0.29.0" = rec { crateName = "nix"; version = "0.29.0"; edition = "2021"; @@ -3906,6 +3940,53 @@ rec { authors = [ "The nix-rust Project Developers" ]; + dependencies = [ + { + name = "bitflags"; + packageId = "bitflags 2.13.2"; + } + { + name = "cfg-if"; + packageId = "cfg-if"; + } + { + name = "libc"; + packageId = "libc"; + features = [ "extra_traits" ]; + } + ]; + buildDependencies = [ + { + name = "cfg_aliases"; + packageId = "cfg_aliases"; + } + ]; + features = { + "aio" = [ "pin-utils" ]; + "dir" = [ "fs" ]; + "memoffset" = [ "dep:memoffset" ]; + "mount" = [ "uio" ]; + "mqueue" = [ "fs" ]; + "net" = [ "socket" ]; + "pin-utils" = [ "dep:pin-utils" ]; + "ptrace" = [ "process" ]; + "sched" = [ "process" ]; + "signal" = [ "process" ]; + "socket" = [ "memoffset" ]; + "ucontext" = [ "signal" ]; + "user" = [ "feature" ]; + "zerocopy" = [ "fs" "uio" ]; + }; + resolvedDefaultFeatures = [ "default" "fs" "mman" ]; + }; + "nix 0.30.1" = rec { + crateName = "nix"; + version = "0.30.1"; + edition = "2021"; + sha256 = "1dixahq9hk191g0c2ydc0h1ppxj0xw536y6rl63vlnp06lx3ylkl"; + authors = [ + "The nix-rust Project Developers" + ]; dependencies = [ { name = "bitflags"; @@ -3935,6 +4016,7 @@ rec { features = { "aio" = [ "pin-utils" ]; "dir" = [ "fs" ]; + "event" = [ "poll" ]; "memoffset" = [ "dep:memoffset" ]; "mount" = [ "uio" ]; "mqueue" = [ "fs" ]; @@ -3948,7 +4030,7 @@ rec { "user" = [ "feature" ]; "zerocopy" = [ "fs" "uio" ]; }; - resolvedDefaultFeatures = [ "default" "fs" "memoffset" "mman" "net" "socket" ]; + resolvedDefaultFeatures = [ "default" "memoffset" "net" "socket" ]; }; "nom" = rec { crateName = "nom"; @@ -4436,9 +4518,9 @@ rec { }; "pest" = rec { crateName = "pest"; - version = "2.9.2"; + version = "2.9.3"; edition = "2021"; - sha256 = "1a11k95dyrpfrn2ql5lprdh4k31k4hksg83w67vfdlps62iarls5"; + sha256 = "1yhr35ds3c5ww7nvh9nvs842p40nmwgqj7ql4z3accwbld5kfs5m"; authors = [ "Dragoș Tiselice " ]; @@ -4448,6 +4530,16 @@ rec { packageId = "memchr"; optional = true; } + { + name = "psm"; + packageId = "psm"; + optional = true; + } + { + name = "stacker"; + packageId = "stacker"; + optional = true; + } { name = "ucd-trie"; packageId = "ucd-trie"; @@ -4459,15 +4551,15 @@ rec { "memchr" = [ "dep:memchr" ]; "miette-error" = [ "std" "pretty-print" "dep:miette" ]; "pretty-print" = [ "dep:serde" "dep:serde_json" ]; - "std" = [ "ucd-trie/std" ]; + "std" = [ "ucd-trie/std" "dep:stacker" "dep:psm" ]; }; resolvedDefaultFeatures = [ "default" "memchr" "std" ]; }; "pest_derive" = rec { crateName = "pest_derive"; - version = "2.9.2"; + version = "2.9.3"; edition = "2021"; - sha256 = "1ky22vlsbqha1jhx5l31d95gm8l54va38b9zmmr2wjicgxan0jr8"; + sha256 = "0zygma96zndmadjsg6m353ak48ziab55c8mb43zsmsscj95ihvmn"; procMacro = true; authors = [ "Dragoș Tiselice " @@ -4493,9 +4585,9 @@ rec { }; "pest_generator" = rec { crateName = "pest_generator"; - version = "2.9.2"; + version = "2.9.3"; edition = "2021"; - sha256 = "029igkh6nsw62bn4325gkjm5c3lf4bjqbkv8jblrwc0ml441z78x"; + sha256 = "150nhpb5mh4vx1gmgl3fmix953pmy3r595xg4sk59pp4cz97hx58"; authors = [ "Dragoș Tiselice " ]; @@ -4519,7 +4611,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.119"; + packageId = "syn 3.0.7"; } ]; features = { @@ -4531,9 +4623,9 @@ rec { }; "pest_meta" = rec { crateName = "pest_meta"; - version = "2.9.2"; + version = "2.9.3"; edition = "2021"; - sha256 = "1jxd8gbm328lsl3m0z2b1pq9ri60h4f7122i189k5jpwkwdbm4zd"; + sha256 = "1qb9r2akz8q1wbvwmdipiaq06km3m8wn27w3kcssqha2icj6z62g"; authors = [ "Dragoș Tiselice " ]; @@ -4706,9 +4798,9 @@ rec { }; "powerfmt" = rec { crateName = "powerfmt"; - version = "0.2.0"; + version = "0.2.1"; edition = "2021"; - sha256 = "14ckj2xdpkhv3h6l5sdmb9f1d57z8hbfpdldjc2vl5givq2y77j3"; + sha256 = "0n293rvnpisjmxdqhq6zmrvcdql7j1c4zrcy5053vrv5x6wr8qsa"; authors = [ "Jacob Pratt " ]; @@ -4863,6 +4955,22 @@ rec { }; resolvedDefaultFeatures = [ "bit-set" "default" "fork" "regex-syntax" "rusty-fork" "std" "tempfile" "timeout" ]; }; + "psm" = rec { + crateName = "psm"; + version = "0.1.24"; + edition = "2015"; + sha256 = "0428cnwx8i9dhkcjwzap0amg9cjk8nhj0xr5hkhm6zl543r9y2r0"; + authors = [ + "Simonas Kazlauskas " + ]; + buildDependencies = [ + { + name = "cc"; + packageId = "cc"; + } + ]; + + }; "quick-error" = rec { crateName = "quick-error"; version = "1.2.3"; @@ -6145,7 +6253,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" "proc-macro" ]; } @@ -6407,9 +6515,9 @@ rec { }; "siphasher" = rec { crateName = "siphasher"; - version = "1.0.3"; + version = "1.0.4"; edition = "2018"; - sha256 = "0jg6l9xyzca5vy4h6gf8r6p4kk84g98fk95pzig1kq6cr4z8grcf"; + sha256 = "0mn28y43123jdpskdn6r9wibmn066f7h30zkkqn88bd6hj8zxx1k"; authors = [ "Frank Denis " ]; @@ -6438,9 +6546,9 @@ rec { }; "smallvec" = rec { crateName = "smallvec"; - version = "1.16.2"; + version = "1.16.3"; edition = "2018"; - sha256 = "13iai5hhwyp8z0pbn8r11q4j5956jaxhcbvvf2drm17f1q7myfgr"; + sha256 = "06z0akmcq2zc9qg22dxda298p50gg3di5ggl3y2nllag8ypwhgav"; authors = [ "The Servo Project Developers" ]; @@ -6481,6 +6589,49 @@ rec { }; resolvedDefaultFeatures = [ "all" ]; }; + "stacker" = rec { + crateName = "stacker"; + version = "0.1.25"; + edition = "2021"; + sha256 = "0rwrws4iyh8cay7pghzf2dy55pprwfn1vm805f5czfh6cza4jzvh"; + authors = [ + "Alex Crichton " + "Simonas Kazlauskas " + ]; + dependencies = [ + { + name = "cfg-if"; + packageId = "cfg-if"; + } + { + name = "libc"; + packageId = "libc"; + } + { + name = "psm"; + packageId = "psm"; + } + { + name = "windows-sys"; + packageId = "windows-sys"; + target = { target, features }: (target.name == "arm64ec-pc-windows-msvc"); + features = [ "Win32_System_Memory" "Win32_System_Threading" "Win32_Foundation" ]; + } + { + name = "windows-sys"; + packageId = "windows-sys"; + target = { target, features }: ((target."windows" or false) && (!("arm64ec" == target."arch" or null))); + features = [ "Win32_System_Memory" "Win32_System_Threading" "Win32_Foundation" ]; + } + ]; + buildDependencies = [ + { + name = "cc"; + packageId = "cc"; + } + ]; + + }; "static_assertions" = rec { crateName = "static_assertions"; version = "1.1.0"; @@ -6623,11 +6774,11 @@ rec { }; resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "extra-traits" "full" "parsing" "printing" "proc-macro" ]; }; - "syn 3.0.6" = rec { + "syn 3.0.7" = rec { crateName = "syn"; - version = "3.0.6"; + version = "3.0.7"; edition = "2021"; - sha256 = "1vmw7s58rzrs926nv5m06x7qbgswm1aa9iw3s1bj5var47kyi4w5"; + sha256 = "1p5agw0rj3f8spsnyqgj3c569nzyv5zz430xasi2qgskc42jwann"; authors = [ "David Tolnay " ]; @@ -6854,7 +7005,7 @@ rec { } { name = "nix"; - packageId = "nix"; + packageId = "nix 0.29.0"; target = { target, features }: (target."unix" or false); features = [ "mman" "fs" ]; } @@ -7040,7 +7191,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; } ]; @@ -7133,9 +7284,9 @@ rec { }; "tokio" = rec { crateName = "tokio"; - version = "1.53.1"; + version = "1.53.2"; edition = "2021"; - sha256 = "1v8b3b45pkpbibls75yniqbvx5dlks2708141ljni5mnf6lawb10"; + sha256 = "0i202ksji8q2asvii0adzgi8m0z93z3j7apn62qfh8d6qzy92pz9"; authors = [ "Tokio Contributors " ]; @@ -7262,7 +7413,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; features = [ "full" ]; } ]; @@ -7513,9 +7664,9 @@ rec { }; "unicase" = rec { crateName = "unicase"; - version = "2.9.0"; + version = "2.10.0"; edition = "2018"; - sha256 = "0hh1wrfd7807mfph2q67jsxqgw8hm82xg2fb8ln8cvblkwxbri6v"; + sha256 = "13x2h9s4jhch7rknqkqb1pb35irpj00fswy9ssgh0dm0qsnc6z1m"; authors = [ "Sean McArthur " ]; @@ -7609,9 +7760,9 @@ rec { }; "uuid" = rec { crateName = "uuid"; - version = "1.26.1"; + version = "1.28.0"; edition = "2021"; - sha256 = "1kl5nb7r3gpmkc43d6nbayvzqhcp2grczk6c7bxv80b6x70xmxif"; + sha256 = "01mnlhz09hwdjaxwzjd6bql3fjb5zl9n8dzaxn7dvdxyhiiiihbw"; authors = [ "Ashley Mannix" "Dylan DPC" @@ -7888,7 +8039,7 @@ rec { } { name = "syn"; - packageId = "syn 3.0.6"; + packageId = "syn 3.0.7"; features = [ "visit" "visit-mut" "full" "extra-traits" ]; } { @@ -8644,7 +8795,7 @@ rec { features = { "debug" = [ "impl-debug" ]; }; - resolvedDefaultFeatures = [ "consoleapi" "fileapi" "handleapi" "impl-default" "iphlpapi" "memoryapi" "namedpipeapi" "processenv" "processthreadsapi" "synchapi" "winbase" "winerror" "winnls" "winnt" "winsock2" "winuser" "ws2def" ]; + resolvedDefaultFeatures = [ "consoleapi" "fileapi" "handleapi" "impl-default" "memoryapi" "namedpipeapi" "processenv" "processthreadsapi" "synchapi" "winbase" "winerror" "winnls" "winnt" "winsock2" "winuser" ]; }; "winapi-i686-pc-windows-gnu" = rec { crateName = "winapi-i686-pc-windows-gnu"; @@ -9065,7 +9216,7 @@ rec { "Win32_Web" = [ "Win32" ]; "Win32_Web_InternetExplorer" = [ "Win32_Web" ]; }; - resolvedDefaultFeatures = [ "Wdk" "Wdk_Foundation" "Wdk_Storage" "Wdk_Storage_FileSystem" "Wdk_System" "Wdk_System_IO" "Win32" "Win32_Foundation" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_Console" "Win32_System_Diagnostics" "Win32_System_Diagnostics_Debug" "Win32_System_IO" "Win32_System_Pipes" "Win32_System_SystemServices" "Win32_System_Threading" "Win32_System_WindowsProgramming" "default" ]; + resolvedDefaultFeatures = [ "Wdk" "Wdk_Foundation" "Wdk_Storage" "Wdk_Storage_FileSystem" "Wdk_System" "Wdk_System_IO" "Win32" "Win32_Foundation" "Win32_NetworkManagement" "Win32_NetworkManagement_IpHelper" "Win32_NetworkManagement_Ndis" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_Console" "Win32_System_Diagnostics" "Win32_System_Diagnostics_Debug" "Win32_System_IO" "Win32_System_Memory" "Win32_System_Pipes" "Win32_System_SystemServices" "Win32_System_Threading" "Win32_System_WindowsProgramming" "default" ]; }; "winnow" = rec { crateName = "winnow"; @@ -9114,9 +9265,9 @@ rec { }; "zerocopy" = rec { crateName = "zerocopy"; - version = "0.8.58"; + version = "0.8.62"; edition = "2021"; - sha256 = "0i6vsbj4qfqj62b8qxl7wdcin8rnqz6pw88p6zzl5dc2mnpqyzn1"; + sha256 = "17asjqfdkqmxsx42myw1x5jlb28m5axlf9ifldqpbiy7dbsjnl46"; dependencies = [ { name = "zerocopy-derive"; @@ -9146,9 +9297,9 @@ rec { }; "zerocopy-derive" = rec { crateName = "zerocopy-derive"; - version = "0.8.58"; + version = "0.8.62"; edition = "2021"; - sha256 = "1whbf7cjcqyp5nhvma78yy489swr7mfgc9m6r6h4ckyz8kh5cpsr"; + sha256 = "06lx0gigf0j5pvp4dhlrv636b4mib37ijzlcc3i62rdhailj0msl"; procMacro = true; libName = "zerocopy_derive"; dependencies = [ diff --git a/Cargo.toml b/Cargo.toml index e0db5c95..b7a83682 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "crates/improvise-formula", "crates/improvise-core", "crates/improvise-protocol", "crates/improvise-io", "crates/improvise-command", "crates/improvise-wasm-client", "crates/improvise-ws-server", "crates/improvise-worker-server", "crates/gp-tree", "crates/improvise-gp", "crates/numtower", "crates/zset", "crates/zset-ffi"] +members = [".", "crates/improvise-formula", "crates/improvise-core", "crates/improvise-protocol", "crates/improvise-io", "crates/improvise-command", "crates/improvise-wasm-client", "crates/improvise-ws-server", "crates/improvise-worker-server", "crates/gp-tree", "crates/improvise-gp", "crates/numtower", "crates/zset", "crates/improvise-incremental", "crates/zset-ffi"] [package] name = "improvise" @@ -70,9 +70,9 @@ improvise-wasm-client = { path = "crates/improvise-wasm-client" } improvise-protocol = { path = "crates/improvise-protocol", features = ["test-support"] } # `tests/headless_cli.rs` detaches the binary from any terminal with `setsid`. libc = "0.2" -pest = "2.9.2" -pest_derive = "2.9.2" -pest_meta = "2.9.2" +pest = "2.9.3" +pest_derive = "2.9.3" +pest_meta = "2.9.3" proptest = "1" tempfile = "3" diff --git a/context/code/improvise-incremental.md b/context/code/improvise-incremental.md new file mode 100644 index 00000000..35e39331 --- /dev/null +++ b/context/code/improvise-incremental.md @@ -0,0 +1,91 @@ +# `improvise-incremental`: the reasoning + +`crates/improvise-incremental` is ADR 009 step 2 (improvise-t8ow, epic improvise-bw4p): the +plug. It holds the `Derived` trait, `Eager` (today's evaluator behind it) and the +differential harness every later backend is checked with. There is no incremental backend +here, the evaluator is untouched and no existing crate changed. The decisions are the ADR's +(`context/adr/009-incremental-evaluation.md`, clause 2). The evidence for each test is in +`context/test-evidence/improvise-incremental.md`. + +## The shape + +| item | what it is | +|---|---| +| `Derived` | the two things the evaluator reads: `value(key, agg)` and `raw_fold(partial)` | +| `Eager` | an owned `Model` plus its None categories; every answer is a call into `Model` | +| `Maintained` | a `Derived` that is told each `Edit`; `Eager` is one | +| `Edit` | one model mutation as data (set, clear, add or remove a formula, set a measure's aggregation) | +| `probes`, `Probe` | every question worth asking of a model, and a way to put it to any backend | +| `differential` | runs one edit sequence on a candidate and on `Eager`, comparing every probe at every step | +| `Divergence`, `Report` | what a failure names (step, probe, both answers) and what a pass compared | + +## Interpretations + +Clause 2 names the trait and says what it carries ("the formula result at a key, and the raw +fold of the cells that match a partial key") and nothing else. These are the readings taken. + +1. **`Derived` lives in this crate, not in improvise-core.** The ADR says improvise-core + "gains a trait". Putting it there changes an existing crate's source, which the brief for + this step ruled out, and the brief was ambiguous about it. This departs from the accepted + ADR's text, so it is flagged to the user and not decided here: improvise-bw4p.2 moves it + (one file and a `pub use`, since the trait names only core types). While it sits + downstream, core cannot read through it, which is the clause's point. +2. **Two methods, the minimum.** `value(key, agg)` is the formula result at a key, or the cell + as stored or folded; `raw_fold(partial)` is the fold of matching stored cells. `agg` is + `Aggregation`, the evaluator's own type. Hidden-item `covers`, totals and occupancy are not + in the trait, so the differential cannot probe them, and a hidden item (view state) cannot + be exercised at all. The ADR's differential compares "every drawn cell, total and occupancy + count"; improvise-bw4p.3 adds them. A group is exercised: a stored key with one coordinate + replaced by its item's group is a probe. +3. **`raw_fold` is never a formula, and `Eager` enforces it.** For a partial naming a formula + target `Eager::raw_fold` is `None`; otherwise it is `evaluate_aggregated(partial, OverHidden)`, + because the evaluator's own raw fold (`aggregate_raw`, `aggregate_cell_values`) is private to + core. Calling it through the public path is exact for any partial that names no formula + target. improvise-bw4p.2 exposes the raw fold from core so `Eager` stops going round. +4. **`Eager` recomputes formulas after every edit.** Production does it in `rebuild_layout` + (`Model::recompute_formulas(none_cats)`), and an over-hidden read of a formula cell reads + the cache it fills. So `Eager` carries the None categories of the view it stands for and + recomputes under them after each edit, as a layout rebuild does. +5. **`Edit` is the vocabulary of the harness, not of the wire.** Five variants, each one + public `Model` call. A later step adds variants for what it maintains (an item added, a + category removed); the harness does not care which. An `Edit` fault reaches both sides of a + differential alike, so it is pinned only where a direct `Eager` test reads the result: + `ClearCell`, `AddFormula` and `RemoveFormula`, and `SetMeasureAgg` through the fold tests. + `SetCell` is read by nearly every test. +6. **Values agree by float rules with every NaN one value** (`same_value`), the rule clause 3 + states for cell deltas. Signed zero compares equal, so a later `ExactSum` cannot be told + from eager's sum by the sign of a zero here; clause 8 has its own property test for that. +7. **No `zset` dependency yet.** Nothing in this step uses a Z-set. It arrives with the first + backend, as the cell delta of step 3. + +## What the harness checks + +`differential(model, none_cats, candidate, edits)` builds `Eager` over a clone of the model, +applies each edit to both, and after step 0 and after every edit asks both every probe. The +probe set **accumulates**: probes from the model at every earlier step stay asked, so a backend +that goes on answering for a removed formula diverges. A pass returns a `Report` of how many +states and answers were compared. + +`probes(model)` takes the base keys (each stored key, and each with one coordinate replaced by +its item's group), every sub-partial of a base key dropping at most `MAX_DROPPED = 3` +coordinates plus the empty key, and each formula's target over each of those. Each key is asked +as an exact value, an over-hidden value and a raw fold. A fold dropping more than three +coordinates from a stored key is not asked about. `edit_probes` +adds the key an edit names, so a cleared cell is asked about after it is gone. A divergence +reads `divergence at step N: : eager != candidate `. + +**A limit, stated plainly (improvise-bw4p.4).** The probe set is every stored key's +sub-partials, not the keys a view draws. A view with four or more categories on None draws a cell +that drops four or more coordinates of a stored key, and the harness checks none of those cells: +a rival wrong only at `{C0=a, _Measure=Revenue}` over a five-category model passes. Neither +shipped demo reaches the limit, so the corpus does not show it. Raising `MAX_DROPPED` is not the +answer, because `sub_partials` walks all 2^n subsets of a key whatever the limit is (93ms at 20 +categories and 225ms at 22 in a debug build, per call). The fix is to probe the keys each view +draws, with steps 3 and 4; until then a candidate on such a view is under-checked. + +The corpus the tests use is `examples/demo.improv` and `web/demo.improv`, loaded through +`improvise_io::persistence::load`, and hand-built models (formulas, text, mixed values, a group). +`eager_matches_the_evaluator_on_the_shipped_documents` takes every view's None categories; the +differential runs take the first per document, and the largest document only its first four +edits, to keep the binary under two seconds in a debug build. A proptest (256 cases) runs random +edit sequences against a candidate that rebuilds an `Eager` from scratch on each edit. diff --git a/context/test-evidence/improvise-incremental.md b/context/test-evidence/improvise-incremental.md new file mode 100644 index 00000000..aac72cbc --- /dev/null +++ b/context/test-evidence/improvise-incremental.md @@ -0,0 +1,194 @@ +# Test evidence: `improvise-incremental` + +What each test guards, which rival it was run against, and where that rival failed. The +reasoning is in `context/code/improvise-incremental.md`; the decisions are ADR 009 clause 2's. + +**How the rivals are kept.** The `the_harness_catches_*` tests run `differential` on a +`Rival`, an `Eager` told every edit and wrong in one way, and assert the step at which the +harness reports the divergence. They run on every build, so each rival is watched failing on +every run and not once by hand. + +**Injected in the source.** Eighteen faults were put into `src/eager.rs`, `src/harness.rs` +and `src/edit.rs` one at a time, each run against the whole suite and each reverted and +followed by a full rerun. The failing tests, by name (a fault no test names survived, and +none did): +- `Eager::value` ignores its `Aggregation`: `eager_follows_an_edit_to_the_cell_a_fold_reads`, + `eager_matches_the_evaluator_on_the_shipped_documents`, `eager_raw_fold_is_never_a_formula`, + `eager_value_reads_a_formula_exactly_and_folded`, + `the_harness_catches_a_backend_that_ignores_the_aggregation`, + `the_harness_catches_a_wrong_folded_formula_value`; +- `Eager::raw_fold` reads `Exact`: `eager_raw_fold_sums_by_default`, + `eager_raw_fold_follows_the_measures_aggregation`, `eager_raw_fold_of_text_is_unanimous_or_mixed`, + `eager_numbers_among_text_are_mixed`, `eager_follows_an_edit_to_the_cell_a_fold_reads`, + `eager_matches_the_evaluator_on_the_shipped_documents`, + `the_harness_catches_a_raw_fold_that_returns_a_formula`; +- `Eager::raw_fold` returns a formula's value: `eager_raw_fold_is_never_a_formula`, + `eager_matches_the_evaluator_on_the_shipped_documents`, + `the_harness_catches_a_raw_fold_that_returns_a_formula`; +- `Eager::apply` does not recompute formulas: `eager_follows_an_edit_to_the_cell_a_fold_reads`, + `a_maintained_eager_equals_a_fresh_one_over_random_edit_sequences`; +- the harness compares only the first edit: `the_harness_catches_a_backend_that_ignores_a_clear`, + `…_ignores_the_measure_aggregation`, `…_goes_stale_after_two_edits`, + `the_harness_catches_a_backend_that_keeps_answering_for_a_removed_formula`; +- the harness never reports a divergence: all nine `the_harness_catches_*` tests; +- `same_value` always true: `same_value_follows_the_float_rules_and_is_exact_elsewhere` and four + `the_harness_catches_*` tests; +- `same_value` NaN unequal, signed zero by bits, any two text equal: each fails + `same_value_follows_the_float_rules_and_is_exact_elsewhere` and nothing else; +- `probes` omit formula cells: `probes_cover_folds_formulas_measures_and_groups` and four + `the_harness_catches_*` tests; +- `probes` drop at most one coordinate: `probes_cover_folds_formulas_measures_and_groups`, + `the_harness_catches_a_fault_that_only_shows_after_two_coordinates_are_dropped`; +- `probes` omit raw folds: `probes_cover_folds_formulas_measures_and_groups`, + `an_edit_names_its_key_as_two_value_probes_and_a_fold`, + `the_harness_catches_a_raw_fold_that_returns_a_formula`; +- `probes` omit group keys: `probes_cover_folds_formulas_measures_and_groups`, + `the_harness_catches_a_wrong_group_fold`; +- the probe history is forgotten between steps: `…_goes_stale_after_two_edits`, + `…_ignores_a_clear`, `the_harness_catches_a_backend_that_keeps_answering_for_a_removed_formula`; +- `edit_probes` names nothing: `an_edit_names_its_key_as_two_value_probes_and_a_fold`; +- `Edit::AddFormula` as a no-op: `eager_add_and_remove_formula_change_what_a_key_reads`; +- `Edit::ClearCell` applied as `SetCell(0.0)`, which both sides of a differential share and so + only a direct `Eager` test can see: `eager_clear_cell_leaves_the_key_absent`. + +## eager_raw_fold_sums_by_default + +Hand-computed: Revenue under Product A is 10 + 5 = 15. Fails on the `raw_fold`-reads-`Exact` +rival (a partial key names no stored cell, so the answer is `None`). + +## eager_raw_fold_follows_the_measures_aggregation + +After `measure_agg[Revenue] = Max` the same fold is 10, and Cost, unchanged, still sums to 6. +A fold that always sums fails on the first; one that always takes the maximum fails on the second. + +## eager_raw_fold_of_text_is_unanimous_or_mixed + +Note under A is two equal texts and reads as that text; under B it is two different texts and +reads `Error("mixed")`. A fold that drops text, which the evaluator used to do (gotcha 7), +returns `None` for both. + +## eager_numbers_among_text_are_mixed + +A text among numbers is `Error("mixed")`, not the sum of the numbers. + +## eager_value_reads_a_formula_exactly_and_folded + +`Profit = Revenue - Cost` reads 6 at East/A exactly, and 9 at Product A with Region over +hidden: (10 + 5) - (4 + 2), the fold taken before the subtraction. Fails on the +`value`-ignores-`Aggregation` rival. + +## eager_follows_an_edit_to_the_cell_a_fold_reads + +After East/A Revenue becomes 20 the fold is 25 and the folded Profit 19. The Profit half is +what fails when `apply` forgets to recompute formulas: the folded cache is then stale. + +## eager_matches_the_evaluator_on_the_shipped_documents + +Both shipped documents, every view's None categories, and the hand model: every probe of +`Eager` equals a direct `evaluate_aggregated` on a model recomputed under the same None +categories, and the probe set is non-empty. Fails on both aggregation rivals. + +## eager_agrees_with_itself_over_every_corpus_edit_sequence + +`differential` over a generated edit sequence on each corpus model returns a `Report` with +`edits + 1` steps and a non-zero probe count. The count assertion is what a harness that +compared nothing would fail; the step count is what one that stopped early would fail. +## the_harness_passes_a_backend_that_is_right + +The control for the `catches` tests: `Eager` as the candidate over the hand-built edits is a +pass, so a harness that rejects everything is seen. + +## the_harness_catches_a_backend_that_ignores_the_aggregation + +A candidate that reads every `value` as `Exact` diverges at step 0, first at the full-key +formula cell `{East, A, Profit}` over hidden (eager `None`, candidate `6`). + +## the_harness_catches_a_backend_that_ignores_the_measure_aggregation + +A candidate that drops `SetMeasureAgg` diverges at step 2, the edit that sets it, on a Revenue key. + +## the_harness_catches_a_backend_that_goes_stale_after_two_edits + +A candidate that stops applying edits after the second diverges at step 3, on a Revenue key. +A prefix of steps cannot see a backend that goes wrong late. + +## the_harness_catches_a_backend_that_ignores_a_clear + +A candidate that keeps a cleared cell diverges at step 3 and the message names it. + +## the_harness_catches_a_wrong_folded_formula_value + +Review of PR 343 (Codex P1): a rival wrong only at `value({Product=A, _Measure=Profit}, OverHidden)`, +the Profit cell a view with Region on None draws, passed 128 probes, because formula cells were +built over full keys only. Now the divergence is at step 0 on exactly that probe, eager `9`, +candidate `-999`. + +## the_harness_catches_a_backend_that_keeps_answering_for_a_removed_formula + +Review of PR 343: probes were rebuilt from the post-edit model, so a candidate that ignores +`RemoveFormula` was never asked about Profit again. Probes now accumulate over every step. The +divergence is at the last step, on a Profit key, eager `None`, candidate `Some`. + +## the_harness_catches_a_fault_that_only_shows_after_two_coordinates_are_dropped + +Review of PR 343 (Copilot): only one-coordinate partials and the empty key were probed. A rival +wrong only at `{_Measure=Revenue}` now diverges at step 0 on `Value({_Measure=Revenue}, Exact)`. +The bound is `MAX_DROPPED = 3` coordinates; a fold dropping more is not asked about. + +## the_harness_catches_a_raw_fold_that_returns_a_formula + +Review of PR 343 (Copilot): `raw_fold` is documented never to be a formula. A rival whose +`raw_fold` is `value(partial, OverHidden)` diverges at step 0 on a `RawFold` of a Profit partial +(eager `None`, candidate `Some`). It could not be seen while `Eager` itself returned the formula. + +## the_harness_catches_a_wrong_group_fold + +A group name stands in for an item in the probe keys. A rival wrong only at +`{Region=Coastal, Product=A, _Measure=Revenue}` diverges at step 0, eager `15`, candidate `-999`. + +## eager_folds_a_group_it_is_asked_about + +The group fold in the grouped model is East + West = 15 exactly, so the rival test above has a +true answer to differ from. + +## eager_raw_fold_is_never_a_formula + +At `{Product=A, _Measure=Profit}` the folded value is `9` and the raw fold is `None`. Fails on the +fault that lets `raw_fold` read the formula cache. + +## eager_add_and_remove_formula_change_what_a_key_reads + +Review of PR 343: `Edit::AddFormula` as a no-op left all 25 tests green, because an `Edit` fault +reaches both sides of a differential alike. `Double = Revenue * 2` reads `None` before the edit +and `20` at East/A after it, and `None` again after `RemoveFormula`. Fails on `AddFormula` as a +no-op. + +## eager_clear_cell_leaves_the_key_absent + +After `ClearCell` the key reads `None` and the model holds no cell. A `ClearCell` applied as +`SetCell(0.0)` hits both sides of a differential identically, so only this direct test fails. + +## same_value_follows_the_float_rules_and_is_exact_elsewhere + +NaN equals NaN, `-0.0` equals `0.0`, `1.0` differs from `1.0000000000000002`, texts and errors +compare exactly, and `None` equals only `None`. Each of three one-line faults in `same_value` +(NaN unequal, signed zero by bits, any two texts equal) survived the whole suite until this test +existed. + +## probes_cover_folds_formulas_measures_and_groups + +`probes` includes a raw fold, a full key, a folded formula cell under both aggregations, the +two-dropped key `{_Measure=Revenue}`, the empty key and a group key. Review of PR 343 showed +`probes` omitting formula cells, `_Measure` keys or raw folds left every test green. + +## an_edit_names_its_key_as_two_value_probes_and_a_fold + +`edit_probes` of a `ClearCell` names the cleared key three ways, and of an edit naming no key +names nothing. + +## a_maintained_eager_equals_a_fresh_one_over_random_edit_sequences + +Clause 2 asks for a property test over a random edit sequence. A proptest (256 cases, up to four +edits of five kinds over the hand-built model) runs `differential` with a candidate that rebuilds +an `Eager` from scratch on every edit. It fails on the `Eager::apply`-skips-recompute fault, which +is the property: maintaining in place equals building afresh. diff --git a/crates/improvise-core/Cargo.toml b/crates/improvise-core/Cargo.toml index a83cf35b..ee52b099 100644 --- a/crates/improvise-core/Cargo.toml +++ b/crates/improvise-core/Cargo.toml @@ -17,7 +17,7 @@ serde = { version = "1", features = ["derive"] } # wasm-clean. siphasher = "1" # Unicode full case folding for slugify (ADR 007 answer 26). no_std, no dependencies. -unicase = "2.9" +unicase = "2.10" [features] # Exposes the fold-evaluation counter to other crates' tests (improvise-0jw5). diff --git a/crates/improvise-formula/Cargo.toml b/crates/improvise-formula/Cargo.toml index 00bf407c..823bd23e 100644 --- a/crates/improvise-formula/Cargo.toml +++ b/crates/improvise-formula/Cargo.toml @@ -8,11 +8,11 @@ repository = "https://github.com/fiddlerwoaroof/improvise" [dependencies] anyhow = "1" -pest = "2.9.2" -pest_derive = "2.9.2" +pest = "2.9.3" +pest_derive = "2.9.3" # For FormulaId alone, which crosses the wire (improvise-km71); no AST derive. serde = { version = "1", features = ["derive"] } [dev-dependencies] -pest_meta = "2.9.2" +pest_meta = "2.9.3" proptest = "1" diff --git a/crates/improvise-incremental/Cargo.toml b/crates/improvise-incremental/Cargo.toml new file mode 100644 index 00000000..3da249b2 --- /dev/null +++ b/crates/improvise-incremental/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "improvise-incremental" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +description = "The seam between improvise-core and the incremental circuit: the Derived trait, the Eager oracle and the differential harness (ADR 009 clause 2)." + +# ADR 009 clause 2: depends on improvise-core, never the other way round. The +# `zset` edge arrives with the first backend that uses it (step 3), not before. +[dependencies] +improvise-core = { path = "../improvise-core" } + +[dev-dependencies] +improvise-io = { path = "../improvise-io" } +proptest = "1" diff --git a/crates/improvise-incremental/src/eager.rs b/crates/improvise-incremental/src/eager.rs new file mode 100644 index 00000000..06362b3a --- /dev/null +++ b/crates/improvise-incremental/src/eager.rs @@ -0,0 +1,46 @@ +//! Today's evaluator behind the trait. Temporary (ADR 009 clause 2). + +use crate::{Derived, Edit, Maintained}; +use improvise_core::model::cell::{CellKey, CellValue}; +use improvise_core::model::{Aggregation, Model}; + +/// A model plus the None categories its formulas were last recomputed under. +/// It adds no logic: every answer is a call into `Model`. +#[derive(Debug, Clone)] +pub struct Eager { + model: Model, + none_cats: Vec, +} + +impl Eager { + pub fn new(mut model: Model, none_cats: Vec) -> Self { + model.recompute_formulas(&none_cats); + Eager { model, none_cats } + } + + pub fn model(&self) -> &Model { + &self.model + } +} + +impl Derived for Eager { + fn value(&self, key: &CellKey, agg: Aggregation) -> Option { + self.model.evaluate_aggregated(key, agg) + } + + fn raw_fold(&self, partial: &CellKey) -> Option { + match self.model.formula_for(partial) { + Some(_) => None, + None => self + .model + .evaluate_aggregated(partial, Aggregation::OverHidden), + } + } +} + +impl Maintained for Eager { + fn apply(&mut self, edit: &Edit) { + edit.apply(&mut self.model); + self.model.recompute_formulas(&self.none_cats); + } +} diff --git a/crates/improvise-incremental/src/edit.rs b/crates/improvise-incremental/src/edit.rs new file mode 100644 index 00000000..c2036ffc --- /dev/null +++ b/crates/improvise-incremental/src/edit.rs @@ -0,0 +1,46 @@ +//! One mutation of a model, as data, so a sequence can be replayed on two backends. + +use improvise_core::formula::ast::{AggFunc, Formula}; +use improvise_core::model::Model; +use improvise_core::model::cell::{CellKey, CellValue}; + +#[derive(Debug, Clone)] +pub enum Edit { + SetCell(CellKey, CellValue), + ClearCell(CellKey), + AddFormula(Formula), + RemoveFormula { + target: String, + target_category: String, + }, + SetMeasureAgg { + measure: String, + func: AggFunc, + }, +} + +impl Edit { + /// The one place an edit becomes a call on the model. + pub fn apply(&self, model: &mut Model) { + match self { + Edit::SetCell(key, value) => model.set_cell(key.clone(), value.clone()), + Edit::ClearCell(key) => model.clear_cell(key), + Edit::AddFormula(formula) => model.add_formula(formula.clone()), + Edit::RemoveFormula { + target, + target_category, + } => model.remove_formula(target, target_category), + Edit::SetMeasureAgg { measure, func } => { + model.measure_agg.insert(measure.clone(), func.clone()); + } + } + } + + /// The key an edit names, if it names one: always probed after it. + pub fn key(&self) -> Option<&CellKey> { + match self { + Edit::SetCell(key, _) | Edit::ClearCell(key) => Some(key), + _ => None, + } + } +} diff --git a/crates/improvise-incremental/src/harness.rs b/crates/improvise-incremental/src/harness.rs new file mode 100644 index 00000000..37281d85 --- /dev/null +++ b/crates/improvise-incremental/src/harness.rs @@ -0,0 +1,195 @@ +//! The differential harness: one edit sequence, two backends, every probe after every step. + +use crate::{Derived, Eager, Edit}; +use improvise_core::model::cell::{CellKey, CellValue}; +use improvise_core::model::{Aggregation, Model}; +use std::collections::HashSet; +use std::fmt; + +/// A backend that holds state of its own and is told each edit. +pub trait Maintained: Derived { + fn apply(&mut self, edit: &Edit); +} + +/// One question put to both backends. +#[derive(Debug, Clone, PartialEq)] +pub enum Probe { + Value(CellKey, Aggregation), + RawFold(CellKey), +} + +impl Probe { + pub fn ask(&self, backend: &dyn Derived) -> Option { + match self { + Probe::Value(key, agg) => backend.value(key, *agg), + Probe::RawFold(partial) => backend.raw_fold(partial), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Divergence { + /// 0 is the model as built, `n` the state after edit `n`. + pub step: usize, + pub probe: Probe, + pub eager: Option, + pub candidate: Option, +} + +impl fmt::Display for Divergence { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "divergence at step {}: {:?}: eager {:?} != candidate {:?}", + self.step, self.probe, self.eager, self.candidate + ) + } +} + +/// How many states and how many answers were compared: a run that compared +/// nothing must be visible as such. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Report { + pub steps: usize, + pub probes: usize, +} + +/// Values agree when equal by float rules with every NaN one value, so a NaN +/// answer is not a divergence from itself. +pub fn same_value(a: &Option, b: &Option) -> bool { + match (a, b) { + (Some(CellValue::Number(x)), Some(CellValue::Number(y))) => { + x == y || (x.is_nan() && y.is_nan()) + } + _ => a == b, + } +} + +/// The most coordinates a probed partial key drops from a stored key; the +/// empty key is probed as well. A fold dropping more is not asked about. +pub const MAX_DROPPED: usize = 3; + +/// The questions asked of a backend, once each, in the order first added. +#[derive(Default)] +struct Asked { + seen: HashSet<(CellKey, u8)>, + order: Vec, +} + +impl Asked { + fn extend(&mut self, probes: impl IntoIterator) { + for p in probes { + let tag = match &p { + Probe::Value(k, Aggregation::Exact) => (k.clone(), 0), + Probe::Value(k, Aggregation::OverHidden) => (k.clone(), 1), + Probe::RawFold(k) => (k.clone(), 2), + }; + if self.seen.insert(tag) { + self.order.push(p); + } + } + } +} + +fn all_three(key: CellKey) -> [Probe; 3] { + [ + Probe::Value(key.clone(), Aggregation::Exact), + Probe::Value(key.clone(), Aggregation::OverHidden), + Probe::RawFold(key), + ] +} + +/// Every key `key` keeps after dropping at most `MAX_DROPPED` coordinates. +fn sub_partials(key: &CellKey) -> impl Iterator + '_ { + let n = key.0.len(); + (0..1u64 << n) + .filter(|mask| mask.count_ones() as usize <= MAX_DROPPED) + .map(move |mask| { + let kept = (0..n) + .filter(|i| mask >> i & 1 == 0) + .map(|i| key.0[i].clone()); + CellKey::new(kept.collect()) + }) +} + +/// Every question worth asking of `model`; the bound and the key set are +/// in `context/code/improvise-incremental.md`. +pub fn probes(model: &Model) -> Vec { + let stored: Vec = model.data.iter_cells().map(|(k, _)| k).collect(); + let grouped = stored.iter().flat_map(|k| { + k.0.iter().filter_map(|(cat, item)| { + let group = model.category(cat)?.items.get(item)?.group.clone()?; + Some(k.clone().with(cat, group)) + }) + }); + let base: Vec = stored.iter().cloned().chain(grouped).collect(); + let partials = base + .iter() + .flat_map(sub_partials) + .chain(std::iter::once(CellKey::new(vec![]))); + let mut asked = Asked::default(); + for key in partials { + let formula_cells = model + .formulas() + .iter() + .map(|f| key.clone().with(&f.target_category, &f.target)) + .collect::>(); + asked.extend(all_three(key)); + asked.extend(formula_cells.into_iter().flat_map(all_three)); + } + asked.order +} + +/// What an edit names: its key, asked all three ways, after the edit. +pub fn edit_probes(edit: &Edit) -> Vec { + edit.key() + .cloned() + .into_iter() + .flat_map(all_three) + .collect() +} + +fn compare( + step: usize, + eager: &Eager, + candidate: &dyn Derived, + asked: &Asked, +) -> Result { + for probe in &asked.order { + let (want, got) = (probe.ask(eager), probe.ask(candidate)); + if !same_value(&want, &got) { + return Err(Divergence { + step, + probe: probe.clone(), + eager: want, + candidate: got, + }); + } + } + Ok(asked.order.len()) +} + +/// Runs `edits` on `candidate` and on `Eager`, comparing every probe ever +/// asked (so a removed formula stays asked about) at each step. `candidate` is built from `model` with the same `none_cats`. +pub fn differential( + model: &Model, + none_cats: &[String], + mut candidate: C, + edits: &[Edit], +) -> Result { + let mut eager = Eager::new(model.clone(), none_cats.to_vec()); + let mut asked = Asked::default(); + asked.extend(probes(eager.model())); + let mut probes_run = compare(0, &eager, &candidate, &asked)?; + for (n, edit) in edits.iter().enumerate() { + eager.apply(edit); + candidate.apply(edit); + asked.extend(probes(eager.model())); + asked.extend(edit_probes(edit)); + probes_run += compare(n + 1, &eager, &candidate, &asked)?; + } + Ok(Report { + steps: edits.len() + 1, + probes: probes_run, + }) +} diff --git a/crates/improvise-incremental/src/lib.rs b/crates/improvise-incremental/src/lib.rs new file mode 100644 index 00000000..0f006f9b --- /dev/null +++ b/crates/improvise-incremental/src/lib.rs @@ -0,0 +1,27 @@ +//! The plug: what the evaluator reads, and the oracle that answers it today. +//! Reasoning: `context/code/improvise-incremental.md`. + +mod eager; +mod edit; +mod harness; + +pub use eager::Eager; +pub use edit::Edit; +pub use harness::{ + Divergence, MAX_DROPPED, Maintained, Probe, Report, differential, edit_probes, probes, + same_value, +}; + +use improvise_core::model::Aggregation; +use improvise_core::model::cell::{CellKey, CellValue}; + +/// The two things the evaluator reads (ADR 009 clause 2). +pub trait Derived { + /// The value at `key`: the formula result when a formula owns it, else the + /// cell as stored or folded over what `agg` hides. `Model::evaluate_aggregated`. + fn value(&self, key: &CellKey, agg: Aggregation) -> Option; + + /// The raw fold of the stored cells that match `partial`, by the measure's + /// aggregation. Never a formula: `None` for a partial naming a formula target. + fn raw_fold(&self, partial: &CellKey) -> Option; +} diff --git a/crates/improvise-incremental/tests/oracle.rs b/crates/improvise-incremental/tests/oracle.rs new file mode 100644 index 00000000..6b3304be --- /dev/null +++ b/crates/improvise-incremental/tests/oracle.rs @@ -0,0 +1,633 @@ +//! `Eager` against the evaluator, and the harness against wrong backends. +//! Evidence: `context/test-evidence/improvise-incremental.md`. + +use improvise_core::formula::ast::AggFunc; +use improvise_core::formula::parse_formula; +use improvise_core::model::cell::{CellKey, CellValue}; +use improvise_core::model::{Aggregation, Model}; +use improvise_core::view::Axis; +use improvise_incremental::{ + Derived, Divergence, Eager, Edit, Maintained, Probe, differential, probes, same_value, +}; +use proptest::prelude::*; +use std::path::Path; + +fn key(region: &str, product: &str, measure: &str) -> CellKey { + CellKey::new(vec![ + ("Region".into(), region.into()), + ("Product".into(), product.into()), + ("_Measure".into(), measure.into()), + ]) +} + +fn partial(product: &str, measure: &str) -> CellKey { + CellKey::new(vec![ + ("Product".into(), product.into()), + ("_Measure".into(), measure.into()), + ]) +} + +fn num(n: f64) -> Option { + Some(CellValue::Number(n)) +} + +fn text(s: &str) -> Option { + Some(CellValue::Text(s.into())) +} + +fn mixed() -> Option { + Some(CellValue::Error("mixed".into())) +} + +/// Region is the None category. Revenue and Cost are numbers; Note is text, +/// unanimous under A and conflicting under B. +fn hand_model() -> Model { + let mut m = Model::new("hand"); + for (cat, items) in [("Region", ["East", "West"]), ("Product", ["A", "B"])] { + m.add_category(cat).unwrap(); + for item in items { + m.category_mut(cat).unwrap().add_item(item); + } + } + for measure in ["Revenue", "Cost", "Note"] { + m.category_mut("_Measure").unwrap().add_item(measure); + } + let rows = [ + ("East", "A", 10.0, 4.0, "n", "n"), + ("West", "A", 5.0, 2.0, "n", "n"), + ("East", "B", 7.0, 3.0, "x", "x"), + ("West", "B", 1.0, 1.0, "y", "y"), + ]; + for (r, p, rev, cost, note, _) in rows { + m.set_cell(key(r, p, "Revenue"), CellValue::Number(rev)); + m.set_cell(key(r, p, "Cost"), CellValue::Number(cost)); + m.set_cell(key(r, p, "Note"), CellValue::Text(note.into())); + } + m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap()); + m +} + +fn hand_eager() -> Eager { + Eager::new(hand_model(), vec!["Region".into()]) +} + +fn hand_edits() -> Vec { + vec![ + Edit::SetCell(key("East", "A", "Revenue"), CellValue::Number(20.0)), + Edit::SetMeasureAgg { + measure: "Revenue".into(), + func: AggFunc::Max, + }, + Edit::ClearCell(key("West", "A", "Revenue")), + Edit::SetCell(key("West", "B", "Revenue"), CellValue::Text("oops".into())), + Edit::AddFormula(parse_formula("Double = Revenue * 2", "_Measure").unwrap()), + Edit::RemoveFormula { + target: "Profit".into(), + target_category: "_Measure".into(), + }, + ] +} + +#[test] +fn eager_raw_fold_sums_by_default() { + assert_eq!(hand_eager().raw_fold(&partial("A", "Revenue")), num(15.0)); +} + +#[test] +fn eager_raw_fold_follows_the_measures_aggregation() { + let mut eager = hand_eager(); + eager.apply(&Edit::SetMeasureAgg { + measure: "Revenue".into(), + func: AggFunc::Max, + }); + assert_eq!(eager.raw_fold(&partial("A", "Revenue")), num(10.0)); + assert_eq!(eager.raw_fold(&partial("A", "Cost")), num(6.0)); +} + +#[test] +fn eager_raw_fold_of_text_is_unanimous_or_mixed() { + let eager = hand_eager(); + assert_eq!(eager.raw_fold(&partial("A", "Note")), text("n")); + assert_eq!(eager.raw_fold(&partial("B", "Note")), mixed()); +} + +#[test] +fn eager_value_reads_a_formula_exactly_and_folded() { + let eager = hand_eager(); + assert_eq!( + eager.value(&key("East", "A", "Profit"), Aggregation::Exact), + num(6.0) + ); + assert_eq!( + eager.value(&partial("A", "Profit"), Aggregation::OverHidden), + num(9.0) + ); +} + +#[test] +fn eager_follows_an_edit_to_the_cell_a_fold_reads() { + let mut eager = hand_eager(); + eager.apply(&Edit::SetCell( + key("East", "A", "Revenue"), + CellValue::Number(20.0), + )); + assert_eq!(eager.raw_fold(&partial("A", "Revenue")), num(25.0)); + assert_eq!( + eager.value(&partial("A", "Profit"), Aggregation::OverHidden), + num(19.0) + ); +} + +#[test] +fn eager_numbers_among_text_are_mixed() { + let mut eager = hand_eager(); + eager.apply(&Edit::SetCell( + key("West", "B", "Revenue"), + CellValue::Text("oops".into()), + )); + assert_eq!(eager.raw_fold(&partial("B", "Revenue")), mixed()); +} + +fn corpus() -> Vec<(String, Model, Vec)> { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let mut out = Vec::new(); + for file in ["examples/demo.improv", "web/demo.improv"] { + let wb = improvise_io::persistence::load(&root.join(file)).unwrap(); + let mut seen: Vec> = Vec::new(); + for (name, view) in &wb.views { + let none: Vec = view + .categories_on(Axis::None) + .into_iter() + .map(String::from) + .collect(); + if !seen.contains(&none) { + seen.push(none.clone()); + out.push((format!("{file}:{name}"), wb.model.clone(), none)); + } + } + } + out.push(("hand".into(), hand_model(), vec!["Region".into()])); + out +} + +/// Edits built from the model's own first cell: overwrite, text, clear, a new +/// record, a new aggregation, and the first formula removed then restored. +fn edits_for(model: &Model) -> Vec { + let Some((first, _)) = model.data.iter_cells().next() else { + return vec![]; + }; + let (last_cat, _) = first.0.last().unwrap().clone(); + let measure = first.get("_Measure").map(String::from); + let fresh = first.clone().with(last_cat, "Zz-new"); + let mut edits = vec![ + Edit::SetCell(first.clone(), CellValue::Number(7.5)), + Edit::SetCell(first.clone(), CellValue::Text("t".into())), + Edit::SetCell(fresh.clone(), CellValue::Number(3.0)), + Edit::ClearCell(first), + ]; + edits.extend(measure.map(|m| Edit::SetMeasureAgg { + measure: m, + func: AggFunc::Max, + })); + if let Some(f) = model.formulas().first() { + edits.push(Edit::RemoveFormula { + target: f.target.clone(), + target_category: f.target_category.clone(), + }); + edits.push(Edit::AddFormula(f.clone())); + } + edits.push(Edit::ClearCell(fresh)); + edits +} + +#[test] +fn eager_matches_the_evaluator_on_the_shipped_documents() { + for (name, model, none) in corpus() { + let eager = Eager::new(model.clone(), none.clone()); + let mut direct = model.clone(); + direct.recompute_formulas(&none); + let asked = probes(eager.model()); + assert!(!asked.is_empty(), "{name}: no probes"); + for probe in asked { + let want = match &probe { + Probe::Value(k, a) => direct.evaluate_aggregated(k, *a), + Probe::RawFold(k) if direct.formula_for(k).is_some() => None, + Probe::RawFold(k) => direct.evaluate_aggregated(k, Aggregation::OverHidden), + }; + assert_eq!(probe.ask(&eager), want, "{name}: {probe:?}"); + } + } +} + +#[test] +fn eager_agrees_with_itself_over_every_corpus_edit_sequence() { + let mut files_done: Vec = Vec::new(); + for (name, model, none) in corpus() { + let file = name.split(':').next().unwrap().to_string(); + if files_done.contains(&file) { + continue; + } + files_done.push(file); + let mut edits = edits_for(&model); + // Probes grow with the cells squared-ish, and a debug build pays for it. + if model.data.iter_cells().count() > 40 { + edits.truncate(4); + } + let report = differential( + &model, + &none, + Eager::new(model.clone(), none.clone()), + &edits, + ) + .unwrap_or_else(|d| panic!("{name}: {d}")); + assert_eq!(report.steps, edits.len() + 1, "{name}"); + assert!(report.probes > 0, "{name}"); + } +} + +fn grouped_model() -> Model { + let mut m = Model::new("grouped"); + m.add_category("Region").unwrap(); + m.add_category("Product").unwrap(); + m.category_mut("Region") + .unwrap() + .add_group(improvise_core::model::category::Group::new("Coastal")); + let region = m.category_mut("Region").unwrap(); + region.add_item_in_group("East", "Coastal"); + region.add_item_in_group("West", "Coastal"); + region.add_item("Inland"); + m.category_mut("Product").unwrap().add_item("A"); + m.category_mut("_Measure").unwrap().add_item("Revenue"); + for (r, v) in [("East", 10.0), ("West", 5.0), ("Inland", 2.0)] { + m.set_cell(key(r, "A", "Revenue"), CellValue::Number(v)); + } + m +} + +fn coastal() -> CellKey { + key("Coastal", "A", "Revenue") +} + +/// A wrong backend: an `Eager` told every edit, answering one thing wrongly. +#[derive(Clone, Copy)] +enum Fault { + IgnoresAggregation, + IgnoresMeasureAggregation, + GoesStaleAfterTwoEdits, + IgnoresClear, + WrongFoldedFormula, + IgnoresRemoveFormula, + WrongWhenTwoAreDropped, + RawFoldReturnsFormula, + WrongOnAGroup, +} + +struct Rival { + inner: Eager, + fault: Fault, + seen: usize, +} + +impl Rival { + fn over(inner: Eager, fault: Fault) -> Self { + Rival { + inner, + fault, + seen: 0, + } + } + + fn of(fault: Fault) -> Self { + Self::over(hand_eager(), fault) + } +} + +impl Derived for Rival { + fn value(&self, k: &CellKey, agg: Aggregation) -> Option { + let wrong = match self.fault { + Fault::WrongFoldedFormula => { + *k == partial("A", "Profit") && agg == Aggregation::OverHidden + } + Fault::WrongWhenTwoAreDropped => *k == only_revenue(), + Fault::WrongOnAGroup => *k == coastal() && agg == Aggregation::Exact, + _ => false, + }; + match (wrong, self.fault) { + (true, _) => num(-999.0), + (_, Fault::IgnoresAggregation) => self.inner.value(k, Aggregation::Exact), + _ => self.inner.value(k, agg), + } + } + + fn raw_fold(&self, p: &CellKey) -> Option { + match self.fault { + Fault::WrongWhenTwoAreDropped if *p == only_revenue() => num(-999.0), + Fault::RawFoldReturnsFormula => self.inner.value(p, Aggregation::OverHidden), + _ => self.inner.raw_fold(p), + } + } +} + +impl Maintained for Rival { + fn apply(&mut self, edit: &Edit) { + self.seen += 1; + let skip = match (self.fault, edit) { + (Fault::IgnoresMeasureAggregation, Edit::SetMeasureAgg { .. }) => true, + (Fault::GoesStaleAfterTwoEdits, _) => self.seen > 2, + (Fault::IgnoresClear, Edit::ClearCell(_)) => true, + (Fault::IgnoresRemoveFormula, Edit::RemoveFormula { .. }) => true, + _ => false, + }; + if !skip { + self.inner.apply(edit); + } + } +} + +fn only_revenue() -> CellKey { + CellKey::new(vec![("_Measure".into(), "Revenue".into())]) +} + +fn measure_of(probe: &Probe) -> Option<&str> { + match probe { + Probe::Value(k, _) | Probe::RawFold(k) => k.get("_Measure"), + } +} + +fn run_rival(fault: Fault) -> Result { + differential( + &hand_model(), + &["Region".to_string()], + Rival::of(fault), + &hand_edits(), + ) +} + +#[test] +fn the_harness_passes_a_backend_that_is_right() { + let report = differential( + &hand_model(), + &["Region".to_string()], + hand_eager(), + &hand_edits(), + ) + .unwrap(); + assert_eq!(report.steps, hand_edits().len() + 1); +} + +#[test] +fn the_harness_catches_a_backend_that_ignores_the_aggregation() { + let d = run_rival(Fault::IgnoresAggregation).unwrap_err(); + assert_eq!(d.step, 0); + assert_eq!( + d.probe, + Probe::Value(key("East", "A", "Profit"), Aggregation::OverHidden) + ); + assert_eq!((d.eager, d.candidate), (None, num(6.0))); +} + +#[test] +fn the_harness_catches_a_backend_that_ignores_the_measure_aggregation() { + let d = run_rival(Fault::IgnoresMeasureAggregation).unwrap_err(); + assert_eq!(d.step, 2, "{d}"); + assert_eq!(measure_of(&d.probe), Some("Revenue"), "{d}"); +} + +#[test] +fn the_harness_catches_a_backend_that_goes_stale_after_two_edits() { + let d = run_rival(Fault::GoesStaleAfterTwoEdits).unwrap_err(); + assert_eq!(d.step, 3, "{d}"); + assert_eq!(measure_of(&d.probe), Some("Revenue"), "{d}"); +} + +#[test] +fn the_harness_catches_a_backend_that_ignores_a_clear() { + let d = run_rival(Fault::IgnoresClear).unwrap_err(); + assert_eq!(d.step, 3, "{d}"); + assert_eq!(measure_of(&d.probe), Some("Revenue"), "{d}"); + assert!(d.to_string().starts_with("divergence at step 3:"), "{d}"); +} + +#[test] +fn the_harness_catches_a_wrong_folded_formula_value() { + let d = run_rival(Fault::WrongFoldedFormula).unwrap_err(); + assert_eq!(d.step, 0); + assert_eq!( + d.probe, + Probe::Value(partial("A", "Profit"), Aggregation::OverHidden) + ); + assert_eq!((d.eager, d.candidate), (num(9.0), num(-999.0))); +} + +#[test] +fn the_harness_catches_a_backend_that_keeps_answering_for_a_removed_formula() { + let d = run_rival(Fault::IgnoresRemoveFormula).unwrap_err(); + assert_eq!(d.step, hand_edits().len(), "{d}"); + assert_eq!(measure_of(&d.probe), Some("Profit"), "{d}"); + assert!(d.eager.is_none() && d.candidate.is_some(), "{d}"); +} + +#[test] +fn the_harness_catches_a_fault_that_only_shows_after_two_coordinates_are_dropped() { + let d = run_rival(Fault::WrongWhenTwoAreDropped).unwrap_err(); + assert_eq!(d.step, 0); + assert_eq!(d.probe, Probe::Value(only_revenue(), Aggregation::Exact)); + assert_eq!(d.candidate, num(-999.0)); +} + +#[test] +fn the_harness_catches_a_raw_fold_that_returns_a_formula() { + let d = run_rival(Fault::RawFoldReturnsFormula).unwrap_err(); + assert_eq!(d.step, 0); + assert!(matches!(d.probe, Probe::RawFold(_)), "{d}"); + assert_eq!(measure_of(&d.probe), Some("Profit"), "{d}"); + assert!(d.eager.is_none() && d.candidate.is_some(), "{d}"); +} + +#[test] +fn the_harness_catches_a_wrong_group_fold() { + let eager = Eager::new(grouped_model(), vec![]); + let d = differential( + &grouped_model(), + &[], + Rival::over(eager, Fault::WrongOnAGroup), + &[], + ) + .unwrap_err(); + assert_eq!(d.probe, Probe::Value(coastal(), Aggregation::Exact)); + assert_eq!((d.eager, d.candidate), (num(15.0), num(-999.0))); +} + +#[test] +fn eager_folds_a_group_it_is_asked_about() { + let eager = Eager::new(grouped_model(), vec![]); + assert_eq!(eager.value(&coastal(), Aggregation::Exact), num(15.0)); +} + +#[test] +fn eager_raw_fold_is_never_a_formula() { + let eager = hand_eager(); + assert_eq!( + eager.value(&partial("A", "Profit"), Aggregation::OverHidden), + num(9.0) + ); + assert_eq!(eager.raw_fold(&partial("A", "Profit")), None); +} + +#[test] +fn eager_add_and_remove_formula_change_what_a_key_reads() { + let mut eager = hand_eager(); + let k = key("East", "A", "Double"); + assert_eq!(eager.value(&k, Aggregation::Exact), None); + eager.apply(&Edit::AddFormula( + parse_formula("Double = Revenue * 2", "_Measure").unwrap(), + )); + assert_eq!(eager.value(&k, Aggregation::Exact), num(20.0)); + eager.apply(&Edit::RemoveFormula { + target: "Double".into(), + target_category: "_Measure".into(), + }); + assert_eq!(eager.value(&k, Aggregation::Exact), None); +} + +#[test] +fn eager_clear_cell_leaves_the_key_absent() { + let mut eager = hand_eager(); + let k = key("East", "A", "Revenue"); + eager.apply(&Edit::ClearCell(k.clone())); + assert_eq!(eager.value(&k, Aggregation::Exact), None); + assert_eq!(eager.model().get_cell(&k), None); +} + +#[test] +fn same_value_follows_the_float_rules_and_is_exact_elsewhere() { + let n = |x: f64| num(x); + assert!(same_value(&n(f64::NAN), &n(f64::NAN))); + assert!(same_value(&n(-0.0), &n(0.0))); + assert!(!same_value(&n(1.0), &n(1.000_000_000_000_000_2))); + assert!(!same_value(&n(1.0), &n(f64::NAN))); + assert!(same_value(&text("a"), &text("a"))); + assert!(!same_value(&text("a"), &text("b"))); + let err = |s: &str| Some(CellValue::Error(s.into())); + assert!(same_value(&err("mixed"), &err("mixed"))); + assert!(!same_value(&err("mixed"), &err("circular"))); + assert!(!same_value(&None, &n(0.0))); + assert!(same_value(&None, &None)); +} + +#[test] +fn probes_cover_folds_formulas_measures_and_groups() { + let hand = probes(&hand_model()); + let has = |p: Probe| hand.contains(&p); + assert!(has(Probe::RawFold(partial("A", "Revenue")))); + assert!(has(Probe::Value( + key("East", "A", "Revenue"), + Aggregation::Exact + ))); + assert!(has(Probe::Value( + partial("A", "Profit"), + Aggregation::Exact + ))); + assert!(has(Probe::Value( + partial("A", "Profit"), + Aggregation::OverHidden + ))); + assert!(has(Probe::Value(only_revenue(), Aggregation::OverHidden))); + assert!(has(Probe::RawFold(CellKey::new(vec![])))); + let grouped = probes(&grouped_model()); + assert!(grouped.contains(&Probe::Value(coastal(), Aggregation::Exact))); +} + +#[test] +fn an_edit_names_its_key_as_two_value_probes_and_a_fold() { + let k = key("East", "A", "Revenue"); + let asked = improvise_incremental::edit_probes(&Edit::ClearCell(k.clone())); + assert!(asked.contains(&Probe::Value(k.clone(), Aggregation::Exact))); + assert!(asked.contains(&Probe::Value(k.clone(), Aggregation::OverHidden))); + assert!(asked.contains(&Probe::RawFold(k))); + let none = improvise_incremental::edit_probes(&Edit::SetMeasureAgg { + measure: "Revenue".into(), + func: AggFunc::Max, + }); + assert!(none.is_empty()); +} + +/// An `Eager` rebuilt from scratch on every edit: maintaining one in place +/// must give the same answers as building it afresh. +struct Fresh { + model: Model, + none: Vec, + eager: Eager, +} + +impl Derived for Fresh { + fn value(&self, k: &CellKey, agg: Aggregation) -> Option { + self.eager.value(k, agg) + } + + fn raw_fold(&self, p: &CellKey) -> Option { + self.eager.raw_fold(p) + } +} + +impl Maintained for Fresh { + fn apply(&mut self, edit: &Edit) { + edit.apply(&mut self.model); + self.eager = Eager::new(self.model.clone(), self.none.clone()); + } +} + +fn any_key() -> impl Strategy { + ( + prop::sample::select(vec!["East", "West"]), + prop::sample::select(vec!["A", "B"]), + prop::sample::select(vec!["Revenue", "Cost", "Note"]), + ) + .prop_map(|(r, p, m)| key(r, p, m)) +} + +fn any_value() -> impl Strategy { + prop_oneof![ + (-4i32..5).prop_map(|n| CellValue::Number(f64::from(n))), + Just(CellValue::Number(-0.0)), + prop::sample::select(vec!["n", "x"]).prop_map(|t| CellValue::Text(t.into())), + ] +} + +fn any_edit() -> impl Strategy { + prop_oneof![ + (any_key(), any_value()).prop_map(|(k, v)| Edit::SetCell(k, v)), + any_key().prop_map(Edit::ClearCell), + prop::sample::select(vec!["Profit = Revenue - Cost", "Double = Revenue * 2"]) + .prop_map(|raw| Edit::AddFormula(parse_formula(raw, "_Measure").unwrap())), + prop::sample::select(vec!["Profit", "Double"]).prop_map(|t| Edit::RemoveFormula { + target: t.into(), + target_category: "_Measure".into(), + }), + ( + prop::sample::select(vec!["Revenue", "Cost"]), + prop::sample::select(vec![AggFunc::Sum, AggFunc::Max, AggFunc::Min, AggFunc::Avg]), + ) + .prop_map(|(m, func)| Edit::SetMeasureAgg { + measure: m.into(), + func, + }), + ] +} + +proptest! { + #[test] + fn a_maintained_eager_equals_a_fresh_one_over_random_edit_sequences( + edits in prop::collection::vec(any_edit(), 0..4) + ) { + let model = hand_model(); + let none = vec!["Region".to_string()]; + let fresh = Fresh { + model: model.clone(), + none: none.clone(), + eager: Eager::new(model.clone(), none.clone()), + }; + let run = differential(&model, &none, fresh, &edits); + prop_assert!(run.is_ok(), "{}", run.unwrap_err()); + } +} diff --git a/crates/improvise-io/Cargo.toml b/crates/improvise-io/Cargo.toml index bb8d367f..88f544ee 100644 --- a/crates/improvise-io/Cargo.toml +++ b/crates/improvise-io/Cargo.toml @@ -14,8 +14,8 @@ chrono = { version = "0.4", features = ["serde"] } csv = "1" flate2 = "1" indexmap = "2" -pest = "2.9.2" -pest_derive = "2.9.2" +pest = "2.9.3" +pest_derive = "2.9.3" serde = { version = "1", features = ["derive"] } # Parses JSON a user's numbers cross, so it must read back the f64 written: per crate, # because each build stands alone (improvise-6yvg, context/code/protocol.md). @@ -47,6 +47,6 @@ features = [ ] [dev-dependencies] -pest_meta = "2.9.2" +pest_meta = "2.9.3" proptest = "1" tempfile = "3" diff --git a/flake.lock b/flake.lock index f0a00207..60fb16ca 100644 --- a/flake.lock +++ b/flake.lock @@ -47,11 +47,11 @@ }, "crane": { "locked": { - "lastModified": 1789760033, - "narHash": "sha256-jtT4yxZpR8seYnlCMMWSSPlFN92zO6ICuZ8pJrmi86k=", + "lastModified": 1790899423, + "narHash": "sha256-THGtz65UGJU+sonZidRboyCMk+9DclpMF7pwL4uDV4M=", "owner": "ipetkov", "repo": "crane", - "rev": "73b980519cefc727a5f6cc8e5c0947a2f9be6edd", + "rev": "47b6b27ed9a3a9181415e4367d0c30ab2a0e0250", "type": "github" }, "original": { @@ -71,11 +71,11 @@ "pre-commit-hooks": "pre-commit-hooks" }, "locked": { - "lastModified": 1782759041, - "narHash": "sha256-2577vxyoBa8+ZRiXr3CuPtOuEtPRYsFPSZuEc/KI/80=", + "lastModified": 1790579054, + "narHash": "sha256-S2NrwKB7aQa6HZsg2UGCmzQPI5Dw+bqUPm8R23BAUh8=", "owner": "nix-community", "repo": "crate2nix", - "rev": "5e1ecfd2d15b34ec90c2e51fdffbe8116595a767", + "rev": "1cb60331b14f15dad145dbd4e207c1ec110e5675", "type": "github" }, "original": { @@ -297,11 +297,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1790185690, - "narHash": "sha256-xJ+X4hBtOcAFGBOe5nAMyMUeF9foJBmIOu3NjBqBycU=", + "lastModified": 1791456040, + "narHash": "sha256-H83yz/do+hjOO0uujHyH8RgtIQP2v7Mw+cLwcZK/xKc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4975466d324710c576dc11ad614684e6bd8cad8e", + "rev": "e7439b6b14ad3cc35d05608ebca9bce01a25f5f8", "type": "github" }, "original": { @@ -368,11 +368,11 @@ "nixpkgs": "nixpkgs_5" }, "locked": { - "lastModified": 1790234952, - "narHash": "sha256-tjQa1vDoHJir4lasZTakz/xgmsfB6SRjeQqaIbefFOk=", + "lastModified": 1791535654, + "narHash": "sha256-QFgGi6Wgh7AgqPFdAXEmkTgnR0B5M0omheIHvo/02dU=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "ed34466c59b766481248959a21a6e46cf0c60468", + "rev": "4ca2963243427cb86bd6b4d9ca6010db0162a619", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 278b5d65..dce8b0e9 100644 --- a/flake.nix +++ b/flake.nix @@ -121,8 +121,10 @@ pkgs.rust-analyzer crate2nix.packages.${system}.default pkgs.cargo-expand + pkgs.cargo-edit pkgs.cargo-llvm-cov pkgs.global + pkgs.iconv pkgs.universal-ctags beads.packages.${system}.default codeMaat diff --git a/harness/opfs-contract/Cargo.lock b/harness/opfs-contract/Cargo.lock index 67bee125..704469e7 100644 --- a/harness/opfs-contract/Cargo.lock +++ b/harness/opfs-contract/Cargo.lock @@ -307,19 +307,21 @@ dependencies = [ [[package]] name = "pest" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" +checksum = "b568374ba38b33a6c627141f891faf16902b08d2db26b8ede1bcb0a15b1919fa" dependencies = [ "memchr", + "psm", + "stacker", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" +checksum = "b66e184b924cebaaff20ab2256ca52f12332d528a39aa76553b5d96f92aacf7f" dependencies = [ "pest", "pest_generator", @@ -327,22 +329,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" +checksum = "a87478d267e4de54a626af9754f2f0f58e927aac6ed0575fe89bc05ad6851694" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] name = "pest_meta" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" +checksum = "4f986f248b4241ac359b831f6139aaa34e03b08a37b6caf7e201a33f95c869e1" dependencies = [ "pest", ] @@ -362,6 +364,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + [[package]] name = "quote" version = "1.0.47" @@ -450,6 +461,19 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + [[package]] name = "syn" version = "2.0.119" @@ -489,9 +513,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-ident" @@ -624,6 +648,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "zlib-rs" version = "0.6.8" diff --git a/nix/ci.nix b/nix/ci.nix index 3a35eb86..1b0500f3 100644 --- a/nix/ci.nix +++ b/nix/ci.nix @@ -50,6 +50,8 @@ "numtower" # ADR 009 clause 1: std-only; nothing depends on it yet. "zset" + # ADR 009 clause 2: the Derived trait, Eager and the differential harness. + "improvise-incremental" ]; pFlags = crates: lib.concatMapStringsSep " " (c: "-p ${c}") crates; diff --git a/scripts/build-doc-site.mjs b/scripts/build-doc-site.mjs index 0ccf5cc6..2fdb30ca 100644 --- a/scripts/build-doc-site.mjs +++ b/scripts/build-doc-site.mjs @@ -69,6 +69,7 @@ const SECTIONS = [ ["context/code/gp.md", "code/gp", "gp-tree and improvise-gp"], ["context/code/numtower.md", "code/numtower", "numtower, the numeric tower"], ["context/code/zset.md", "code/zset", "zset, the Z-set group"], + ["context/code/improvise-incremental.md", "code/improvise-incremental", "improvise-incremental, the plug"], ["context/code/kb-tool.md", "code/kb-tool", "The kb tool"], ["context/code/tooling.md", "code/tooling", "The repository's own checkers"], ], @@ -89,6 +90,7 @@ const SECTIONS = [ ["context/test-evidence/gp.md", "test-evidence/gp", "gp-tree and improvise-gp"], ["context/test-evidence/numtower.md", "test-evidence/numtower", "numtower"], ["context/test-evidence/zset.md", "test-evidence/zset", "zset"], + ["context/test-evidence/improvise-incremental.md", "test-evidence/improvise-incremental", "improvise-incremental"], ["context/test-evidence/tooling.md", "test-evidence/tooling", "The repository's own gates"], ["context/test-evidence/layout-cost.md", "test-evidence/layout-cost", "Layout and keypress cost"], ["context/test-evidence/scale-bisect.md", "test-evidence/scale-bisect", "Scale bisect"],