From f273a190f0e08b8cfe0bb7260d0e7d889f1c090a Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 26 Sep 2026 23:30:15 +0200 Subject: [PATCH] Test activation conflicts and draft-preserving application reload Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9 --- TREE.md | 4 + research/README.md | 4 + .../candidates/IndependentFeatureBoundary.md | 13 + research/candidates/LocalDraftState.md | 9 + research/catalog/SourceCatalog.md | 5 + .../experiments/feature-release/README.md | 27 + .../conflicts-reload-results.json | 1099 +++++++++++++++++ .../feature-release/conflicts-reload.mjs | 63 + .../feature-release/reload-host.js | 44 + research/plan/OpenQuestions.md | 5 + .../reviews/ActivationConflictsAndReload.md | 84 ++ .../walkthroughs/VC06-MicrofrontendEstate.md | 11 + workplans/FEP-WP-0003-contrasting-schools.md | 17 + 13 files changed, 1385 insertions(+) create mode 100644 research/experiments/feature-release/conflicts-reload-results.json create mode 100644 research/experiments/feature-release/conflicts-reload.mjs create mode 100644 research/experiments/feature-release/reload-host.js create mode 100644 research/reviews/ActivationConflictsAndReload.md 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(`Safe reload probe

`); + }else if(assets.has(path)){res.setHeader('Content-Type','application/javascript');res.end(assets.get(path));} + else{res.writeHead(404);res.end('missing');} +}); +server.listen(0,'127.0.0.1');await once(server,'listening');const url=`http://127.0.0.1:${server.address().port}`; +const results=[]; +async function put(value,rev,unsafe=false){const r=await fetch(url+(unsafe?'/unsafe':'/activation'),{method:'PUT',headers:{'Content-Type':'application/json','If-Match':String(rev)},body:JSON.stringify(value)});return {status:r.status,body:await r.json()};} +const get=()=>fetch(url+'/activation').then(r=>r.json()); +async function control(id,run){reset();const r={id,kind:'HTTP-control-plane',status:'pass'};try{r.observation=await run();}catch(e){r.status='fail';r.error=e.message;}r.trace=[...trace];results.push(r);} +try{ + await control('C01-blind-update-loses-neighbor',async()=>{const [a,b]=await Promise.all([get(),get()]);await put({...a,reports:'/reports-r2.js'},a.revision,true);await put({...b,account:'A2'},b.revision,true);assert.equal(state.reports,'/reports-r1.js');assert.equal(state.account,'A2');return {lostReportsUpdate:true};}); + await control('C02-stale-disjoint-update-refetch',async()=>{const [a,b]=await Promise.all([get(),get()]);assert.equal((await put({...a,reports:'/reports-r2.js'},a.revision)).status,200);assert.equal((await put({...b,account:'A2'},b.revision)).status,409);const current=await get();assert.equal((await put({...current,account:'A2'},current.revision)).status,200);assert.equal(state.reports,'/reports-r2.js');assert.equal(state.account,'A2');return {bothUpdatesRetained:true,explicitRefetchAndReapply:true};}); + await control('C03-same-feature-needs-decision',async()=>{const [a,b]=await Promise.all([get(),get()]);await put({...a,reports:'/reports-r2.js'},a.revision);const conflict=await put({...b,reports:'/reports-r1.js'},b.revision);assert.equal(conflict.status,409);assert.equal(state.reports,'/reports-r2.js');return {staleRollbackRejected:true,noAutomaticOverwrite:true};}); + for(const[engine,launcher]of [['chromium',chromium],['firefox',firefox]]){ + const browser=await launcher.launch({headless:true,...(engine==='chromium'&&process.env.FEP_CHROMIUM_PATH?{executablePath:process.env.FEP_CHROMIUM_PATH}:{})}); + try{ + async function scenario(id,run){reset();const p=await browser.newPage();p.setDefaultTimeout(2500);const errors=[];p.on('pageerror',e=>errors.push(e.message));const r={id,engine,browser:browser.version(),status:'pass'}; + try{await p.goto(url);await p.waitForFunction(()=>window.ready);await p.locator('#draft').fill('unfinished proposal');await put({...state,reports:'/reports-r2.js'},state.revision);r.observation=await run(p);assert.deepEqual(errors,[]);}catch(e){r.status='fail';r.error=e.message;} + r.snapshot=await p.evaluate(()=>({draft:document.getElementById('draft').value,status:document.getElementById('status').textContent,version:document.getElementById('version').textContent,readOnly:document.getElementById('draft').readOnly,saved:sessionStorage.getItem('fep-review-draft')}));r.errors=errors;r.trace=[...trace];results.push(r);if(held){held();held=null;}await p.close();await writeFile('/tmp/fep-conflicts-reload-checkpoint.json',JSON.stringify({status:'incomplete',results},null,2)+'\n');} + async function reload(p,button){await Promise.all([p.waitForEvent('load'),p.locator(button).click()]);await p.waitForFunction(()=>window.ready);} + const draft= p=>p.locator('#draft').inputValue(); + await scenario('R01-unguarded-reload-loses-draft',async p=>{await p.reload();await p.waitForFunction(()=>window.ready);assert.equal(await draft(p),'');assert.equal(await p.locator('#version').textContent(),'R2');return {draftLost:true,negativeControl:true};}); + await scenario('R02-defer-keeps-work',async p=>{await p.locator('#stay').click();assert.equal(await draft(p),'unfinished proposal');assert.equal(await p.locator('#version').textContent(),'R1');return {declinedReload:true};}); + await scenario('R03-preserve-and-restore',async p=>{await reload(p,'#preserve');assert.equal(await draft(p),'unfinished proposal');assert.equal(await p.locator('#version').textContent(),'R2');const s=await p.evaluate(()=>JSON.parse(sessionStorage.getItem('fep-review-draft')));assert.equal(s.base,'record-v1');return {restored:true,basePreserved:true};}); + await scenario('R04-storage-failure-blocks',async p=>{await p.evaluate(()=>window.failStorage=true);await p.locator('#preserve').click();await p.locator('#status').filter({hasText:'storage failed'}).waitFor();assert.equal(await draft(p),'unfinished proposal');assert.equal(await p.locator('#version').textContent(),'R1');return {reloadBlocked:true};}); + await scenario('R05-schema-mismatch-blocks',async p=>{await put({...state,draftSchema:2},state.revision);await p.locator('#preserve').click();await p.locator('#status').filter({hasText:'incompatible draft schema'}).waitFor();assert.equal(await draft(p),'unfinished proposal');assert.equal(await p.locator('#version').textContent(),'R1');return {reloadBlocked:true};}); + await scenario('R06-explicit-discard',async p=>{await p.evaluate(()=>sessionStorage.setItem('fep-review-draft',JSON.stringify({schema:1,text:'older saved draft',base:'record-v1'})));await reload(p,'#discard');assert.equal(await draft(p),'');assert.equal(await p.evaluate(()=>sessionStorage.getItem('fep-review-draft')),null);assert.equal(await p.locator('#version').textContent(),'R2');return {explicitlyDiscarded:true};}); + await scenario('R07-unknown-write-blocks',async p=>{await p.evaluate(()=>window.pendingUnknown=true);await p.locator('#preserve').click();await p.locator('#status').filter({hasText:'unknown operation'}).waitFor();assert.equal(await draft(p),'unfinished proposal');assert.equal(await p.locator('#version').textContent(),'R1');return {uncertainOutcomeNotDiscarded:true};}); + await scenario('R08-edit-during-preflight',async p=>{holdNext=true;await p.locator('#preserve').click();await untilHeld();await p.locator('#draft').fill('newer proposal while checking');const release=held;held=null;const done=p.waitForEvent('load');release();await done;await p.waitForFunction(()=>window.ready);assert.equal(await draft(p),'newer proposal while checking');return {latestEditPreserved:true};}); + await scenario('R09-activation-changes-after-check',async p=>{holdNext=true;await p.locator('#preserve').click();await untilHeld();await put({...state,draftSchema:2},state.revision);const release=held;held=null;const done=p.waitForEvent('load');release();await done;await p.waitForFunction(()=>window.ready);assert.equal(await draft(p),'unfinished proposal');assert.equal(await p.locator('#draft').getAttribute('readonly'),'');await p.locator('#status').filter({hasText:'Recovery required'}).waitFor();return {startupRevalidation:true,draftRetainedButNotEditable:true};}); + }finally{await browser.close();} + } +}finally{if(held)held();await new Promise(resolve=>server.close(resolve));} +async function untilHeld(){for(let i=0;i<100;i++){if(held)return;await new Promise(r=>setTimeout(r,10));}throw new Error('preflight barrier not reached');} +const files={};for(const f of [...names,'conflicts-reload.mjs','package.json','package-lock.json'])files[f]=createHash('sha256').update(await readFile(dir+f)).digest('hex');const output=process.env.FEP_RESULT_PATH||'/tmp/fep-conflicts-reload.json';const result={date:new Date().toISOString(),node:process.version,files,results,passed:results.filter(x=>x.status==='pass').length,total:results.length};await writeFile(output,JSON.stringify(result,null,2)+'\n');console.log(JSON.stringify({output,passed:result.passed,total:result.total,failures:results.filter(x=>x.status==='fail')},null,2));if(result.passed!==result.total)process.exitCode=1; diff --git a/research/experiments/feature-release/reload-host.js b/research/experiments/feature-release/reload-host.js new file mode 100644 index 0000000..bee47f9 --- /dev/null +++ b/research/experiments/feature-release/reload-host.js @@ -0,0 +1,44 @@ +const input = document.getElementById('draft'); +const status = document.getElementById('status'); +const key = 'fep-review-draft'; +const startup = JSON.parse(document.getElementById('startup').textContent); +window.observed = {startup, outcomes: [], evaluations: [], restored: false}; +window.pendingUnknown = false; +window.failStorage = false; +let blocked = false; +const saved = sessionStorage.getItem(key); +if (saved) { + const record = JSON.parse(saved); + input.value = record.text; + if (record.schema !== startup.draftSchema) { + blocked = true; input.readOnly = true; status.textContent = 'Recovery required: draft schema mismatch'; + } else {window.observed.restored = true; status.textContent = 'Draft restored';} +} +if (!saved) status.textContent = 'Ready'; +window.feature = await import('reports'); +document.getElementById('version').textContent = window.feature.version; +window.ready = true; +window.requestReload = async choice => { + if (choice === 'stay') {status.textContent = 'Update deferred'; return false;} + if (blocked) {status.textContent = 'Recovery required: draft schema mismatch'; return false;} + if (window.pendingUnknown) {status.textContent = 'Resolve unknown operation outcome first'; return false;} + const latest = await fetch('/activation').then(r => r.json()); + // Recheck after asynchronous work; edits made while checking must not be replaced by an old snapshot. + if (window.pendingUnknown) {status.textContent = 'Resolve unknown operation outcome first'; return false;} + if (choice !== 'discard' && latest.draftSchema !== 1) { + status.textContent = 'Update deferred: incompatible draft schema'; return false; + } + try { + if (window.failStorage) throw new Error('injected storage refusal'); + if (choice === 'discard') sessionStorage.removeItem(key); + else { + const record = {schema: 1, text: input.value, base: 'record-v1'}; + sessionStorage.setItem(key, JSON.stringify(record)); + if (sessionStorage.getItem(key) !== JSON.stringify(record)) throw new Error('write verification failed'); + } + } catch {status.textContent = 'Update deferred: draft storage failed'; return false;} + location.reload();return true; +}; +document.getElementById('stay').onclick = () => window.requestReload('stay'); +document.getElementById('preserve').onclick = () => window.requestReload('preserve'); +document.getElementById('discard').onclick = () => window.requestReload('discard'); diff --git a/research/plan/OpenQuestions.md b/research/plan/OpenQuestions.md index 045c658..cb15472 100644 --- a/research/plan/OpenQuestions.md +++ b/research/plan/OpenQuestions.md @@ -109,3 +109,8 @@ distinguishes artifact retention, document binding and module-instance lifetime. Old delayed loads can fail after artifact retirement even while already-loaded old sessions work. Deployment independence remains conditional on a session-support contract; no general answer or canonical promotion follows from this fixture. + +Q06 activation/reload follow-up: [conditional update and draft tests](../reviews/ActivationConflictsAndReload.md) +show why disjoint-intent retry differs from competing rollback, and why preflight +compatibility must be rechecked after reload. Same-tab draft preservation is narrower +than durability or remote-outcome recovery. No question or maturity gate is closed. diff --git a/research/reviews/ActivationConflictsAndReload.md b/research/reviews/ActivationConflictsAndReload.md new file mode 100644 index 0000000..8fcb42c --- /dev/null +++ b/research/reviews/ActivationConflictsAndReload.md @@ -0,0 +1,84 @@ +# Activation conflicts and unfinished-work reload — 2026-09-26 + +Status: executed synthetic HTTP/browser experiment, editorial analysis by Codex. +WP-0003 T01, continuation of [feature release lifecycle](FeatureReleaseExperiment.md). +[Runner](../experiments/feature-release/conflicts-reload.mjs), +[reload host](../experiments/feature-release/reload-host.js) and +[receipt](../experiments/feature-release/conflicts-reload-results.json) record 21/21 +expected observations: three HTTP update cases plus nine reload cases in Chromium +151.0.7922.34 and Firefox 155.0. No live deployment, backend domain write or model call. + +## Activation-update cases + +Two clients read the same revision before either submits. The runner then forces a +known write ordering over real loopback HTTP; this is a reproducible stale-writer +interleaving, not an uncontrolled concurrency load test. + +| Case | Expected and observed | +| --- | --- | +| C01 Blind whole-map replacement | Reports update succeeds, then a stale Account replacement silently restores the old Reports binding: negative lost-update control | +| C02 Conditional replacement, disjoint intent | Stale Account replacement rejected; explicit refetch and reapplication of only Account intent preserves both updates | +| C03 Competing intent for one feature | Stale Reports rollback rejected; active newer Reports binding retained; no automatic overwrite | + +The fixture uses an integer revision carried in If-Match and a synthetic 409 conflict +response, not a full HTTP ETag/precondition implementation. Comparison and replacement +have no asynchronous gap in one server process. A distributed store still needs its +own atomic conditional-write primitive. Refetch alone does not justify retry: C02 +reapplies a disjoint intent; C03 deliberately leaves conflicting intent unresolved. +These tests do not execute import-map-deployer or verify its implementation. + +## Reload cases + +The user-controlled update path offers keep-working, preserve-and-reload, and explicit +discard-and-reload. Persistence uses sessionStorage with synthetic non-sensitive text, +a draft schema and a base-record marker. No server save or remote acceptance is inferred. + +| Case | Expected and observed in both browsers | +| --- | --- | +| R01 Unguarded reload | Unsaved input disappears and R2 loads: negative control | +| R02 Defer update | Draft stays editable in the old R1 session | +| R03 Preserve and reload | R2 loads with exact draft text and original base marker recovered | +| R04 Inject storage refusal | Reload blocked; original draft and R1 remain | +| R05 Incompatible target draft schema | Update deferred before reload; original input retained | +| R06 Explicit discard | Both current input and an older stored draft cleared; R2 starts empty | +| R07 Unknown write outcome | Reload blocked while the synthetic pending-outcome flag is set | +| R08 Edit during asynchronous preflight | Latest edited text, not an earlier snapshot, persists and returns | +| R09 Activation changes after preflight snapshot | New document detects schema mismatch again; stored text retained in read-only recovery state | + +R08/R09 hold a server response at an explicit barrier, make the intervening edit or +activation change, then release it. No arbitrary settling delay or automatic retry +is used. The poll waits for the barrier itself. R09 demonstrates why a successful +preflight cannot promise that the next document has the same compatibility contract. +The fixture deliberately changes its declared draft schema; it does not implement +an actual schema migration or prove compatibility of a real feature's draft format. + +## Limits and interpretation + +“Pass” includes intentionally observed failures in C01 and R01. The preservation +path is an authored policy, not an automatic browser or framework guarantee. Explicit +recovery retains bytes; it does not resolve a stale base, merge edits or establish +permission to retain/export them. The unknown-write flag tests a UI gate only; no +real operation lookup or idempotent replay occurs. Discard does not reverse a remote +operation and is blocked by that same unknown-outcome gate. + +sessionStorage covers this tab's same-origin reload, not device loss, guaranteed +browser restart or independent backup. The injected write refusal tests the failure +branch, not browser quota exhaustion. Other tabs, login/access revocation, storage +schema migration, interruptions during navigation, native reload/tab close and browser +crashes are not covered. An application-level update button is not a universal unload +interceptor. Persisted drafts remain until explicit discard in this fixture; a real +product needs an authorized retention/cleanup policy and safe handling of stale drafts. + +## Candidate disposition + +[Independent Feature Boundary](../candidates/IndependentFeatureBoundary.md) gains +conditional activation and session-transition obligations. [Local Draft State](../candidates/LocalDraftState.md) +gains a partial implementation of explicit preservation/refusal, with post-navigation +schema revalidation. [VC-06](../walkthroughs/VC06-MicrofrontendEstate.md) remains +provisional: this supports its release/reload trace, not production rollback or a +completed accessibility/security review. Corpus remains seventeen hypotheses. + +Next WP-0003 T01 evidence: navigation/history/full-versus-fragment caching, which is +still pending from VC-03. Retain distributed activation atomicity, authorized draft +retention and native-unload/crash behavior as limits requiring evidence before a +production recommendation; do not grow this fixture into a release platform. diff --git a/research/walkthroughs/VC06-MicrofrontendEstate.md b/research/walkthroughs/VC06-MicrofrontendEstate.md index 2fa477b..0b71f42 100644 --- a/research/walkthroughs/VC06-MicrofrontendEstate.md +++ b/research/walkthroughs/VC06-MicrofrontendEstate.md @@ -104,3 +104,14 @@ This supersedes the lack of any load/rollback execution in the initial self-revi but the quality criterion remains revise: browser focus/accessibility, unsaved work, real API/data compatibility and independent review are still unverified. VC-06 stays provisional; no production release or M2 acceptance is inferred. + +## Activation/update-reload follow-up — 2026-09-26 + +[21 expected observations](../reviews/ActivationConflictsAndReload.md) now cover +stale binding writers and the application-controlled reload path with unfinished work. +Conditional replacement rejects stale maps; disjoint intent is explicitly reapplied, +while conflicting rollback awaits a decision. Preserve the latest authorized draft +before reload and revalidate its schema on the next document; a preflight check can +be invalidated by a later activation. Storage failure and unknown write outcome block +the fixture's reload. This does not establish distributed-map atomicity, native-unload +protection, backend acceptance, authorized retention or complete VC-06 validation. diff --git a/workplans/FEP-WP-0003-contrasting-schools.md b/workplans/FEP-WP-0003-contrasting-schools.md index 10ff78a..b3a746e 100644 --- a/workplans/FEP-WP-0003-contrasting-schools.md +++ b/workplans/FEP-WP-0003-contrasting-schools.md @@ -168,3 +168,20 @@ safe reload with unfinished work; navigation/history/cache evidence remains pend Do not build a production release platform to satisfy this research model. T02/T03, independent-human gate and M1/M2 completion remain unchanged. See research/reviews/FeatureReleaseExperiment.md. + +## Requested activation-update conflicts and safe reload — 2026-09-26 + +T01 progress: conflicts-reload.mjs adds three HTTP stale-writer cases and nine reload +cases in Chromium/Firefox, 21/21 expected observations. Blind overwrite and unguarded +reload reproduce loss as negative controls. Conditional replacement rejects stale maps; +disjoint intent is explicitly reapplied, same-feature rollback stays unresolved. +Draft deferral, preserve/restore, injected storage failure, schema mismatch, explicit +discard, unknown-write gating, edit-during-preflight and post-check activation covered. +New-page schema revalidation retains incompatible text read-only. See +research/reviews/ActivationConflictsAndReload.md. Original release artifacts unchanged. + +Requested bounded tests complete; no distributed-store atomicity, native-unload/crash, +real backend outcome or retention authorization claim. These remain T01 limits for +any production recommendation. Next breadth task: pending VC-03 navigation/history/ +full-versus-fragment cache evidence. Seventeen hypotheses; T02/T03 and M1/M2 gates +unchanged. No model inference or production actions.