diff --git a/TREE.md b/TREE.md index 0e5d8f1..0fb3828 100644 --- a/TREE.md +++ b/TREE.md @@ -370,11 +370,14 @@ research/experiments/boundary-recovery/routing-transform.mjs research/experiments/boundary-recovery/routing.mjs research/experiments/feature-release/README.md research/experiments/feature-release/account-a1.js +research/experiments/feature-release/conflicts-reload-results.json +research/experiments/feature-release/conflicts-reload.mjs research/experiments/feature-release/experiment.mjs research/experiments/feature-release/host.js research/experiments/feature-release/package-lock.json research/experiments/feature-release/package.json research/experiments/feature-release/provenance.json +research/experiments/feature-release/reload-host.js research/experiments/feature-release/reports-r1.js research/experiments/feature-release/reports-r2.js research/experiments/feature-release/reports-r3.js @@ -390,6 +393,7 @@ research/plan/OpenQuestions.md research/plan/ResearchProgram.md research/plan/ResearchProtocol.md research/plan/ValidationCases.md +research/reviews/ActivationConflictsAndReload.md research/reviews/AuthorityPilot.md research/reviews/BoundaryRecoveryExperiment.md research/reviews/CarniatoAdmissionRegression.json diff --git a/research/README.md b/research/README.md index ee13b77..b562a6f 100644 --- a/research/README.md +++ b/research/README.md @@ -69,3 +69,7 @@ hypotheses and four synthetic walkthroughs. No deployment independence was runti Latest execution: [feature release lifecycle](reviews/FeatureReleaseExperiment.md) records 14/14 expected observations across two browsers. It distinguishes fresh, already-loaded and delayed-load sessions, and includes negative failure controls. + +Latest release follow-up: [activation conflicts and unfinished-work reload](reviews/ActivationConflictsAndReload.md) +records 21/21 expected observations, including blind-update and unguarded-reload loss +controls. Original experiments retained; production atomicity/unload limits remain open. diff --git a/research/candidates/IndependentFeatureBoundary.md b/research/candidates/IndependentFeatureBoundary.md index 753d5f0..0a0ca79 100644 --- a/research/candidates/IndependentFeatureBoundary.md +++ b/research/candidates/IndependentFeatureBoundary.md @@ -59,3 +59,16 @@ binding; deleting the old artifact can break its delayed first load. A post-impo API check runs after evaluation and is not a security boundary. A shared startup failure can block otherwise separate features. These are bounded mechanism observations, not execution of the source examples or proof of full-pattern production independence. + +## Activation conflict and reload policy — 2026-09-26 + +[Follow-up tests](../reviews/ActivationConflictsAndReload.md) demonstrate lost updates +under blind binding replacement and explicit conflict rejection/refetch for disjoint +intent. A same-feature stale rollback is not automatically reapplied. Atomicity is +verified only in the single-process fixture, not a distributed deployment service. + +For unfinished work, distinguish defer, preserve-and-reload and authorized explicit +discard. Block unsafe progress on storage refusal or unresolved operation outcome; +read the latest draft after asynchronous checks. Revalidate stored schema in the new +session because activation can change after preflight. The tested per-tab preservation +is not native-unload/crash protection or a substitute for retention policy. diff --git a/research/candidates/LocalDraftState.md b/research/candidates/LocalDraftState.md index fccebfd..7ab1f1c 100644 --- a/research/candidates/LocalDraftState.md +++ b/research/candidates/LocalDraftState.md @@ -42,3 +42,12 @@ partial known uses of unsent-intent separation, not confirmation of the entire base/draft/current merge contract. Full-pattern recurrence remains unknown. A composer may hand submitted content to a separate pending record. Preserve the recoverable intent across that handoff; do not require the input box to stay filled. + +## Executed application-update path — 2026-09-26 + +[Reload follow-up](../reviews/ActivationConflictsAndReload.md) recovers text and a base +marker across a same-tab reload, supports declining an update, and refuses progress +on injected storage failure or schema mismatch. It reads edits made during asynchronous +preflight and rechecks schema after navigation. Explicit discard clears stale stored +content too. These are synthetic policy mechanisms, not permission to persist restricted +data or guarantees for native unload, device failure, multiple tabs or remote outcomes. diff --git a/research/catalog/SourceCatalog.md b/research/catalog/SourceCatalog.md index 1bfbe23..4ef2941 100644 --- a/research/catalog/SourceCatalog.md +++ b/research/catalog/SourceCatalog.md @@ -159,3 +159,8 @@ pinned source examples; no deployed release workflow was executed. SRC-0037: [MDN dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import#module_namespace_object), Module namespace object; living documentation accessed 2026-09-26, exact revision unknown. Context for evaluated module reuse versus HTTP caching. [Review](../reviews/FeatureReleaseExperiment.md). + +SRC-0036 follow-up: [activation conflicts and update reload](../reviews/ActivationConflictsAndReload.md) +adds an authored conditional-write server and reload policy with sessionStorage. +Three HTTP cases plus nine cases per browser, 21/21 expected observations; negative +controls included. Same synthetic evidence family, not an independently deployed use. diff --git a/research/experiments/feature-release/README.md b/research/experiments/feature-release/README.md index 7cd5e66..e88a790 100644 --- a/research/experiments/feature-release/README.md +++ b/research/experiments/feature-release/README.md @@ -48,3 +48,30 @@ can already have side effects before that check. No backend writes, authenticati API migrations, atomic/concurrent map updates, accessibility audit or safe reload of unsaved work is modeled. The startup outage reflects this fixture's chosen dependency, not an assertion that every independently released frontend must fail that way. + +## Activation conflicts and safe update reload + +[Review](../../reviews/ActivationConflictsAndReload.md) and +[receipt](conflicts-reload-results.json): three deterministic stale-writer HTTP cases +and nine reload scenarios in each browser, 21/21 expected observations. + +```bash +PLAYWRIGHT_BROWSERS_PATH=/tmp/fep-playwright-browsers FEP_CHROMIUM_PATH=/path/to/chrome node conflicts-reload.mjs +``` + +Uses the same pinned dependencies/browsers as the original release experiment. +Default output `/tmp/fep-conflicts-reload.json`; `FEP_RESULT_PATH` overrides it. +Incomplete browser progress goes to `/tmp/fep-conflicts-reload-checkpoint.json`. +The earlier host, runner and receipts remain unchanged. The new reload-host.js is +an authored policy using sessionStorage, explicit user choices and schema checks. + +C01 and R01 deliberately reproduce data loss from unsafe choices. C02 refetches and +reapplies a disjoint intent; C03 rejects a conflicting stale rollback. R02–R09 exercise +update deferral, preservation, storage refusal, incompatible schema, explicit discard, +unknown-outcome gating, edits during preflight and activation after preflight. + +Activation comparison/replacement is atomic only within this single-process model; +its revision header and 409 response are not a full HTTP precondition implementation. +Storage refusal is injected. Preservation assumes retention is authorized and protects +only the tested same-tab reload. Native reload/tab close, crashes, real backend +outcome recovery, multiple tabs and automatic schema migration remain out of scope. diff --git a/research/experiments/feature-release/conflicts-reload-results.json b/research/experiments/feature-release/conflicts-reload-results.json new file mode 100644 index 0000000..67478a5 --- /dev/null +++ b/research/experiments/feature-release/conflicts-reload-results.json @@ -0,0 +1,1099 @@ +{ + "date": "2026-09-26T21:28:01.965Z", + "node": "v24.11.1", + "files": { + "reload-host.js": "34fafe371e68ce4ac069b7dcef0a7899c7f2a238de87ddaa549734227b4e59a3", + "reports-r1.js": "f2a39800e5dad7ba9a2b338fb62a4353094fc2156640d8674be7a15143e71206", + "reports-r2.js": "17de3351f9b4be3d2d0b517e11486cc75f6c9e4dd50b994d0a6eb7b9b5b44927", + "conflicts-reload.mjs": "c84cd1c2b2864153d213d7d5829e4bd7dc05407418c6b39c2153b554e4d9448a", + "package.json": "6986c09f8fee8c8bd7005dafb1c40e09538438127ae9602fa6b0686fab4b755b", + "package-lock.json": "2e6bf0caeea9d291d83f9008ceedefc1cf71a4e0fef2ab3b01f49edbee597af8" + }, + "results": [ + { + "id": "C01-blind-update-loses-neighbor", + "kind": "HTTP-control-plane", + "status": "pass", + "observation": { + "lostReportsUpdate": true + }, + "trace": [ + { + "event": "read", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/unsafe", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/unsafe", + "state": { + "revision": 3, + "reports": "/reports-r1.js", + "account": "A2", + "draftSchema": 1 + } + } + ] + }, + { + "id": "C02-stale-disjoint-update-refetch", + "kind": "HTTP-control-plane", + "status": "pass", + "observation": { + "bothUpdatesRetained": true, + "explicitRefetchAndReapply": true + }, + "trace": [ + { + "event": "read", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "conflict", + "expected": "1", + "actual": 2 + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 3, + "reports": "/reports-r2.js", + "account": "A2", + "draftSchema": 1 + } + } + ] + }, + { + "id": "C03-same-feature-needs-decision", + "kind": "HTTP-control-plane", + "status": "pass", + "observation": { + "staleRollbackRejected": true, + "noAutomaticOverwrite": true + }, + "trace": [ + { + "event": "read", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "conflict", + "expected": "1", + "actual": 2 + } + ] + }, + { + "id": "R01-unguarded-reload-loses-draft", + "engine": "chromium", + "browser": "151.0.7922.34", + "status": "pass", + "observation": { + "draftLost": true, + "negativeControl": true + }, + "snapshot": { + "draft": "", + "status": "Ready", + "version": "R2", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "document", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R02-defer-keeps-work", + "engine": "chromium", + "browser": "151.0.7922.34", + "status": "pass", + "observation": { + "declinedReload": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Update deferred", + "version": "R1", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R03-preserve-and-restore", + "engine": "chromium", + "browser": "151.0.7922.34", + "status": "pass", + "observation": { + "restored": true, + "basePreserved": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Draft restored", + "version": "R2", + "readOnly": false, + "saved": "{\"schema\":1,\"text\":\"unfinished proposal\",\"base\":\"record-v1\"}" + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "document", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R04-storage-failure-blocks", + "engine": "chromium", + "browser": "151.0.7922.34", + "status": "pass", + "observation": { + "reloadBlocked": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Update deferred: draft storage failed", + "version": "R1", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R05-schema-mismatch-blocks", + "engine": "chromium", + "browser": "151.0.7922.34", + "status": "pass", + "observation": { + "reloadBlocked": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Update deferred: incompatible draft schema", + "version": "R1", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 3, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 2 + } + }, + { + "event": "read", + "snapshot": { + "revision": 3, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 2 + } + } + ] + }, + { + "id": "R06-explicit-discard", + "engine": "chromium", + "browser": "151.0.7922.34", + "status": "pass", + "observation": { + "explicitlyDiscarded": true + }, + "snapshot": { + "draft": "", + "status": "Ready", + "version": "R2", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "document", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R07-unknown-write-blocks", + "engine": "chromium", + "browser": "151.0.7922.34", + "status": "pass", + "observation": { + "uncertainOutcomeNotDiscarded": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Resolve unknown operation outcome first", + "version": "R1", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R08-edit-during-preflight", + "engine": "chromium", + "browser": "151.0.7922.34", + "status": "pass", + "observation": { + "latestEditPreserved": true + }, + "snapshot": { + "draft": "newer proposal while checking", + "status": "Draft restored", + "version": "R2", + "readOnly": false, + "saved": "{\"schema\":1,\"text\":\"newer proposal while checking\",\"base\":\"record-v1\"}" + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "document", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R09-activation-changes-after-check", + "engine": "chromium", + "browser": "151.0.7922.34", + "status": "pass", + "observation": { + "startupRevalidation": true, + "draftRetainedButNotEditable": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Recovery required: draft schema mismatch", + "version": "R2", + "readOnly": true, + "saved": "{\"schema\":1,\"text\":\"unfinished proposal\",\"base\":\"record-v1\"}" + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 3, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 2 + } + }, + { + "event": "document", + "snapshot": { + "revision": 3, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 2 + } + } + ] + }, + { + "id": "R01-unguarded-reload-loses-draft", + "engine": "firefox", + "browser": "155.0", + "status": "pass", + "observation": { + "draftLost": true, + "negativeControl": true + }, + "snapshot": { + "draft": "", + "status": "Ready", + "version": "R2", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "document", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R02-defer-keeps-work", + "engine": "firefox", + "browser": "155.0", + "status": "pass", + "observation": { + "declinedReload": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Update deferred", + "version": "R1", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R03-preserve-and-restore", + "engine": "firefox", + "browser": "155.0", + "status": "pass", + "observation": { + "restored": true, + "basePreserved": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Draft restored", + "version": "R2", + "readOnly": false, + "saved": "{\"schema\":1,\"text\":\"unfinished proposal\",\"base\":\"record-v1\"}" + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "document", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R04-storage-failure-blocks", + "engine": "firefox", + "browser": "155.0", + "status": "pass", + "observation": { + "reloadBlocked": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Update deferred: draft storage failed", + "version": "R1", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R05-schema-mismatch-blocks", + "engine": "firefox", + "browser": "155.0", + "status": "pass", + "observation": { + "reloadBlocked": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Update deferred: incompatible draft schema", + "version": "R1", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 3, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 2 + } + }, + { + "event": "read", + "snapshot": { + "revision": 3, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 2 + } + } + ] + }, + { + "id": "R06-explicit-discard", + "engine": "firefox", + "browser": "155.0", + "status": "pass", + "observation": { + "explicitlyDiscarded": true + }, + "snapshot": { + "draft": "", + "status": "Ready", + "version": "R2", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "document", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R07-unknown-write-blocks", + "engine": "firefox", + "browser": "155.0", + "status": "pass", + "observation": { + "uncertainOutcomeNotDiscarded": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Resolve unknown operation outcome first", + "version": "R1", + "readOnly": false, + "saved": null + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R08-edit-during-preflight", + "engine": "firefox", + "browser": "155.0", + "status": "pass", + "observation": { + "latestEditPreserved": true + }, + "snapshot": { + "draft": "newer proposal while checking", + "status": "Draft restored", + "version": "R2", + "readOnly": false, + "saved": "{\"schema\":1,\"text\":\"newer proposal while checking\",\"base\":\"record-v1\"}" + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "document", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + } + ] + }, + { + "id": "R09-activation-changes-after-check", + "engine": "firefox", + "browser": "155.0", + "status": "pass", + "observation": { + "startupRevalidation": true, + "draftRetainedButNotEditable": true + }, + "snapshot": { + "draft": "unfinished proposal", + "status": "Recovery required: draft schema mismatch", + "version": "R2", + "readOnly": true, + "saved": "{\"schema\":1,\"text\":\"unfinished proposal\",\"base\":\"record-v1\"}" + }, + "errors": [], + "trace": [ + { + "event": "document", + "snapshot": { + "revision": 1, + "reports": "/reports-r1.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "read", + "snapshot": { + "revision": 2, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 1 + } + }, + { + "event": "commit", + "path": "/activation", + "state": { + "revision": 3, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 2 + } + }, + { + "event": "document", + "snapshot": { + "revision": 3, + "reports": "/reports-r2.js", + "account": "A1", + "draftSchema": 2 + } + } + ] + } + ], + "passed": 21, + "total": 21 +} diff --git a/research/experiments/feature-release/conflicts-reload.mjs b/research/experiments/feature-release/conflicts-reload.mjs new file mode 100644 index 0000000..32c5ba6 --- /dev/null +++ b/research/experiments/feature-release/conflicts-reload.mjs @@ -0,0 +1,63 @@ +import {chromium,firefox} from 'playwright'; +import {createServer} from 'node:http'; +import {once} from 'node:events'; +import {readFile,writeFile} from 'node:fs/promises'; +import {fileURLToPath} from 'node:url'; +import {createHash} from 'node:crypto'; +import assert from 'node:assert/strict'; +const dir=fileURLToPath(new URL('.',import.meta.url)); +const names=['reload-host.js','reports-r1.js','reports-r2.js']; +const assets=new Map(await Promise.all(names.map(async n=>['/'+n,await readFile(dir+n)]))); +let state,trace,held,holdNext; +function reset(){state={revision:1,reports:'/reports-r1.js',account:'A1',draftSchema:1};trace=[];held=null;holdNext=false;} +reset(); +const copy=()=>({...state}); +const json=(res,code,value)=>{res.writeHead(code,{'Content-Type':'application/json','Cache-Control':'no-store'});res.end(JSON.stringify(value));}; +const server=createServer(async(req,res)=>{ + const path=new URL(req.url,'http://local').pathname;res.setHeader('Cache-Control','no-store'); + if(path==='/activation'&&req.method==='GET'){ + const snapshot=copy();trace.push({event:'read',snapshot}); + if(holdNext){holdNext=false;held=()=>json(res,200,snapshot);}else json(res,200,snapshot); + }else if((path==='/activation'||path==='/unsafe')&&req.method==='PUT'){ + let body='';for await(const chunk of req)body+=chunk;const next=JSON.parse(body); + // Single-process model: no await between revision comparison and replacement. + if(path==='/activation'&&req.headers['if-match']!==String(state.revision)){ + trace.push({event:'conflict',expected:req.headers['if-match'],actual:state.revision});json(res,409,copy());return; + } + state={...next,revision:state.revision+1};trace.push({event:'commit',path,state:copy()});json(res,200,copy()); + }else if(path==='/'){ + const snapshot=copy();trace.push({event:'document',snapshot});res.setHeader('Content-Type','text/html');res.end(`