From f0a35bfa31ceeb310fb81afc414a8179ae22b9dc Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 22 Sep 2026 21:25:58 +0100 Subject: [PATCH 1/4] Ensure reconcile(p, a, a) == a --- src/operation_transformation.rs | 3 + .../utils/elongate_operations.rs | 114 ++++++++++-------- src/utils/myers_diff.rs | 39 ++++-- tests/idempotence.rs | 81 +++++++++++++ 4 files changed, 179 insertions(+), 58 deletions(-) create mode 100644 tests/idempotence.rs diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs index bbaf73e..c549b6c 100644 --- a/src/operation_transformation.rs +++ b/src/operation_transformation.rs @@ -14,6 +14,9 @@ use crate::{Tokenizer, types::text_with_cursors::TextWithCursors}; /// return a document containing all changes from both `left` /// and `right`. /// +/// If both updated texts are identical, the resulting text is exactly that +/// text, regardless of the original document. +/// /// If a span has been inserted in either the `left` or `right` /// versions, it will be present in the return value. If both sides /// insert the same span with a common prefix, that prefix will only diff --git a/src/operation_transformation/utils/elongate_operations.rs b/src/operation_transformation/utils/elongate_operations.rs index fd388be..2096bef 100644 --- a/src/operation_transformation/utils/elongate_operations.rs +++ b/src/operation_transformation/utils/elongate_operations.rs @@ -1,4 +1,3 @@ -use core::iter; use std::fmt::Debug; use crate::raw_operation::RawOperation; @@ -11,55 +10,40 @@ where I: IntoIterator>, T: PartialEq + Clone + Debug, { - // This might look bad, but this makes sense. The inserts and deltes can be - // interleaved, such as: IDIDID and we need to turn this into IIIDDD. - // So we need to keep track of both the last insert and delete operations, not - // just the last one. - let mut maybe_previous_insert: Option> = None; - let mut maybe_previous_delete: Option> = None; + let mut inserts: Vec> = Vec::new(); + let mut deletes: Vec> = Vec::new(); + let mut result = Vec::new(); - // We don't elongate `equals` as they're needed to maintain cursor positions - // when merging against deletes. - let mut result: Vec> = raw_operations - .into_iter() - .flat_map(|next| match next { - RawOperation::Insert(..) => match maybe_previous_insert.take() { - Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => { - maybe_previous_insert = Some(prev.join(next)); - Box::new(iter::empty()) as Box>> - } - prev => { - maybe_previous_insert = Some(next); - Box::new(prev.into_iter()) - } - }, - RawOperation::Delete(..) => match maybe_previous_delete.take() { - Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => { - maybe_previous_delete = Some(prev.join(next)); - Box::new(iter::empty()) as Box>> - } - prev => { - maybe_previous_delete = Some(next); - Box::new(prev.into_iter()) - } - }, - RawOperation::Equal(..) => Box::new( - maybe_previous_delete - .take() - .into_iter() - .chain(maybe_previous_insert.take()) - .chain(iter::once(next)), - ) as Box>>, - }) - .collect(); + // Emit all deletions before insertions within each changed span, even + // when tokens cannot be joined. Otherwise an insertion followed by a + // deletion at the same offset can separate identical insertions during + // merging and prevent their deduplication. + for next in raw_operations { + let pending = match next { + RawOperation::Insert(..) => &mut inserts, + RawOperation::Delete(..) => &mut deletes, + RawOperation::Equal(..) => { + result.append(&mut deletes); + result.append(&mut inserts); - if let Some(prev) = maybe_previous_delete { - result.push(prev); + // Keep retains separate for cursor positioning + result.push(next); + + continue; + } + }; + + match pending.pop() { + Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => { + pending.push(prev.join(next)); + } + Some(prev) => pending.extend([prev, next]), + None => pending.push(next), + } } - if let Some(prev) = maybe_previous_insert { - result.push(prev); - } + result.append(&mut deletes); + result.append(&mut inserts); result } @@ -115,7 +99,7 @@ mod tests { #[test] fn merges_interleaved_insert_delete_sequences() { - // Pattern IDID -> II DD + // Pattern IDID -> DD II let ops = vec![ins(&["i1"]), del(&["d1"]), ins(&["i2"]), del(&["d2"])]; let result = elongate_operations(ops); @@ -123,4 +107,40 @@ mod tests { assert!(matches!(result[0], RawOperation::Delete(_))); assert!(matches!(result[1], RawOperation::Insert(_))); } + + #[test] + fn orders_non_joinable_edits_before_each_retain() { + let insert1 = ins_custom("a", false, false); + let insert2 = ins_custom("\n", false, false); + let delete1 = RawOperation::Delete(vec![Token::new( + "b".to_owned(), + "b".to_owned(), + false, + false, + )]); + let delete2 = del(&["c"]); + let retain = RawOperation::Equal(vec!["d".into()]); + let ops = vec![ + insert1.clone(), + delete1.clone(), + insert2.clone(), + delete2.clone(), + retain.clone(), + insert1.clone(), + delete1.clone(), + ]; + + assert_eq!( + elongate_operations(ops), + vec![ + delete1.clone(), + delete2, + insert1.clone(), + insert2, + retain, + delete1, + insert1, + ], + ); + } } diff --git a/src/utils/myers_diff.rs b/src/utils/myers_diff.rs index f04df56..0480b3b 100644 --- a/src/utils/myers_diff.rs +++ b/src/utils/myers_diff.rs @@ -306,10 +306,10 @@ fn conquer( // Check for common prefix let prefix_len = common_prefix_len(old, old_range.clone(), new, new_range.clone()); if prefix_len > 0 { - result.extend( - old[old_range.start..old_range.start + prefix_len] - .iter() - .map(|token| RawOperation::Equal(vec![token.clone()])), + emit_matches( + &old[old_range.start..old_range.start + prefix_len], + &new[new_range.start..new_range.start + prefix_len], + result, ); } old_range.start += prefix_len; @@ -317,9 +317,10 @@ fn conquer( // Check for common suffix let suffix_len = common_suffix_len(old, old_range.clone(), new, new_range.clone()); - let suffix_start = old_range.end - suffix_len; - old_range.end -= suffix_len; - new_range.end -= suffix_len; + let old_suffix_start = old_range.end - suffix_len; + let new_suffix_start = new_range.end - suffix_len; + old_range.end = old_suffix_start; + new_range.end = new_suffix_start; if old_range.is_empty() && new_range.is_empty() { // do nothing @@ -377,14 +378,30 @@ fn conquer( } if suffix_len > 0 { - result.extend( - old[suffix_start..suffix_start + suffix_len] - .iter() - .map(|token| RawOperation::Equal(vec![token.clone()])), + emit_matches( + &old[old_suffix_start..old_suffix_start + suffix_len], + &new[new_suffix_start..new_suffix_start + suffix_len], + result, ); } } +/// Normalized matches guide alignment, but only identical original text can +/// be retained. Emit replacements for matches with different spellings. +fn emit_matches(old: &[Token], new: &[Token], result: &mut Vec>) +where + T: PartialEq + Clone + Debug, +{ + for (old_token, new_token) in old.iter().zip(new) { + if old_token.original() == new_token.original() { + result.push(RawOperation::Equal(vec![old_token.clone()])); + } else { + result.push(RawOperation::Delete(vec![old_token.clone()])); + result.push(RawOperation::Insert(vec![new_token.clone()])); + } + } +} + #[cfg(test)] mod tests { use insta::assert_debug_snapshot; diff --git a/tests/idempotence.rs b/tests/idempotence.rs new file mode 100644 index 0000000..ca154f7 --- /dev/null +++ b/tests/idempotence.rs @@ -0,0 +1,81 @@ +use reconcile_text::{BuiltinTokenizer, Token, reconcile}; +use test_case::{test_case, test_matrix}; + +#[test_matrix([ + BuiltinTokenizer::Character, + BuiltinTokenizer::Line, + BuiltinTokenizer::Markdown, + BuiltinTokenizer::Word +])] +fn identical_edits_preserve_text(tokenizer: BuiltinTokenizer) { + let texts = [ + "", + "a", + "b\n", + "\r\n\r\n", + "# Héllo\n\n- 世界\n", + "a a a\nb b b\n", + ]; + for parent in texts { + for updated in texts { + let input = updated.into(); + assert_eq!( + reconcile(parent, &input, &input, &*tokenizer) + .apply() + .text(), + updated, + "tokenizer={tokenizer:?}, parent={parent:?}, updated={updated:?}", + ); + } + } +} + +#[test_matrix([ + BuiltinTokenizer::Character, + BuiltinTokenizer::Line, + BuiltinTokenizer::Markdown, + BuiltinTokenizer::Word +])] +fn identical_short_edits_preserve_text(tokenizer: BuiltinTokenizer) { + let mut texts = vec![String::new()]; + let mut level = texts.clone(); + for _ in 0..3 { + level = level + .iter() + .flat_map(|text| ['a', 'b', ' ', '\n'].map(|ch| format!("{text}{ch}"))) + .collect(); + texts.extend(level.iter().cloned()); + } + + for parent in &texts { + for updated in &texts { + let input = updated.into(); + assert_eq!( + reconcile(parent, &input, &input, &*tokenizer) + .apply() + .text(), + *updated, + "tokenizer={tokenizer:?}, parent={parent:?}, updated={updated:?}", + ); + } + } +} + +#[test_case("A", "a"; "lowercase")] +#[test_case("a", "A"; "uppercase")] +#[test_case("Hello WORLD", "HELLO world")] +#[test_case("X A", "x a\n")] +#[test_case("A", "new a"; "normalized_suffix")] +#[test_case("a old B", "A new b"; "normalized_prefix_and_suffix")] +fn identical_edits_preserve_original_spelling(parent: &str, updated: &str) { + let tokenizer = |text: &str| { + text.chars() + .map(|ch| Token::new(ch.to_ascii_lowercase(), ch.to_string(), false, false)) + .collect() + }; + let input = updated.into(); + assert_eq!( + reconcile(parent, &input, &input, &tokenizer).apply().text(), + updated, + ); +} From 20914afd1753113792ec4becc626881b1f4ada3e Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 22 Sep 2026 21:59:06 +0100 Subject: [PATCH 2/4] Fix sed on linux --- scripts/bump-version.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 3298f8b..8c3fd47 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -24,6 +24,15 @@ else echo "Your working directory is clean." fi +# GNU sed takes the suffix as part of -i, BSD sed requires a separate argument +sed_inplace() { + if sed --version >/dev/null 2>&1; then + sed -i "$@" + else + sed -i '' "$@" + fi +} + echo "Bumping versions" which cargo-set-version || cargo install cargo-edit @@ -39,8 +48,8 @@ npm install NEWVER=$(grep '^version = ' ../Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') cd ../reconcile-python -sed -i '' "s/^version = \".*\"/version = \"$NEWVER\"/" Cargo.toml -sed -i '' "s/^version = \".*\"/version = \"$NEWVER\"/" pyproject.toml +sed_inplace "s/^version = \".*\"/version = \"$NEWVER\"/" Cargo.toml +sed_inplace "s/^version = \".*\"/version = \"$NEWVER\"/" pyproject.toml cargo update --workspace uv lock From 6f7602f318ea09468ce785c6698d4c7c61f80dda Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 22 Sep 2026 21:59:16 +0100 Subject: [PATCH 3/4] Bump versions to 0.13.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- examples/website/package-lock.json | 14 ++----------- reconcile-js/package-lock.json | 14 +++---------- reconcile-js/package.json | 2 +- reconcile-python/Cargo.lock | 4 ++-- reconcile-python/Cargo.toml | 2 +- reconcile-python/pyproject.toml | 2 +- reconcile-python/uv.lock | 32 +++++++++++++++--------------- 9 files changed, 28 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0971f12..2ef1897 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,7 +428,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "reconcile-text" -version = "0.12.1" +version = "0.13.0" dependencies = [ "console_error_panic_hook", "diff-match-patch-rs", diff --git a/Cargo.toml b/Cargo.toml index bce734f..88a4d3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "reconcile-text" description = "Intelligent 3-way text merging with automated conflict resolution" -version = "0.12.1" +version = "0.13.0" rust-version = "1.94" authors = ["Andras Schmelczer "] edition = "2024" diff --git a/examples/website/package-lock.json b/examples/website/package-lock.json index cfab4c3..7061433 100644 --- a/examples/website/package-lock.json +++ b/examples/website/package-lock.json @@ -28,7 +28,7 @@ }, "../../reconcile-js": { "name": "reconcile-text", - "version": "0.12.1", + "version": "0.13.0", "dev": true, "license": "MIT", "devDependencies": { @@ -1136,7 +1136,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1177,7 +1176,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1414,7 +1412,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3980,7 +3977,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4489,7 +4485,6 @@ "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", @@ -5309,7 +5304,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5393,8 +5387,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsyringe": { "version": "4.10.0", @@ -5436,7 +5429,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5577,7 +5569,6 @@ "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -5627,7 +5618,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json index 397ff47..ca49f08 100644 --- a/reconcile-js/package-lock.json +++ b/reconcile-js/package-lock.json @@ -1,12 +1,12 @@ { "name": "reconcile-text", - "version": "0.12.1", + "version": "0.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reconcile-text", - "version": "0.12.1", + "version": "0.13.0", "license": "MIT", "devDependencies": { "@types/jest": "^30.0.0", @@ -25,7 +25,7 @@ }, "../pkg": { "name": "reconcile-text", - "version": "0.12.1", + "version": "0.13.0", "dev": true, "license": "MIT" }, @@ -66,7 +66,6 @@ "version": "7.28.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1658,7 +1657,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1685,7 +1683,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1972,7 +1969,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3076,7 +3072,6 @@ "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.3.0", "@jest/types": "30.3.0", @@ -4960,7 +4955,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5097,7 +5091,6 @@ "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -5145,7 +5138,6 @@ "version": "6.0.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", diff --git a/reconcile-js/package.json b/reconcile-js/package.json index e713d6d..1339c8b 100644 --- a/reconcile-js/package.json +++ b/reconcile-js/package.json @@ -1,6 +1,6 @@ { "name": "reconcile-text", - "version": "0.12.1", + "version": "0.13.0", "description": "Intelligent 3-way text merging with automated conflict resolution", "main": "dist/reconcile.node.js", "browser": "dist/reconcile.web.js", diff --git a/reconcile-python/Cargo.lock b/reconcile-python/Cargo.lock index 1ea367b..d2ae578 100644 --- a/reconcile-python/Cargo.lock +++ b/reconcile-python/Cargo.lock @@ -130,14 +130,14 @@ dependencies = [ [[package]] name = "reconcile-text" -version = "0.12.1" +version = "0.13.0" dependencies = [ "thiserror", ] [[package]] name = "reconcile-text-python" -version = "0.12.1" +version = "0.13.0" dependencies = [ "pyo3", "reconcile-text", diff --git a/reconcile-python/Cargo.toml b/reconcile-python/Cargo.toml index 37facf0..7ff3cfc 100644 --- a/reconcile-python/Cargo.toml +++ b/reconcile-python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reconcile-text-python" -version = "0.12.1" +version = "0.13.0" edition = "2024" rust-version = "1.94" authors = ["Andras Schmelczer "] diff --git a/reconcile-python/pyproject.toml b/reconcile-python/pyproject.toml index 45a1c4b..9ad5ef3 100644 --- a/reconcile-python/pyproject.toml +++ b/reconcile-python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "reconcile-text" -version = "0.12.1" +version = "0.13.0" description = "Intelligent 3-way text merging with automated conflict resolution" readme = "README.md" license = { text = "MIT" } diff --git a/reconcile-python/uv.lock b/reconcile-python/uv.lock index 6e5156f..6f2b13b 100644 --- a/reconcile-python/uv.lock +++ b/reconcile-python/uv.lock @@ -20,7 +20,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -132,13 +132,13 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pluggy", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -153,13 +153,13 @@ resolution-markers = [ "python_full_version >= '3.10'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pluggy", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -168,7 +168,7 @@ wheels = [ [[package]] name = "reconcile-text" -version = "0.12.1" +version = "0.13.0" source = { editable = "." } [package.dev-dependencies] From 1dda7c1093dd7ed84f5b27539a9c9b9b455f9c3b Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sat, 19 Sep 2026 20:03:02 +0000 Subject: [PATCH 4/4] Update dependency sass-loader to v16.0.8 --- examples/website/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/website/package-lock.json b/examples/website/package-lock.json index 7061433..6ec1055 100644 --- a/examples/website/package-lock.json +++ b/examples/website/package-lock.json @@ -4501,9 +4501,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.7.tgz", - "integrity": "sha512-w6q+fRHourZ+e+xA1kcsF27iGM6jdB8teexYCfdUw0sYgcDNeZESnDNT9sUmmPm3ooziwUJXGwZJSTF3kOdBfA==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.8.tgz", + "integrity": "sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==", "dev": true, "license": "MIT", "dependencies": {