diff --git a/CHANGELOG.md b/CHANGELOG.md index ff549e5..222c101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.1.0-beta.2 + +- Compare the crate and an in-tree helper on eight edit scenarios, including Cancel, Undo and changes to the target or revision. Run `cargo test --locked --example helper_comparison` for the executable checks. +- Document dependency source choices for hosts with an approval policy. The host maintainers choose their source. + ## 0.1.0-beta.1 Published on 1 November 2026 (Europe/Kyiv) to [crates.io](https://crates.io/crates/edit-session/0.1.0-beta.1) diff --git a/Cargo.lock b/Cargo.lock index f1ad1d8..826634d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "edit-session" -version = "0.1.0-beta.1" +version = "0.1.0-beta.2" diff --git a/Cargo.toml b/Cargo.toml index 4cb49b1..5780eac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "edit-session" -version = "0.1.0-beta.1" +version = "0.1.0-beta.2" edition = "2024" rust-version = "1.88" description = "Toolkit-independent preview, validation and commit lifecycle" @@ -16,6 +16,10 @@ include = ["src/**", "tests/**", "examples/**", "docs/**", "tools/context_eviden name = "retry_command" test = true +[[example]] +name = "helper_comparison" +test = true + [lints.rust] unsafe_code = "forbid" diff --git a/PACKAGE.md b/PACKAGE.md index b9655b7..e058dbd 100644 --- a/PACKAGE.md +++ b/PACKAGE.md @@ -1,8 +1,6 @@ # edit-session -**Published beta:** `0.1.0-beta.1` is available on -[crates.io](https://crates.io/crates/edit-session/0.1.0-beta.1), verified 1 November 2026 -(Europe/Kyiv). Read the [compatibility boundary](https://github.com/PerchAnton/edit-session/blob/main/docs/COMPATIBILITY.md) before upgrading. +Beta **0.1.0-beta.2** includes the runnable local-helper comparison and the dependency source guide. Read the [compatibility boundary](https://github.com/PerchAnton/edit-session/blob/main/docs/COMPATIBILITY.md) before upgrading. A small, toolkit-independent Rust library for preview, validation and commit lifecycles. **`no_std`, zero runtime dependencies, Rust 1.88+.** MIT OR Apache-2.0. @@ -17,7 +15,7 @@ Add a published copy of this prerelease to your dependencies: ```toml [dependencies] -edit-session = "=0.1.0-beta.1" +edit-session = "=0.1.0-beta.2" ``` This is an experimental beta. Pin an exact version and keep `Cargo.lock` for diff --git a/README.md b/README.md index 8a6887b..9199d9d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # edit-session -**Published beta:** `0.1.0-beta.1` is available on -[crates.io](https://crates.io/crates/edit-session/0.1.0-beta.1), verified 1 November 2026 -(Europe/Kyiv). Read the [compatibility boundary](https://github.com/PerchAnton/edit-session/blob/main/docs/COMPATIBILITY.md) before upgrading. +Beta **0.1.0-beta.2** includes the runnable local-helper comparison and the dependency source guide. Read the [compatibility boundary](https://github.com/PerchAnton/edit-session/blob/main/docs/COMPATIBILITY.md) before upgrading. Keep an editable draft separate from live data, then check that its document, target and revision are still current before the host applies a change. A standalone Rust @@ -54,9 +52,9 @@ development tool is optional. Neither project depends on the other. ## Current prerelease -**[0.1.0-beta.1 on crates.io](https://crates.io/crates/edit-session/0.1.0-beta.1) · published 1 November 2026 (Europe/Kyiv) · Rust 1.88+** +**[0.1.0-beta.2 on crates.io](https://crates.io/crates/edit-session/0.1.0-beta.2) · Rust 1.88+** -**[GitHub prerelease and checksums](https://github.com/PerchAnton/edit-session/releases/tag/v0.1.0-beta.1)** · **[API documentation](https://docs.rs/edit-session/0.1.0-beta.1/edit_session/)** · [Download the crate](https://crates.io/api/v1/crates/edit-session/0.1.0-beta.1/download) · [SHA-256 and installation evidence](docs/evidence/release-beta1.json) · [Changelog](CHANGELOG.md) · [Use as a dependency](#use-from-another-project) +**[GitHub prerelease and checksums](https://github.com/PerchAnton/edit-session/releases/tag/v0.1.0-beta.2)** · **[API documentation](https://docs.rs/edit-session/0.1.0-beta.2/edit_session/)** · [Download the crate](https://crates.io/api/v1/crates/edit-session/0.1.0-beta.2/download) · [Beta.1 installation evidence](docs/evidence/release-beta1.json) · [Changelog](CHANGELOG.md) · [Use as a dependency](#use-from-another-project)
Historical alpha.5 package snapshot and verification @@ -92,7 +90,7 @@ correction, Cancel, preserved Redo and duplicate confirmation. From a checkout of this release: ```sh -git clone --branch v0.1.0-beta.1 --depth 1 https://github.com/PerchAnton/edit-session.git +git clone --branch v0.1.0-beta.2 --depth 1 https://github.com/PerchAnton/edit-session.git cd edit-session cargo run --locked --example retry_command cargo test --locked --example retry_command @@ -123,7 +121,7 @@ dated measurements. Each figure has a source and a defined scope. | Proposal/check loop | **999 pairs; 0 additional clones** | [Instrumented test](tests/lifecycle.rs); one clone at construction, one more on Reset; caller work excluded | | Integration cost | **+217 production UI lines** | [267 added / 50 removed](docs/ADOPTION.md#count-the-cost-as-well); no total-code or speed improvement is claimed | -**Experimental beta, 0.1.0-beta.1.** Maintained by [PerchAnton](https://github.com/PerchAnton). +**Experimental beta, 0.1.0-beta.2.** Maintained by [PerchAnton](https://github.com/PerchAnton). Available on crates.io and as a Git dependency. PhotoCraft and PdfCraft integrations are proposed and await upstream approval. @@ -175,11 +173,11 @@ sequence and shared report help your consumer. The [API contracts](src/lib.rs) a Add this exact prerelease to your crate's `[dependencies]`: ```toml -edit-session = "=0.1.0-beta.1" +edit-session = "=0.1.0-beta.2" ``` -Commit the resulting `Cargo.lock`. For a Git dependency, the same release is pinned -at revision `e3da338fdfa15190f98111cb57f9b3f25c1c2f2f`. +Commit the resulting `Cargo.lock`. For a Git dependency, resolve the release tag +`v0.1.0-beta.2` to its full commit SHA and pin that revision. The companion [interaction-check](https://github.com/PerchAnton/interaction-check) project replays semantic scenarios against adapters using this library. diff --git a/docs/ADOPTION.md b/docs/ADOPTION.md index eedd259..c3998ff 100644 --- a/docs/ADOPTION.md +++ b/docs/ADOPTION.md @@ -101,3 +101,10 @@ had to write and the production lines added or removed. Count tests separately. If both versions pass and the second dialog reuses as much with the local helper, keep the helper. A useful trial result is the pair of diffs and tests, including the cases where the crate added work. + +## Source policy comes before a dependency trial + +See [dependency-source options and the executable local-helper comparison](source-policy.md). +The same eight lifecycle scenarios run against both implementations, including Cancel, +a changed object and edit/Undo with the original value. No library advantage or host +approval follows from equivalent results. diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index aef004d..e27df8a 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -28,7 +28,7 @@ lock. An `Rc>` clone is not an independent snapshot. ## Upgrading and evidence -Pin the exact prerelease (`=0.1.0-beta.1`). Re-run the host's native +Pin the exact prerelease (`=0.1.0-beta.2`). Re-run the host's native regressions before changing the pin. The beta adds no fields to stored documents: sessions are in-memory state, not a serialization format. Error variants are for branching; Display/Debug prose and memory layout are not a machine protocol. diff --git a/docs/source-policy.md b/docs/source-policy.md new file mode 100644 index 0000000..97c4de4 --- /dev/null +++ b/docs/source-policy.md @@ -0,0 +1,64 @@ +# Dependency sources and a local-helper alternative + +Correctness and source approval are separate gates. A commit pin fixes content; +it does not show that the receiving organisation reviewed or accepted that source. + +## PhotoCraft proposal: current obstacle + +At integration head [`df2b3a7`](https://github.com/PerchAnton/photocraft/commit/df2b3a722660d72634fb445ef860b0aca0d4d58b), +[PhotoCraft #9190](https://github.com/storytold/photocraft/pull/9190) still pins +edit-session alpha.1 to an external Git source. The exception has been removed, +but the unchanged **Git dependencies come from storytold** gate rejects that source. +The old standalone interaction workflow is also absent from this head. A previous +successful CI run does not establish that the current dependency is acceptable. + +The [maintainer review](https://github.com/storytold/photocraft/pull/9190#pullrequestreview-5727709694) +requests an organisation-owned source or an explicitly reviewed vendor patch with +provenance. No such source decision has been granted. Do not change the source gate, +hide a dependency with Cargo configuration, or switch to crates.io merely because +the Git-only check would stop detecting it. + +| Option | Concrete preparation | Remaining decision / cost | +| --- | --- | --- | +| Organisation-owned component | Propose the existing small lifecycle to an approved repository; pin the revision after review | Maintainers choose ownership, location and maintenance responsibility; no URL or approval is assumed | +| Reviewed vendor patch | Keep source, licence, exact upstream revision and a numbered patch with a `vendor/README.md` entry | Maintainers must approve the vendor layout and update process; passing a gate alone is insufficient | +| Application-local helper | Use the native lifecycle demonstrated below; remove the library entries from workspace/UI manifests and regenerate the lockfile | No external source for this lifecycle, but the host owns its tests, updates and command integration | + +The local helper is the immediate dependency-free comparison option. These are +preparation options, not an approved change to PhotoCraft. Full host tests, layers, +wasm and unchanged source checks must run on the actual chosen application patch. +The separate interaction-check runner remains a separate CI-policy decision. + +## Reproduce the local-helper comparison + +```sh +cargo test --locked --example helper_comparison +cargo run --locked --example helper_comparison +``` + +[The example](../examples/helper_comparison.rs) exercises both a small in-tree helper +and the existing EditSession API through the same synchronous host. It reuses the +Cancel and context/revision cases; it adds no library API or runtime dependency. + +| Scenario | Required for both implementations | +| --- | --- | +| Cancel after another edit | Preserve the host value and all history counters | +| Same value, different object | Reject the old context; perform no command | +| Same value, different document | Reject the old context; perform no command | +| Edit then Undo to original value | Reject the changed revision; retain Redo | +| Changed value without revision | Reject the changed live value | +| Invalid input after a valid preview | Prevent committing the previous preview | +| Confirm unchanged draft | Preserve Redo and avoid an empty command | +| Confirm valid changed draft | Dispatch exactly one host update | + +Eight tests execute both implementations: sixteen scenario/implementation checks. +This is a teaching model, not a rerun of PhotoCraft's UI integration. Host identity, +monotonic revision, permissions, parsing, dialog lifetime and serialized command +execution remain necessary in either option. The helper intentionally implements +only this example's lifecycle; it is not a replacement for every library method. + +Equivalent results establish no code-size or performance advantage for the crate. +If a small local helper meets the host's needs and source policy, retaining it is a +valid outcome. Consider the library only if reuse across real dialogs offsets its +approval and maintenance cost. The existing [Cancel/retry example](../examples/retry_command.rs) +and [Undo/context walkthrough](../examples/context_walkthrough.rs) remain available. diff --git a/examples/helper_comparison.rs b/examples/helper_comparison.rs new file mode 100644 index 0000000..d34878e --- /dev/null +++ b/examples/helper_comparison.rs @@ -0,0 +1,276 @@ +//! Standalone synchronous teaching model. No upstream bug or adoption claim. +//! Run with cargo test --locked --example helper_comparison. +use edit_session::{EditSession, FinishError}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Context { + document: u64, + object: u64, + revision: u64, +} + +// BEGIN local lifecycle: count separately from common host and tests. +struct LocalDraft { + original: i32, + preview: i32, + context: Context, + valid: bool, +} +impl LocalDraft { + fn new(value: i32, context: Context) -> Self { + Self { + original: value, + preview: value, + context, + valid: true, + } + } + fn propose(&mut self, value: Result) { + self.valid = value.is_ok(); + if let Ok(value) = value { + self.preview = value; + } + } + fn finish(self, live: i32, context: Context) -> Result, &'static str> { + if context != self.context { + return Err("context changed"); + } + if live != self.original { + return Err("value changed"); + } + if !self.valid { + return Err("invalid proposal"); + } + Ok((self.preview != self.original).then_some(self.preview)) + } + fn cancel(self) {} +} +// END local lifecycle. + +// BEGIN library adapter: same application-facing result, no local lifecycle. +struct LibraryDraft(EditSession); +impl LibraryDraft { + fn new(value: i32, context: Context) -> Self { + Self(EditSession::new(value, context)) + } + fn propose(&mut self, value: Result) { + let _ = self.0.propose(value); + } + fn finish(self, live: i32, context: Context) -> Result, &'static str> { + self.0 + .finish(&live, &context) + .map(|v| v.map(|c| c.after)) + .map_err(|e| match e { + FinishError::ContextChanged => "context changed", + FinishError::ValueChanged => "value changed", + FinishError::InvalidProposal => "invalid proposal", + }) + } + fn cancel(self) { + self.0.cancel(); + } +} +// END library adapter. + +// Shared host/test plumbing is required in either implementation. +enum Draft { + Local(LocalDraft), + Library(LibraryDraft), +} +impl Draft { + fn open(library: bool, host: &Host) -> Self { + if library { + Self::Library(LibraryDraft::new(host.value, host.context)) + } else { + Self::Local(LocalDraft::new(host.value, host.context)) + } + } + fn propose(&mut self, value: Result) { + match self { + Self::Local(d) => d.propose(value), + Self::Library(d) => d.propose(value), + } + } + fn finish(self, host: &mut Host) -> Result<(), &'static str> { + // No await, callback or intervening mutation between comparison and write. + let proposal = match self { + Self::Local(d) => d.finish(host.value, host.context), + Self::Library(d) => d.finish(host.value, host.context), + }?; + if let Some(value) = proposal { + host.apply(value); + } + Ok(()) + } + fn cancel(self, _host: &mut Host) { + match self { + Self::Local(d) => d.cancel(), + Self::Library(d) => d.cancel(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Host { + value: i32, + context: Context, + commands: usize, + redo: usize, +} +impl Host { + fn new() -> Self { + Self { + value: 100, + context: Context { + document: 7, + object: 2, + revision: 42, + }, + commands: 0, + redo: 1, + } + } + fn apply(&mut self, value: i32) { + self.value = value; + self.context.revision += 1; // Bounded synthetic sequence; production uses checked arithmetic. + self.commands += 1; + self.redo = 0; + } +} + +fn main() { + for library in [false, true] { + let label = if library { + "edit-session (current checkout)" + } else { + "local helper" + }; + let mut host = Host::new(); + let mut draft = Draft::open(library, &host); + draft.propose(Ok(75)); + host.apply(120); + draft.cancel(&mut host); + println!( + "{label}: Cancel after external edit -> live {}, revision {}, commands {}", + host.value, host.context.revision, host.commands + ); + let mut draft = Draft::open(library, &host); + draft.propose(Ok(90)); + host.context.object = 3; + println!( + "{label}: same value, different object -> {:?}", + draft.finish(&mut host) + ); + let mut draft = Draft::open(library, &host); + draft.propose(Ok(90)); + host.apply(150); + host.apply(120); + println!("{label}: edit then Undo -> {:?}", draft.finish(&mut host)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn each(test: impl Fn(bool)) { + for library in [false, true] { + test(library); + } + } + #[test] + fn cancel_preserves_external_edit_and_history() { + each(|library| { + let mut h = Host::new(); + let mut d = Draft::open(library, &h); + d.propose(Ok(75)); + h.apply(120); + let before = h.clone(); + d.cancel(&mut h); + assert_eq!(h, before); + }); + } + #[test] + fn same_value_different_object_is_rejected() { + each(|library| { + let mut h = Host::new(); + let mut d = Draft::open(library, &h); + d.propose(Ok(75)); + h.context.object = 3; + let before = h.clone(); + assert_eq!(d.finish(&mut h), Err("context changed")); + assert_eq!(h, before); + }); + } + #[test] + fn same_value_different_document_is_rejected() { + each(|library| { + let mut h = Host::new(); + let mut d = Draft::open(library, &h); + d.propose(Ok(75)); + h.context.document = 8; + let before = h.clone(); + assert_eq!(d.finish(&mut h), Err("context changed")); + assert_eq!(h, before); + }); + } + #[test] + fn edit_then_undo_is_still_stale() { + each(|library| { + let mut h = Host::new(); + let mut d = Draft::open(library, &h); + d.propose(Ok(75)); + h.apply(120); + h.apply(100); + let before = h.clone(); + assert_eq!(d.finish(&mut h), Err("context changed")); + assert_eq!(h, before); + }); + } + #[test] + fn changed_value_without_revision_is_rejected() { + each(|library| { + let mut h = Host::new(); + let mut d = Draft::open(library, &h); + d.propose(Ok(75)); + h.value = 120; + let before = h.clone(); + assert_eq!(d.finish(&mut h), Err("value changed")); + assert_eq!(h, before); + }); + } + #[test] + fn invalid_input_cannot_commit_previous_valid_preview() { + each(|library| { + let mut h = Host::new(); + let mut d = Draft::open(library, &h); + d.propose(Ok(75)); + d.propose(Err("not a number")); + let before = h.clone(); + assert_eq!(d.finish(&mut h), Err("invalid proposal")); + assert_eq!(h, before); + }); + } + #[test] + fn no_op_preserves_redo() { + each(|library| { + let mut h = Host::new(); + let d = Draft::open(library, &h); + let before = h.clone(); + assert_eq!(d.finish(&mut h), Ok(())); + assert_eq!(h, before); + }); + } + #[test] + fn fresh_draft_commits_once() { + each(|library| { + let mut h = Host::new(); + let mut d = Draft::open(library, &h); + d.propose(Ok(75)); + assert_eq!(d.finish(&mut h), Ok(())); + assert_eq!( + (h.value, h.context.revision, h.commands, h.redo), + (75, 43, 1, 0) + ); + }); + } +}