Skip to content

v1.29.2

Latest

Choose a tag to compare

@github-actions github-actions released this 22 Oct 14:53
· 1 commit to main since this release
v1.29.2
5483510

[1.29.2] β€” 2026-10-22

  • Interim maintenance release between v1.29.1 and v1.30. Highlights, each detailed below:

    • Settings that were stored but never applied now work, and dead controls are gone (#818).
    • The production web app ships its self-hosted DuckDB-WASM files (#1124).
    • The page paints before startup finishes, and a stalled start becomes visible (#1134).
    • Font and app license texts ship with every build (#1136).
    • The Gemini key leaves the request URL (#1135).
    • Desktop and container publishing waits for an exact-commit security audit (#911).
    • The local model list offers only models the browser runtime can load (#743).
    • The desktop third-party notices and SBOM are complete again under pnpm 12 (#871).

    v1.29.2 adds no new storage format and makes no encrypted desktop project storage claim; that work (R-15) belongs to v1.30.

  • Fix (offline translations after an update): right after a service-worker update, the app could lose its translations when it went offline before re-fetching them. The new version's dynamic cache was still empty, and the locale handler never looked at the bundles already precached in the static cache. It now falls back to that precached copy (PR #1141).

  • Fix (local AI models): Settings offered three WebLLM models that the bundled WebLLM runtime cannot load: Gemma 3 1B, Gemma 3 4B and Llama 3.3 70B (PR #1142, #743). They are not in its model list, so downloading one failed and generation quietly fell back to the small ONNX model. The three entries are removed, and devices with little GPU memory are now recommended Llama 3.2 1B instead of Gemma 3 1B. A selection stored before this change is kept as it is and keeps its local fallback. A test now checks every offered model against the installed runtime's model list. The defect dates back to v1.5; the release checks for v1.29.0 and v1.29.1 counted only changed AI source files and did not check that the offered models exist.

  • Fix (desktop license notices): Since the move to pnpm 12 (PR #1110), the third-party notices and CycloneDX SBOM in the desktop installers left out about 60 shipped JavaScript packages (PR #1143, #871). These included the local AI runtimes WebLLM, Transformers.js and ONNX Runtime Web, and the optional dependencies of jsPDF and the collaboration transport. pnpm 12's licenses list --prod drops optionalDependencies and the dependencies of linked workspace packages such as @domain/ai-core. scripts/generate-third-party-inventory.mjs now takes the shipped set from pnpm-lock.yaml (dependencies and optional dependencies of the app and its linked workspace packages, transitively) and takes only the license metadata from pnpm -r licenses list. The Linux inventory has 666 components again (v1.29.1: 665). The differences are real dependency updates. No published release was affected: v1.29.1 was built with pnpm 11. The release check caught this by comparing the candidate's SBOM with the published one.

  • Fix (startup): The page no longer stays blank while startup waits (PR #1134). index.html now paints a static boot shell inside #root before any script runs, and React replaces it on mount. If the app has not mounted after 15 seconds (for example because another tab still holds an older IndexedDB connection, now logged through onblocked), a notice explains the likely cause and offers Reload, and the stall is logged as an error. Both notices show English at once and switch to the active language from the runtime locale bundles (new keys error.startup.slowStart and error.startup.critical in all 19 locales). The production smoke test now waits for the real app and rejects the boot shell and startup notices, so a bundle that never mounts can no longer pass it. The startup error and the slow-start notice are built with DOM APIs; the old Reload button used an inline onclick that the script-src 'self' CSP blocked, so it never worked. Lighthouse's recurring NO_FCP failures (a run where the app never painted within 90 s) can no longer abort the gate, because the shell always paints; a boot that still hangs now shows the notice and logs the error instead of a blank page. Why those runs did not mount is not yet proven.

  • Compliance (font licenses): The SIL Open Font License texts of the ten bundled @fontsource fonts and the app's MIT license are now produced at build time by scripts/copy-third-party-licenses.mjs into public/licenses/ (PR #1136, #818). This runs in predev, prebuild and the Vercel edge build (RUNTIME_ASSET_SCRIPTS), so the texts ship with every build from the pinned packages. The font list lives in services/thirdPartyNotices.ts, and a test keeps it equal to the fonts index.tsx imports. Eleven copies of these files had been committed by accident with #1124; they are removed from version control, and public/licenses/ is ignored like public/duckdb/.

  • Security (release publication): Publishing now requires an exact-ref security audit of the commit being published (PR #1137, #911). Tauri release (GitHub Release assets and updater latest.json) and GHCR build-push (version, major.minor and latest tags) each need a security-audit job in the same workflow. That job validates github.sha, checks out exactly that commit, proves HEAD matches it, and runs the pinned OSV scanner with src-tauri/osv-scanner.toml over pnpm-lock.yaml, src-tauri/Cargo.lock and crates/Cargo.lock. The publish jobs run only when that audit succeeded for the same SHA; a finding, scanner error, skip, cancellation, timeout or SHA mismatch leaves them unrun, for tag pushes and manual dispatches alike. scripts/release-security-gate.mjs and its unit tests reject any weakening, including a new write-privileged job outside the gate. The dispatch-only release-gate-proof.yml demonstrates the gate without publishing anything.

  • Security (AI connection test): The Gemini connection test under Settings β†’ AI sends the API key in Google's x-goog-api-key header instead of the ?key= query string, so the key no longer appears in the request URL (PR #1135, #1129). In the Local and Eco AI modes, the test for any cloud provider is now refused before a key is read or a request is made ("The current AI mode allows only local models", new key settings.ai.testError.aiModeBlocked in all 19 locales), matching the AI mode as the single cloud authority (#1122). Local providers stay testable.

  • Docs: The in-app help (19 locales) now names the real Early Access Features section instead of "Experimental flags", says which features are on by default, drops switches that no longer exist (cross-project search, Plot Board), describes the flag reset correctly, and lists the actual appearance options instead of non-existent specialty themes (PR #1123).

  • Privacy: The AI execution mode is now the single switch for cloud AI (PR #1122, #818). "Local storage only" (on by default) blocked cloud AI on top of the mode, and "EU data residency" only blocked OpenAI and Grok without controlling where data is processed; both toggles are removed. A one-time migration keeps existing installs as they were (a local-only choice becomes local mode); fresh installs start in hybrid. A remote LanguageTool server is used only when you enable it and enter that server.

  • Maintenance (CI): Lighthouse CI waits up to 90 s for first paint and 120 s for load, instead of the defaults (30 s and 45 s), in both the mobile and desktop configs (PR #1133). One run that errors aborts the whole lhci autorun. On 2026-10-21 a cold first run after the visual-regression phase failed the required Browser Quality job three times with NO_FCP, although the app paints.

  • Settings: New Restore default settings under Settings β†’ Data & Backups (PR #1121, #818). It returns every setting to its default without deleting projects, snapshots or API keys, and keeps the UI language, the AI execution mode, privacy choices and collaboration servers, so a reset never silently re-enables cloud AI. Until now the only way back was a factory reset that deletes everything.

  • Settings: Theme Customization under Settings β†’ Appearance gets a card with a reset per color and a reset of all customization back to the selected theme (PR #1120, #818). Since custom colors became active there was no way to undo them. The card also warns about an explicit text color below WCAG 4.5:1, bounds the custom CSS editor, and its hints say that high contrast pauses custom colors (19 locales).

  • Maintenance (Vercel builds): Vercel Previews now build on demand (PR #1130). vercel.json ignoreCommand runs scripts/vercel-ignore-build.mjs: Production always builds, and a Preview builds on a branch's first deployment, when the commit message contains [preview], or when a deployment input changed: vercel.json, vite.config.*, package.json, packages/*/package.json, patches/**, pnpm-lock.yaml, pnpm-workspace.yaml, .npmrc, .nvmrc, the edge build scripts (build-edge, runtime-asset-steps, sync-deploy-base, resolve-deploy-base, dependency-state, copy-*), config/resolveViteBase.ts, the ignore step itself, or api/**. Unknown state builds. This saves build minutes; correction waves and the strict merge cascade had driven about 75 builds in one day. A skipped build still counts as a canceled deployment. The nightly record pruner now keeps the newest three Vercel deployment records per environment instead of ten, plus main and every open PR head.

  • Fix (web): The production web app (Vercel) did not ship the self-hosted DuckDB-WASM files, so the analytics store could not start; requests for them returned the app page instead (PR #1124). The edge build now produces these assets like the regular build does. The desktop app and GitHub Pages were not affected.

  • Settings: Spell Check under Settings β†’ Advanced Editor was stored but never applied; it is replaced by an opt-in Browser Spell Check (off by default, with a note that some browsers' enhanced spell check sends text to their provider) (PR #1118, #818). Auto-Complete Suggestions and Grammar Check, which had no implementation, are removed; grammar checking remains available through LanguageTool under Integrations.

  • Settings: The Word Count Display, Reading Time Estimate and Writing Statistics toggles under Settings β†’ Advanced Editor now work (PR #1117, #818). The editor badge shows the word count and a reading-time estimate according to their toggles, and the inspector's statistics card follows Writing Statistics.

  • Settings: The Top P and Rate Limit sliders under Settings β†’ Advanced AI are removed; their values were stored but never sent to a provider or used by any limiter (PR #1119, #818). The unused frequencyPenalty, presencePenalty and customPrompts fields are gone too. Temperature and Max Tokens stay.

  • Settings: Theme Customization now works (PR #1116, #702, #818). Its colors and custom CSS were saved but never applied; they now map onto the design tokens (base theme β†’ appearance preset β†’ customization β†’ accessibility; high contrast, in the app or from the system setting, pauses the custom colors while custom CSS stays active; labels on fixed status fills keep their own color). Custom CSS runs in one app-owned stylesheet with @import and remote url() stripped; the stored settings keep the original source and any unknown fields unchanged. Button text on a custom primary color and text on a custom background without a text color are picked to reach at least 4.5:1 contrast. The never-applied old defaults are treated as unset, so existing installs do not change. New installs use the plain writing surface (#701); an explicit textured choice is kept.

  • Docs: WorldScript has no user accounts, by design. The README and the in-app help (19 locales) now say so plainly instead of "no account required" (PR #1114). The help no longer describes a Cloud Sync feature that does not exist (article removed, 2947 β†’ 2945 keys) or lists "cloud sync providers". The feature-flag article names all 8 opt-in flags instead of stale counts. The broken DeepWiki and i18n README badges are fixed.

  • Maintenance (Dependabot): vite-plugin-pwa 1.3.0 β†’ 2.0.0 (PR #1115, successor of #1109). The major version only raises the Node floor to 20.19 (the repo requires β‰₯ 22.19) and allows @vite-pwa/assets-generator ^2 as a peer; the plugin source is unchanged between the two releases and Workbox stays at 7.4.1.

  • Maintenance (Dependabot): google/osv-scanner-action 2.6.0 β†’ 2.7.0 and ossf/scorecard-action 2.4.4 β†’ 2.5.0 (actions-minor-patch group; PR #1113, successor of #1107), with the OSV pin in workflowPolicy.test.ts moved to match. OSV 2.7.0 scans the same 1673 npm, 583 src-tauri and 175 Core Cargo packages as 2.6.0.

  • Maintenance (Dependabot): tokio 1.53.2 β†’ 1.53.3 in src-tauri (cargo-minor-patch group; PR #1112, successor of #1105). Patch release (Rust 1.71, below the 1.90 floor); lockfile only.

  • Toolchain: Move the pinned pnpm from 11.28.5 to 12.13.0 (PR #1110, QNB-55): packageManager, engines.pnpm, the pnpm/setup steps in ci.yml and the shared setup action, and the docs that state the pin. 12.13.0 is the newest release past the 7-day minimumReleaseAge. pnpm-lock.yaml stays unchanged: pmOnFail: ignore stops pnpm 12 from adding its package-manager document at the top of the lockfile, because GitHub's dependency graph reads only the first YAML document and would otherwise lose every npm dependency (dependabot-core#15904). dependency-state.mjs reconcile (Vercel's install command and the local reconcile path) now refuses to install when the running pnpm differs from the pin; CI installs the exact pin through pnpm/setup; and Vercel builds use Corepack (ENABLE_EXPERIMENTAL_COREPACK=1) to run exactly that pin. The supply-chain settings (minimumReleaseAge, strictDepBuilds/allowBuilds, verifyDepsBeforeRun, engineStrict, frozen lockfile) were re-proved under pnpm 12, which also rejects unknown pnpm-workspace.yaml settings. The general CI and the shared setup action install pnpm 12.13.0 through pnpm/setup; the reviewer trust workflow installs whatever the base branch's packageManager pins (#1111), so it moves to 12.13.0 once this lands.

  • CI: Restore the required Reviewer Governance Trust Boundary check (PR #1111). Since #1101 it failed on every PR because pnpm/setup refuses a version input (11.22.0) that differs from the base packageManager pin (11.28.5). The trust workflow now has no version input, so the trusted base's own pin selects pnpm, and the workflow policy test keeps it that way.

  • Toolchain: Update the pinned pnpm from 11.22.0 to 11.28.5 (PR #1101): packageManager, engines.pnpm, the pnpm/setup steps in ci.yml and the shared setup action, and the docs that state the pin. 11.28.6 is newer but still inside the 7-day minimum release age. The lockfile is unchanged; the frozen reconcile accepts it as is. reviewer-governance-trust.yml is a protected trust-boundary workflow and keeps pnpm/setup 11.22.0 until a base-owned transition authorizes the change; pnpm follows packageManager and runs 11.28.5 there too.

  • Maintenance (Dependabot): Monthly, grouped version updates (PR #1100). Minor and patch updates are grouped per ecosystem: npm-minor-patch, cargo-minor-patch, core-minor-patch, actions-minor-patch, next to the existing react, dev-tooling, tauri-deps (now minor/patch) and codeql-action groups. Majors that are not ignored (the Stryker majors are) stay separate, except in the lockstep react and codeql-action groups, and vite, @huggingface/transformers and zod keep their own PRs. Minor/major updates of the documented Core MSRV pins in crates/ are ignored; patches still arrive. Security updates are unaffected by the schedule. The triage runbook now describes the signed-successor flow it already used, and the inventories in docs/DEPENDABOT-TRIAGE.md and docs/CI.md match the new configuration.

  • Toolchain: Declare src-tauri rust-version = "1.90" and build it with exactly that toolchain in the Tauri Rust Gate (cargo +1.90 check --locked --all-targets) (PR #1099, #572). The declared 1.77.2 no longer held: 82 of 581 locked crates.io packages already required a newer Rust, up to 1.89 (notify-rust 4.18 via tauri-plugin-notification), and no CI job built src-tauri with its declared floor. The current Tauri plugin lines need 1.90. The Rust Core (crates/) keeps 1.77.2 and its existing exact build. The higher floor activates Clippy's MSRV-gated suggestions; the one hit (unnecessary_map_or in task_supervisor.rs) now uses Option::is_none_or with unchanged behaviour. The updater stays on 2.12.x until the Tauri plugin group is admitted.

  • Security: Update rustls-webpki 0.103.15 β†’ 0.103.16, webpki-roots 1.0.7 β†’ 1.0.10 and webpki-root-certs 1.0.7 β†’ 1.0.10 in the Tauri lockfile (PR #1103) for RUSTSEC-2026-0382 (certificate verification may accept distrusted intermediates) and RUSTSEC-2026-0381 / -0383 (bundled roots trusted distrusted emSign intermediates). The advisories were published on 2026-10-19/20 and turned the required OSV scan red on main (475da803, run 39486423119). The fixed versions declare Rust 1.71; lockfile only, with no other package changed.

  • Security: Pin follow-redirects to exactly 1.16.1 through an override (PR #1102) for GHSA-8r9p-f939-6h3c and GHSA-q3jh-wrc6-jgq8 (both High, CVSS 8.2, published 2026-10-20). In 1.16.0, credential headers could survive cross-origin redirects. The package is transitive via axios and http-proxy, whose ranges already allow 1.16.1 (published 2026-10-01, outside the 7-day release-age floor). Before this override, the required OSV scan reported both advisories on every PR head; with it, the scan passes with no findings. The lockfile change is follow-redirects 1.16.0 β†’ 1.16.1 only.

  • Dependencies: Update vite from 8.2.1 to 8.3.4 (PR #1098; Dependabot source PR #1054). Releases 8.3.0–8.3.4 list features, bug fixes and Rolldown dependency updates, no breaking change. Because the production build uses Rolldown, the required Build job’s production smoke:prod check (the real bundle must mount in headless Chromium) is the release check for this bump, as docs/DEPENDABOT-TRIAGE.md requires.

  • Dependencies (CI): Update the github/codeql-action group from 4.38.2 to 4.38.3 (init, autobuild, analyze, upload-sarif) in codeql.yml and scorecard.yml (PR #1097; Dependabot source PR #1084). Patch release; all four subpaths move to the same pinned commit together, as docs/DEPENDABOT-TRIAGE.md requires.

  • Dependencies (CI): Update actions/download-artifact from 8.0.1 to 8.0.2 in ci.yml, mutation.yml and tauri-build.yml (PR #1077; Dependabot source PR #1051). Patch release; every uses: reference moves to the same pinned commit.

  • Dependencies (CI): Update actions/upload-artifact from 7.0.1 to 7.0.2 in every workflow that uses it (PR #1096; Dependabot source PR #1049). Patch release; every uses: reference moves to the same pinned commit, so no workflow keeps the older version.

  • Dependencies: Update the dev-tooling group (PR #1095; Dependabot source PR #1083): @playwright/test 1.63.0 β†’ 1.64.0, Storybook 10.6.0 β†’ 10.6.2 (core, addon-a11y, builder-vite, react, react-vite), @storybook/test-runner 0.24.5 β†’ 0.26.0, vitest and @vitest/coverage-v8 5.0.2 β†’ 5.0.4. The playwright / playwright-core overrides in pnpm-workspace.yaml move to 1.64.0 with it, so the runner and the core stay one version (they had pinned 1.61.0). Playwright 1.64 now forwards screen from device descriptors, so emulated window.screen and media queries follow the device. Upstream marked @storybook/test-runner as no longer officially supported in 0.26 (#610 there); a later move to the Storybook Vitest addon is the replacement path.

  • Maintenance (deployment records): The daily prune-deployments.yml run now also bounds the GitHub deployment records that Vercel creates (PR #1094). It keeps the newest ten per Production / Preview environment, the record of the current main commit and the Preview records of open PR heads; github-pages records stay with the existing Pages pruner. The step runs inside the already-allowlisted jobs. The dispatched plan reads only. The scheduled job writes only for the trusted workflow on the commit that is still main; before each candidate it re-reads the record, main and the open PR heads, requires a recognized and then proven inactive status before the DELETE, keeps the shared rate reserve, and deletes at most 40 records per pass. New scripts/prune-vercel-deployment-records.mjs with a node:test suite (12 cases) run through Vitest; documented in docs/PAGES-DEPLOYMENT-RECORD-PRUNING.md.

  • Dependencies: Update @huggingface/transformers from 4.2.0 to 4.3.1 in packages/ai-core (PR #1093; Dependabot source PR #1055). The dist/transformers.web.js entry used by the vite.config.ts alias and the exports map are unchanged, and the vendor-* chunks key on our own source paths. It brings onnxruntime-web 1.31-dev and onnxruntime-node 1.30.0; adm-zip >=0.6.1 and allowBuilds: onnxruntime-node=false still apply. Release notes list no breaking change.

  • Maintenance (Vercel quota): Turn off Git-triggered Vercel deployments for dependabot/** and deptrain/** branches through vercel.json git.deploymentEnabled (PR #1092). In the 24 hours before 2026-10-20 10:55 UTC, 38 of the project's 47 deployments came from these branches (30 Dependabot rebases, 8 cohort successors) on the Hobby plan. An ignoreCommand was not used because canceled builds still count as deployments. Dependency PRs are qualified by the required GitHub Actions checks; Production builds of main and Previews of other branches are unchanged. Documented in docs/VERCEL-PREVIEW-RETENTION-POLICY.md.

  • Dependencies: Update @google/genai from 2.22.0 to 2.28.0 (PR #1091; Dependabot source PR #1059, which by then proposed 2.29.0). 2.28.0 is the last release before 2.29.0 made automatic continuation the default for generateContent / generateContentStream; that path has defects fixed only in 2.30.0, which is still inside the 7-day minimum-release-age. The version is pinned exactly so no lockfile re-resolution can move to 2.29.x. Releases 2.23–2.28 list features and bug fixes only; lockfile and manifest only.

  • Dependencies: Update ai (Vercel AI SDK) from 7.0.109 to 7.0.138 (PR #1090; Dependabot source PR #1057, rebased by Dependabot after the cohort freeze; same 7.0 patch line). Manifest range ^7.0.130; lockfile and manifest only.

  • Dependencies: Update @ai-sdk/google from 4.0.85 to 4.0.90 (PR #1089; Dependabot source PR #1058). Patch releases; lockfile and manifest only.

  • Dependencies: Update yjs from 13.6.32 to 13.6.33 (PR #1088; Dependabot source PR #1060). Patch release; lockfile and manifest only.

  • Dependencies: Update serde_json from 1.0.151 to 1.0.154 in the Core lockfile (PR #1087; Dependabot source PR #1081). serde_json 1.0.154 declares Rust 1.71, inside the Core floor 1.77.2. No manifest or source changes.

  • Dependencies: Update serde_json from 1.0.151 to 1.0.154 in the Tauri lockfile (PR #1086; Dependabot source PR #1080). serde_json 1.0.154 declares Rust 1.71, inside the 1.77.2 floor. No manifest or source changes.

  • Maintenance: Require the Vercel retention operator to run Node 22.19.0 or later in the 22.x line before source checks or provider/child-process access; the application's Node 22/24 CI matrix is unchanged. Issue #627. PR #1078.

  • Security (desktop updater): Build tauri-plugin-updater without its zip feature (PR #1079, part of #911). The feature only unpacks the v1 .zip / .tar.gz updater bundles; with createUpdaterArtifacts: true the release ships the raw -setup.exe / .msi / .AppImage, and the macOS .app.tar.gz path does not depend on it. zip 4.6.1 (RUSTSEC-2026-0367/-0368/-0369) and its arbitrary helpers leave src-tauri/Cargo.lock. No version change; Rust 1.77.2 stays the declared floor.

  • Security (desktop): Remove the unused optional rust-compute feature and its candle-core / candle-nn dependencies (PR #1079, part of #911). The feature was off by default, no Rust code was gated on it, and no build enabled it. The 64 packages of that subtree leave src-tauri/Cargo.lock, among them zip 8.6.0 (RUSTSEC-2026-0367/-0368/-0369), safetensors, tokenizers, gemm and pulp; no package is added or changes version.

  • Security (OSV policy): Accept RUSTSEC-2026-0370/-0371/-0372 (zvariant, zbus_names, zvariant_derive) in src-tauri/osv-scanner.toml until 2026-11-30 (PR #1079, part of #911). They are "unmaintained" notices without CVE, CVSS or fixed version: zbus 6.0 absorbed the three crates. The 5.x copies are Linux-only through the Tauri notification and single-instance plugins, whose every published line requires zbus ^5; the 4.x copies are a never-compiled weak keyring edge in crates/Cargo.lock that cannot be dropped without downgrading the Secret Service session to Plain. The RUSTSEC-2024-0436 (paste) entry is removed because no scanned lockfile contains paste any more.

  • Dependencies: Update Tokio from 1.53.1 to 1.53.2 in the Tauri lockfile (PR #1076; Dependabot source PR #1044). No manifest or source changes.

  • R-15 Gate 4: removal on Windows, test-support only (slice H1a-2, part 4 of 4). The object is opened through the handle-relative lookup with delete access and read-only sharing (no handle that opens the file can write to it while the removal holds it, with the exception of a writable mapped view that existed before, see below; a writer that already has it open makes the open fail Busy), authenticated on that handle, and then given the POSIX delete disposition on the same handle: no FILE_FLAG_DELETE_ON_CLOSE, no fallback, a read-only file is refused. One new unsafe call (the disposition, one fixed structure, layout pinned by a test); a counter-proof shows what write sharing would have let through. The cases of remove.rs now run on Windows too, every code that means "no POSIX delete" (including ERROR_INVALID_FUNCTION) is Unsupported, and a directory is looked at again right before the disposition so that one converted into a junction meanwhile is left alone (Redirected; narrows the window, does not close it). A writable memory-mapped view that another process created earlier and keeps after closing its handle is not stopped by the sharing rule (same-user residual, stated, not closed); on Unix, which has no sharing mode, any same-user writer can change the bytes between the authentication and the removal, which was not stated before: the removal binds the object, not its bytes. No new package. Issue #359. PR #1075.

  • R-15 Gate 4: removal of a directory on Linux, the recovery of leftovers and the crash cases on Unix, test-support only (slice H1a-2, part 3). remove_empty_dir moves the directory through the same quarantine directory and checks the link count of the held directory afterward; macOS reports no usable signal for a removed directory that is still open (hosted probe: link count and F_GETPATH unchanged), so there an empty directory answers Unsupported before anything is moved; remove_leftover_file takes a leftover .h1q-* directory by its generated name only, authenticates the file in it again and removes it only if accepted. A simulated crash, an entry stranded by a blocked put-back and a foreign object left by a refused put-back are covered; the held directory is synced for what the unlink changed in it, like any removal; a non-empty directory swapped in the last window is Disturbed, not NotEmpty. The recovery also removes an empty directory left by a crashed directory removal (Linux; it holds nothing, so it is not authenticated), and syncs the parent when it removes the emptied quarantine directory, whatever the removal came to. No new unsafe, no new package. Issue #359. PR #1074.

  • R-15 Gate 4: hardening of the Unix file removal against a same-user actor in the quarantine directory, test-support only (slice H1a-2, part 2b of 5). The parent is synced after a rollback, a stranded entry, a put-back and a vanished name, and the quarantine directory an entry moved through is synced too (a sync of it that fails is an error after a put-back, a kept entry or an entry an actor removed again, and makes a removal NotConfirmed, never another outcome); the quarantine directory, also when its setup fails, is removed by its name only after a check that the name still leads to the directory that was created (the check narrows the race and does not close it, since rmdir takes a name: a lost race removes at most an empty directory); an entry, a foreign one included, is reported as retained only if the recorded name still leads to its directory and the entry in it was looked at and is the one that was seen, otherwise Disturbed (a look at a name, so it detects after the fact and does not prevent; a foreign entry is not opened, so nothing holds its identity unique and a reused identity is not noticed); a quarantine directory that cannot be opened is refused. Tests force each case, including the quarantine renamed away and the put-back blocked. No new unsafe, no new package. Issue #359. PR #1073.

  • R-15 Gate 4: removal of a file on Unix, test-support only (slice H1a-2, part 2 of 4). PinnedDir::remove_authenticated_file moves the entry into a quarantine directory with RENAME_NOREPLACE, identifies the open object there and only then unlinks it; anything else is put back, a failure after the move rolls back or reports Stranded, and the parent is synced. The one window no Unix primitive closes (a same-user actor in the quarantine directory after the last identification) is forced in a test and reported as Disturbed, detected after the fact and not prevented. The rustix process feature is added for geteuid; no new package, no new unsafe. Directories, leftovers and Windows follow. Issue #359. PR #1069.

  • R-15 Gate 4: the API and the isolation of the bound removal, test-support only (slice H1a-2, part 1 of 4). PinnedDir::remove_authenticated_file, remove_empty_dir and remove_leftover_file with RemovalOutcome, RemovalStage and Observer exist only with the test-only feature, have no caller and, in this part, answer Unsupported and remove nothing; the platform implementations follow in three stacked parts. A scan of the sources and a compile of a dependent with and without the feature keep the primitives out of every other build. PinnedDir::sync (no deletion) is added. The contract states what no Unix primitive can do (unlink "this inode": a same-user actor in the quarantine directory can win one race, which is detected after the fact as Disturbed, not prevented) and leaves the decision about that residual to the maintainer before any sweep uses it. No new unsafe, no new dependency feature, no new package. Issue #359. PR #1068.

  • R-15 Gate 4: handle-relative pinned I/O (slice H1a-1). A new parallel module pinned opens one directory with no-follow semantics, resolves it once to a link-free path, and reaches everything below it through a directory that is held open (PinnedDir, PinnedFile): bounded listing, bounded reading from the start of the file, identity and hard-link count; a link, junction or reparse point is refused at every component below the root, and a name must be exactly one ordinary component (on Windows also no stream, wildcard or device name). Unix uses *at calls on directory descriptors and checks the root's path without following a link; Windows pins a directory with a handle that does not share delete and opens children (NtCreateFile relative to the handle), listings (the handle's directory information) and entry kinds relative to it, so a directory converted into a reparse point in place leads nowhere. It changes nothing in the file system, has no caller and does not change DurableFs; the removal primitives follow in H1a-2 behind test-support. Local unsafe for the relative open, the listing and the final path, and one typed handle-query helper in root_lock (an unsafe trait with two implementations); the Wdk_* features of the windows-sys already locked, no new package. Issue #359. PR #1067.

  • R-15 Gate 4: takeover with a trusted clock observation (slice Q1). The mutation-capable takeover no longer takes a timestamp: JournalTakeoverCommit carries a LeaseClock, which is sealed (it cannot be implemented outside the crate; the one production clock is SystemLeaseClock, the UTC wall clock of the operating system), commit_journal_takeover reads it once while the root-commit lock is held into an opaque TrustedLeaseTimeObservation (no constructor from a number), and publish_takeover_fenced takes that observation. An unavailable or unset (zero) clock refuses before a journal or authority byte is written. The predicate now refuses a takeover by the committed lease owner itself (that is a renewal, which needs no clock) and a committed lease whose fields contradict each other. The OS wall clock is not attested and no protection against a compromised clock is claimed. Decision Q1-D1 mirrored on #359, with the one deviation from its letter (the clock reader lives sealed in Core) flagged for the owner. Issue #359. PR #1066.

  • R-15 Gate 4: inherit unchanged pages in a streamed capture (slice I1b). StreamedCapture::push_page_inheriting keeps the stored predecessor's page of the same index when the pushed entries are exactly its entries (the predecessor's page is read back and authenticated first, one page at a time): the kept page takes the predecessor's reference and generation, nothing is staged for it, and its entries still go into the inventory digest, so the successor equals what capture_inventory builds from the same pages. A page that differs or lies beyond the predecessor's pages is staged as before. The promotion stores a kept page from the predecessor's set directory under the new set's own directory, and the root-bound begin_inheriting_streamed_capture verifies the stored set once and keeps unchanged pages on every push; begin_streamed_capture is unchanged. A predecessor of another manifest or a foreign key is refused before anything is created. Stored-set reclamation is the next slice. Issue #359. PR #1065.

  • R-15 Gate 4: inherit unchanged inventory pages (slice I1a). promote_inventory_set_inheriting_fenced lets a capture keep a page of the stored predecessor instead of rewriting it, but only if the page's reference (index, generation, entry count and the content digest of its exact envelope bytes) equals the reference of the same index in a VerifiedInventory of exactly the committed manifest the set is written under: a re-sealed copy of the same entries, a changed page that claims an older generation, a page beyond the predecessor's page count and a predecessor of another manifest are refused before the first byte. An inherited page is stored again under the new set's own directory, so each stored set stays self-contained. The root-bound commit_inventory_capture (and the exclusive final capture) verifies the stored set the root names when, and only when, a page keeps an earlier generation; the strict promote_inventory_set_fenced and the streamed path keep their rule (streamed inheritance and stored-set reclamation are the next slices). Issue #359. PR #1064.

  • R-15 Gate 4: reclaim abandoned inventory staging (slice R1). reclaim_abandoned_inventory (compiled only with the test-only test-support feature until handle-relative I/O closes the check-then-use windows of its path-based removal; no production build contains the sweep, and tests/feature_isolation_test.rs keeps it so) reads the binding, the manifest and the journal key from the committed root, takes only the owner's fence, and removes the pending directories (inventory/pending-<revision>-<random>) of a revision the journal has committed past: a promotion needs the next revision and the committed revision only grows. Revision R + 1 and above is kept (a live attempt, a retry or a prepared root may own it), as are stored page sets (reclaiming them waits for the inheritance slice, which breaks the generation-based proof), anything outside the exact name grammar, every link or junction (nothing is read or unlinked through one: DurableFs::entry_kind) and any file that does not open as this operation's page; a directory is validated completely before its first removal and files are re-authenticated just before they are unlinked. The root commit event is held from the read of the binding to the last removal, and a former owner, a wrong fence and a journal in RECOVERY_REQUIRED remove nothing. DurableFs::remove_dir and entry_kind are provided methods that default to Unsupported, so an adapter without them is refused before anything is deleted; StdFs removes only empty directories. No caller schedules the sweep and the path-based directory and root identity stay the H1 residual, whose acceptance criteria the Gate 4D evidence lists. Issue #359. PR #1063.

  • Security: update rustls to the OSV-fixed release 0.23.46 (RUSTSEC-2026-0346 to -0349), which the required OSV scan on main reported after PR #1061 had passed. rustls is a direct dependency of tauri-plugin-updater 2.12.0, reqwest 0.12.28 and 0.13.4, hyper-rustls 0.27.9, tokio-rustls 0.26.4, quinn 0.11.9, quinn-proto 0.11.18 and rustls-platform-verifier 0.7.0 (version-exact cargo metadata resolve nodes); the lockfile-only update changes no manifest, feature, or toolchain pin. PR #1062.

  • R-15 Gate 4E: the PREPARE/DONE window (W1). ProtectedStorage::with_journal_locator hands the storage the locator of its journal (configuration only). With one, the ordinary operations (write, reconcile, list, read) over a bound root take one finite root event under the shared admission they already hold, authenticate the journal through the locator and are admitted only if its phase is PREPARE or DONE and its binding is exactly the binding of the snapshot root; writes and reconciles repeat that verdict right before their first root commit. Everything else keeps the E1 refusal (MigrationRequired) and writes nothing: no locator, a missing, misplaced, symlinked or wrong-scope location, a journal that does not authenticate, any other phase, a moved binding. A busy root event is the error Protected(RootBusy) to retry (never the pre-observation None); an admission fault stays an admission error; a prepared root left by an interrupted catalog commit is recovered by the write and the window is judged again. The journal owner's progress checkpoints hold no admission, so the ordinary catalog commits copy the committed binding forward and two tests land a checkpoint between two exact reads and a sweep lands one at every sixth read of a write: it is neither lost nor written over. Reads and lists follow the same two phases (decision recorded on GH #359). No producer of a binding, no E3 beyond the final capture, no Gate 5–7. Issue #359. PR #1061.

  • CI / Pages: admit the Pages record pruner's destructive writes. DESTRUCTIVE_WRITES_ADMITTED is true and the pruning job holds deployments: write, but only a scheduled run on main writes (at most ten records, workflow_dispatch stays a read-only dry run) and the workflow stays disabled until the operator enables it after a clean live dry run. funds() now treats a missing or malformed rate allowance as unfunded, so a response, or an error that ends a read, without a usable header cannot leave a stale budget behind, the reservation before the inactivation now includes the POST itself, and every deletion re-checks the budget after the inactivation settles and before the DELETE. A candidate that changed or stopped being eligible mid-run stops the run instead of being skipped. workflow_dispatch now runs a separate read-only plan job, so only the scheduled job holds deployments: write. The writer-fence section of the pruning contract describes what a delete can still reach (one round trip after the last revalidation, bounded to a record that was a candidate at that read) and the fail-closed list. Only positively proven own github-pages records are touched; the newest three, the current and rollback publication and the evidence holds are never candidates. Issue #1028. PR #1041.

  • R-15 Gate 4E: the final inventory capture is now exclusive by type. commit_final_inventory_capture and commit_final_streamed_inventory_capture publish the capture that runs in ADMIT (final_inventory_captured) and take the installation's exclusive admission by mutable borrow and the root from it, so a shared guard does not compile and no other root or retargeted alias can be given. The ordinary commit_inventory_capture and commit_streamed_inventory_capture now publish only the preliminary inventory and refuse the final one (FinalCaptureNotExclusive); the final commits refuse a preliminary one (NotAFinalCapture), each before the file system is touched. No window, no new producer of a binding. Issue #359. PR #1038.

  • R-15 Gate 4E: add the writer journal locator. JournalLocator names the journal directory of one installation (its scope and one directory name of a fixed grammar, no path, no search) and authenticate_journal resolves it under a root event of either admission mode: the locator's scope must be the anchor's, the location must be a real directory (not a symlink) strictly below the admitted installation and outside the authority root, opened as that entry itself without following a symlink, pinned and unchanged after the reads, and the committed root, binding, manifest and key route are authenticated by the existing keyless read. Any failure is a typed refusal that writes nothing and reads no other directory. The result borrows the root event and so ends with the admission. The directory pinning of the conversion gate moves into a shared placement module with unchanged behaviour. Nothing consumes the locator yet: the E1 barrier, the PREPARE/DONE windows and the E3 capture barrier are unchanged. Issue #359. PR #1037.

  • Documentation: correct the merge runbooks to the repository's branch-retention setting. A merge no longer deletes its head branch (delete_branch_on_merge is false since 2026-10-13), so the protected squash command drops --delete-branch and the dependent-PR check guards stacked PRs instead (no hosted CI off main, squash rewrites ancestry) rather than branch deletion. Deleting a merged branch becomes a manual step after a census of dependent PRs, evidence naming its tip and local worktrees. PR-CI-MERGE-WORKFLOW.md, the CodeAnt and DeepSource review loops and the worktree housekeeping guide no longer state that GitHub auto-deletes head branches. PR #1036.

  • R-15 Gate 4E: the recovery of a prepared root that changes the live-migration binding now needs the exclusive admission. recover_root refuses to complete such a root (typed, before any sync or write; the preparation stays) and the ordinary operation that ran it is refused with RecoveryPending; the new recover_root_exclusive completes it, and begin_conversion resolves a pending preparation through it before it reads the journal (it now takes the provider by mutable reference). A prepared root that keeps the binding still completes under a shared admission. The classification compares the authenticated committed and prepared roots; no journal locator, no E3 writer and no PREPARE/DONE window. Issue #359. PR #1035.

  • R-15 Gate 4E: a journal phase change is now published only by commit_phase_transition, which takes the installation's exclusive admission by type. The progress checkpoint, the binding advance and the journal-level checkpoint publish refuse a successor that changes the phase, and the transition refuses one that keeps it, each before a journal byte is written. The conversion driver's own phase changes use the new commit and behave as before. The raw root commit is no longer exported by the production crate, only with the test-only test-support feature, so it cannot be used to name a phase change. The capture, takeover and renewal writers, the writer journal locator and the PREPARE/DONE windows are unchanged and stay closed. Issue #359. PR #1033, which was closed unmerged: its six commits landed through PR #1035.

  • Operations: add a versioned Vercel Preview retention runner for the post-merge
    reconciliation. pnpm run vercel:retention:dry-run is read-only and
    pnpm run vercel:retention:reconcile runs the full pass, both without arguments. A pass
    proves the exact-main gates and Production, classifies every preview conservatively,
    re-reads the provider and revalidates each ID before deleting it, never retries a write,
    and ends with a post-proof. A mutating pass runs only from a source equal to the reviewed
    origin/main. Issue #627. PR #1032.

  • Operations: supersede the release-batched Vercel Preview retention record with
    ordinary per-merge retention. A dated Active mode section in the retention policy names
    the local Claude Code CLI session as the destructive writer under the maintainer decision
    recorded in GH #627, keeps the #1020 record as history, and requires every deletion to
    name one exact deployment ID revalidated against live data. AGENTS.md and the
    deployment, merge and Cursor Cloud runbooks point to it. PR #1031.

  • CI / Pages: reconcile owned deployment records safely, keeping the newest three
    plus current publication, rollback and evidence holds. Daily bounded reconciliation
    replaces broad CI-completion triggers; manual dispatch is read-only. Both publication
    slots are proven against the triggering run's actor before any candidate is classified,
    so a forged status can neither consume the rollback slot nor vouch for a record.
    Failed inactivation stops deletion, and every mutation revalidates the candidate, both
    slots and trusted-main authority within a measured per-run request budget.
    Destructive writes stay closed in code and in the workflow token until the owner admits a
    writer-fence protocol, so even an enabled schedule only reports. Issue #1028. PR #1029.

  • R-15 E2 preparation: lifecycle transitions authenticate the committed migration binding before catalog pages and refuse with RecoveryPending even when those pages are missing or corrupt. Existing root-error, locked-provider and unbound-catalog behavior is preserved. The contract records the owner-approved locator, phase-barrier and error-priority direction; PREPARE/DONE windows remain closed. PR #1027.

  • Security: update quinn-proto to the OSV-fixed release 0.11.18 (RUSTSEC-2026-0338 to -0342). The crate is a transitive dependency through tauri-plugin-http, reqwest 0.12 and quinn; the lockfile-only update changes no manifest, feature, or toolchain pin and unblocks the required OSV scan. PR #1030.

  • R-15 Gate 4E evidence: coordination-resource preservation checks inspect links without following them and reject nonempty files, FIFOs, sockets, and observation errors. Fixture-scoped regressions cover missing/empty resources and link variants. The ordinary-operation barrier is unchanged. Clean successor to PR #1021. PR #1026.

  • Docs: a release tag that is not an ancestor of the checked history now fails as one tag-boundary class instead of a list of disjoint subjects. Real undocumented subjects still fail after a correct ancestor tag. A history read that fails after that ancestor check fails closed instead of passing as empty history. PR #1023.

  • Operations: destructive Vercel Preview retention for source merges may now
    wait until the next sanctioned release reconciliation, owned by Codex CLI in
    the local VS Code/Ubuntu environment. Cursor Cloud still proves exact-main
    CI/CD, CodeQL, Production READY, canonical HTTP, and alias sanity after every
    merge, and it does not delete deployments or publish a release. Production,
    rollback, active-PR previews, and unknown metadata stay protected. Gate 7
    and the storage-authority switch stay maintainer-gated. Deferral is capped
    at 14 days and 24 non-protected Preview deployments; either cap, or provider
    resource pressure, requires one exceptional local Codex CLI pass and stops
    dependent repository mutation until that pass is recorded. That pass does
    not restore per-merge destructive cleanup. The cycle ends only after the
    sanctioned release pass. The mode is inert until this policy is on main.
    Introducing record: #1020.

  • R-15 Gate 4E: the ordinary-operation barrier of a bound root is now pinned with evidence. With an unlocked
    provider, a loadable catalog and no interrupted root commit, a committed root that binds a live migration already
    made ProtectedStorage refuse every ordinary operation (write, reconcile, list, read, lock, unlock, shutdown) in
    every phase, from the committed root and so across a crash. Eighteen new cases show it with the installation tree
    unchanged apart from the two coordination resources, a binding that names no journal, a cold start, a provider that
    starts locked, a lost catalog page, an interrupted root commit and unbound controls; the refusal codes in states
    they do not pin are stated as such. No behaviour changed. The barrier is stricter than the contract's PREPARE
    window, which stays a later step. PR #1019.

  • R-15 Gate 4D: every DurableFs adapter must now bound its reads. read_at_most and list_dir_at_most
    lose their default bodies (which loaded the whole file or directory before applying the limit) and become
    required trait methods, so an adapter cannot compile without stating how it bounds the allocation; a doctest
    triple pins the rule. Test doubles use two test-support-only helpers that keep their former behaviour.
    Behaviour of StdFs and of every journal read is unchanged. PR #1018.

  • R-15 Gate 4D: the conversion session iterates the inventory and leaves CONVERT (part c-2 of the
    conversion driver). ConversionSession::convert_next converts up to a batch of entries of the page the
    cursor points into through a caller-supplied, idempotent EntryStep, records the page-local cursor and
    reports More or Done; a step failure records no checkpoint (what the step had already converted is converted again, so it must be idempotent) and a restart resumes at the persisted cursor.
    finish_convert moves CONVERT to VERIFY only after the session saw the end of the last page. The
    caller owns the loop and the lease clock. PR #1017.

  • R-15 Gate 4D: the conversion session reads the inventory it converts and the cursor becomes page-local
    (part c-1 of the conversion driver). ConversionSession::verify_inventory authenticates the whole page set
    once against the committed root (keylessly, one page in memory at a time) and page reads one authenticated
    page; advance_cursor needs the verified set and refuses an entry index outside the selected page, where it
    used to check the total only. The contract now says what cursor_entry_index means: the index within its
    page of the next entry to process. The cursor-driven iteration is the next part. PR #1016.

  • R-15 Gate 4D: the conversion session enters CONVERT and moves the checkpoint cursor (part b-2 of the
    owner's steps through the gate). ConversionSession::enter_convert (idempotent: a session already in
    CONVERT writes nothing) and advance_cursor (forward only, inside the inventory, in CONVERT only) share
    one private step with renew_lease, which now commits through commit_lease_renewal_held or the new
    commit_journal_checkpoint_held, and every outcome after a landed commit is followed by the admission
    check. The cursor-driven iteration is the next part. PR #1015.

  • R-15 Gate 4D: the conversion session renews its lease (part b-1 of the owner's steps through the
    gate). ConversionSession::renew_lease builds the renewal from the session's own snapshot (renewed_lease),
    takes the root event through the held admission, commits it with the key route and active epoch the
    committed root names, reads the committed journal back under that root event and returns the commit; it is
    the first caller of the D3b renewal. The journal directory must lie below the admitted installation and is
    pinned for the session, and every file operation goes through canonical paths; the root settles what a
    step did, a failed step keeps the snapshot, a retry adopts what was written, a differing candidate of a
    crashed attempt is moved aside, and an unsettled step spends the session. Entering
    CONVERT and moving the cursor are the next part. PR #1014.

  • R-15 Gate 4D: the entry gate of the exclusive conversion driver (part a). begin_conversion takes the
    installation's exclusive admission by mutable borrow, derives the root layout from the scope that admission
    was acquired for, re-reads the committed binding, the registry-routed journal key and the root-named
    manifest, and returns a ConversionSession only for the owner of the lease while the operation is in ADMIT
    or CONVERT with the final inventory captured. It reads no clock, holds no key and writes nothing; moving
    the journal under the gate is the next part. PR #1013.

  • R-15 Gate 4D: stage a streamed inventory without handling a journal key (stage two, part b, of the
    streaming capture). begin_streamed_capture routes the journal key from the authenticated root and
    registry, reads the binding and the committed manifest from the root and returns a StagingSession that
    stages page by page under that key and one journal directory value; commit_streamed_inventory_capture no
    longer takes a committed manifest or a journal directory, promoting into the directory the capture was
    staged in and loading the manifest from the root under the lock. PR #1012.

  • R-15 Gate 4D: commit a staged inventory (stage two, part a, of the streaming capture).
    commit_streamed_inventory_capture promotes the pages that StreamedCapture staged into the digest
    directory of their successor one page at a time (each page confirmed against its reference, its entries
    absorbed into the inventory digest, an identical page already there adopted), publishes the successor and
    advances the root binding, in the order and under the guarantees of commit_inventory_capture, which now
    shares one core with it. The staged files are removed only after the root has committed, so a retry after a
    failure still has them; a staged page found wrong stops the commit before any manifest is published. The
    key-routing staging session is the next part. PR #1011.

  • R-15 Gate 4D: stage a captured inventory one page at a time (stage one of the streaming capture).
    StreamedCapture takes the entries page by page, assigns the page index and generation, checks that they
    are valid and strictly ascending across pages, seals each page once and stages its envelope in a private
    inventory/pending-* directory (never authority, never read by a reader), keeping one page and one
    reference per page in memory; the total is announced up front because the inventory digest commits to it
    first. finish builds the successor with the same function as capture_inventory, and
    load_staged_page confirms a staged page against its reference, a missing one being a broken attempt and
    not a recovery state of the journal. A capture is bound to its journal directory and to the key that opened
    its manifest, and a failure the caller can handle removes the pages staged so far. Promotion under the root
    lock and the composed commit are the next stage. PR #1010.

  • R-15 Gate 4D: the journal owner can renew its own lease (maintainer decision C, the wiring half).
    commit_lease_renewal publishes a manifest that moves the lease expiry strictly forward and changes nothing
    else (publish_renewal_fenced, a publish of its own so the ordinary checkpoint keeps refusing lease changes)
    and advances the root binding to it as a renewal, under the root lock and the routed journal key, in the
    takeover's order. It reads no clock: the fence settles whether a lapsed lease may still be renewed. A stale or
    foreign owner, an expiry that does not move forward and any other changed field are refused before a journal
    write; a retry after a failed root commit adopts its own identical candidate. PR #1009.

  • R-15 Gate 4D: constrain the cursor and the lease across successive journal manifests (maintainer decision C).
    The successor relation now requires a forward phase change to enter the new phase at cursor (0, 0)
    (CursorNotReset; transition_phase produces it), entering RECOVERY_REQUIRED to keep the last cursor, and
    every ordinary successor (checkpoint, phase change, recovery entry, inventory capture) to leave the lease
    untouched, so the owner and the fence change only by takeover. assert_renewal_successor is the predicate of
    the separate same-owner renewal (the expiry moves strictly forward, nothing else changes); wiring it into a
    journal-owner operation is the next slice. A lease can no longer appear through a checkpoint: its first owner
    is the bootstrap manifest or a takeover. PR #1008.

  • R-15 Gate 4D: state and enforce how a journal's target epoch relates to its source. The contract
    (Β§10.1.1, Β§10.4): a rotation's target_epoch is above its source_epoch (Β§8.3 item 2), and an envelope or
    schema migration keeps the key epoch (target_epoch == source_epoch; creating a newer epoch, with the durable
    target verifier that needs, is a rotation). journal_envelope_epoch, which the manifest validation already
    calls, enforces it, so a ROTATE from 5 to 3 or from 2 to 2 and an ENVELOPE_MIGRATION from 2 to 3 or
    from 3 to 2 neither encode nor decode (InvalidCounter). This closes the pre-caller
    validation criterion before any caller writes a rotation journal. PR #1007.

  • R-15 Gate 4D: use the key route in the journal-owner operations and drop the caller-supplied journal key.
    commit_journal_checkpoint, commit_journal_takeover, commit_inventory_capture and advance_live_migration
    resolve the journal key through resolve_journal_key while they hold the root lock, right after the committed
    binding is read and before any journal write; JournalSource is now { dir, operation }. An epoch that cannot
    be routed (Revoked, unregistered, not the committed registry) refuses all four operations
    (AuthorityError::JournalRoute) with nothing written, and a route to a key the journal was not sealed under is
    refused by the authenticated manifest load. This closes the D2b criteria (source-key retention, manifest
    preflight, registry route and its wiring). PR #1006.

  • R-15 Gate 4D: add the registry-resolved key route of a bound migration journal. resolve_journal_key
    reads the root-named manifest generation, requires its digest to match the committed binding, takes the
    header epoch the root vouches for, looks that epoch up in the authenticated key-epoch registry
    (Prepared, Active and RetiredRecoveryOnly are usable; Revoked or absent fail closed) and resolves the
    record's route to the key. The binding and the registry are read from the committed root inside the call
    (the registry must hash to the root's key_epoch_set_digest, so a removed or uncommitted generation is
    refused), never taken from the caller. It never reads the root's active epoch to choose the key, so a
    journal still bound after cutover resolves under its source epoch, and it refuses before any mutation. MemoryKeyProvider::import_epoch_key
    gives test fixtures a key of known material. Nothing calls the route yet; wiring it into the composed
    journal operations and dropping the caller-supplied key is the next slice. PR #1005.

  • R-15 Gate 4D: compare a manifest's journal envelope epoch with trusted authority before the journal key is
    used. JournalManifest::open takes the epoch the caller trusts (ManifestRead) and refuses a different header
    epoch as KeyEpochMismatch before any key use, instead of as an authentication failure afterwards; the epoch
    derived from the authenticated body must still agree. The promote readback and candidate adoption pass the epoch
    of the manifest they hold, load_manifest_generation takes an explicit expected epoch, and
    load_authoritative_manifest judges the exact bytes against the root-bound digest before opening them and takes
    the epoch from the header the root vouches for. A bad root-named file now reports the authority mismatch. The
    registry-resolved journal key route stays a later slice. PR #1004.

  • R-15 Gate 4D: refuse revoking a key epoch while a live migration is bound. write_key_epoch reads the
    authenticated committed root under the root_commit_mutex it already holds and refuses a Revoked
    generation (RootStoreError::RevocationWhileMigrationBound) before creating anything when the root binds a
    live migration, so the epoch the bound journal is sealed under stays resolvable until the binding is cleared.
    The rule covers every new revocation because the binding does not name the journal's epoch; other statuses,
    and any status without a bound migration, are unaffected. The registry-resolved journal key route and the
    manifest pre-key epoch comparison stay later slices. PR #1003.

  • R-15 Gate 4D: seal the migration journal under an operation-stable key epoch instead of a hard-coded
    1. journal_envelope_epoch derives it from the authenticated manifest: ENABLE uses the target epoch
    (and must be exactly the first-time 0 β†’ 1 of the contract), ROTATE and ENVELOPE_MIGRATION the source
    epoch, in every phase, so the epoch no longer depends on the authority root's active epoch that moves at
    cutover. The manifest codec, the durable promotion, the page store and the page reader all use that one
    derivation and refuse a manifest or page sealed under another epoch (KeyEpochMismatch) before a byte is
    written or accepted; the reader and the page store compare the epoch before the key is used, and the single-page
    promote seals only for the manifest the root names, so a same-owner copy with other epochs cannot choose it. Registry resolution of the journal key,
    the refusal to revoke the source epoch of a bound journal and the lease/cursor phase rules stay later
    slices. PR #1002.

  • R-15 Gate 4D: persist the proof of the final inventory capture. The journal manifest gains an
    authenticated final_inventory_captured flag (one strict byte after journal_page_set_digest):
    false before ADMIT, set only by the capture that runs inside ADMIT in the same successor that
    binds the final inventory digests, never cleared, required from CONVERT on. ADMIT β†’ CONVERT
    is refused without it (FinalInventoryNotCaptured) by the transition constructor, the successor
    relation and the root, a capture is refused once the final inventory is captured, and a progress
    checkpoint, phase transition or takeover cannot change it. After a crash a journal can therefore
    tell a preliminary inventory carried forward from a final one captured behind the write barrier.
    The unreleased manifest format is amended in place (no release contains the codec, the crate is
    not a dependency of the desktop app), so the format version stays 1. The conversion driver, the
    journal envelope epoch and the lease/cursor phase rules stay later slices. PR #1001.

  • R-15 Gate 4D: add the composed capture commit. commit_inventory_capture runs under one
    root_commit_mutex: it stores the captured page set, publishes the capture manifest and advances
    the root binding as a capture, so the manifest is never written before its pages are durable and
    the root never names a manifest whose pages are missing; a retry after a failure adopts what is
    durable and writes only what is missing. The root now accepts a change of the inventory fields
    only through a capture (assert_capture_successor) and requires every other checkpoint,
    including advance_live_migration, to leave them alone (assert_progress_successor,
    FrozenFieldChanged); before this the successor relation froze them only from CONVERT.
    Inheriting unchanged pages, streaming capture and conversion stay later slices. PR #1000.

  • R-15 Gate 4D: add the verified reader of a stored inventory page set. verify_stored_inventory
    loads the root-named manifest itself, resolves each page's generation from its digest-keyed
    directory only as a hint, listed with a bound (DurableFs::list_dir_at_most; a missing page, a
    missing directory or mixed generations is RecoveryRequired), reads each page bounded, opens it as its own identity at the journal key
    epoch, streams the inventory digest one page at a time and keeps only the page references, then
    confirms the page-set and inventory digests against the manifest. load_inventory_page reads one
    page by its authenticated reference and refuses a file that changed after verification. The
    reader creates nothing and takes no lock. Inheriting unchanged pages in the store, the
    semantic-open recovery mapping and the composed capture commit stay later slices. PR #999.

  • R-15 Gate 4D: add the inventory page store. The pages of a captured inventory live under a
    directory keyed by the manifest's authenticated page-set digest
    (inventory/<hex digest>/page-<index>/), so a discarded capture attempt can never be mistaken for
    the page set a manifest names and no directory listing is authoritative. seal_inventory_pages
    seals each page once, and promote_inventory_set_fenced writes the whole captured set for the
    committed owner only. The committed manifest must be the generation the root binding names (read
    back and compared), and the set is first proven against the manifest successor: the successor is
    a valid, encodable capture of the open inventory, the pages are non-empty, indexed 0..n and
    carry exactly the new revision as their generation (every page is rewritten until a verified
    reader can prove an unchanged page against the predecessor's page references), the page-set and
    inventory digests match, and each envelope opens as its own page under the journal key epoch.
    Nothing is created when the set is refused. The directory chain up to the journal directory is
    synced, a failed sync being reported as already promoted, and a retry of a set that failed
    partway adopts the identical pages already on disk and continues with the first missing one.
    Reading the pages back, the composed commit under the root lock and a streaming capture for very
    large inventories stay later slices. PR #998.

  • R-15 Gate 4D: add capture_inventory, the manifest-side constructor of the Β§10.3 inventory
    capture. From sealed pages it builds the successor that carries the page set, the entry count and
    the inventory digest, in DISCOVER (preliminary) or ADMIT (final) while no conversion has run
    (not in PREPARE, where ordinary writes are still admitted). Pages must be indexed from 0 without gap or duplicate, hold globally ascending valid
    entries and carry a generation no newer than the new revision, so an unchanged page keeps the
    generation that names it; the result is re-verified and checked as a valid successor. Pure: no
    I/O. The physical page layout, promotion and reading, and the conversion itself, stay later
    slices. PR #997.

  • R-15 Gate 4D: the composed journal commits can replace a stale candidate. The new
    CandidateConflict policy (default Refuse, the behaviour so far) lets commit_journal_checkpoint
    and commit_journal_takeover move a durable next-revision candidate that differs from their
    manifest (a different manifest, an unopenable file or an oversized one) aside with its bytes
    preserved, then publish their own, after every authority, lease and successor check; an identical
    candidate is still adopted. This resolves the takeover liveness limit: a new claim after the first
    claim's lease expired is no longer refused forever. Safe because reaching the publish proves that
    no prepared root names the candidate. PR #996.

  • R-15 Gate 4D: add commit_journal_takeover, the owner takeover (Β§10.1). Under one root lock
    it reads the committed binding, publishes a new owner's claim and advances the binding to the new
    fencing generation. The committed lease must be expired at the caller-supplied clock (or absent),
    and the claim carries fence + 1 and revision + 1, holds a lease that outlives now, and changes
    ownership only: the phase, cursor, inventory, target key and recovery reason stay the committed
    ones. A retry after a failed root commit adopts its own identical claim. The commit evidence
    records the new fence, so the former owner's checkpoint is refused afterwards. Nothing calls it
    yet; progress by the new owner is an ordinary checkpoint, and bind, clear and the discard half of
    R2B stay later slices. PR #995.

  • R-15 Gate 4D: the journal checkpoint publishes only a valid successor of the manifest the root
    names. publish_manifest_fenced loads that generation (exact path, bounded, authenticated against
    the binding digest) and assert_manifest_successor refuses, before any read of the candidate or
    any write, a manifest that skips or repeats a revision, changes the operation, type, epochs or
    fence, jumps or reverses a phase, leaves a terminal phase, regresses the cursor within a phase,
    rewrites the target key on or after ADMIT or the inventory on or after CONVERT, or changes the recovery
    reason other than by entering recovery, or carries a cursor outside its own inventory.
    advance_live_migration enforces the same relation, so a generation promoted directly cannot
    become authoritative. The journal manifest loaders now use the bounded read as well. Lease fields and the cursor across a phase change stay unconstrained until their slices.
    PR #994.

  • R-15 Gate 4B operations integration harness: gate4b_operations_test no longer assumes a
    nonblocking storage operation resolves immediately after a guard is released. Ok(None) is legal
    until a forked probe child execs and closes its inherited flock descriptor, so the lock,
    unlock, shutdown, snapshot and reconcile calls that expect a result now go through a bounded
    poll_admitted helper, while the tests that assert Ok(None) because they hold admission still
    call the operation directly. Closes the recurring both_root_contention_windows_preserve_intents_and_refuse_clean_drain flake, the same
    family as the write helper from PR #962. PR #993.

  • R-15 Gate 4D: commit_journal_checkpoint now adopts an identical candidate on retry. After a
    failed root commit the new manifest revision stays durable as an unadopted candidate; the retry
    reads that exact generation under the same fence and authority check and, if it authenticates,
    carries the key epoch a promote pins and decodes to the manifest being published, advances the
    binding to its digest without writing a journal byte or leaving staging residue. A different,
    unopenable or oversized candidate is refused as GenerationExists and left untouched; the
    candidate is read through the new bounded DurableFs::read_at_most, so a crafted file cannot
    exhaust memory. Discarding such a candidate, a successor-relation guard,
    bind, clear and takeover stay later slices. PR #992.

  • R-15 Gate 4D: add commit_journal_checkpoint, the journal owner's checkpoint. Under one root
    lock it reads the committed live-migration binding from the root, publishes the owner's next
    manifest revision against that binding (so a stale owner is refused before any journal write) and
    advances the binding to the published generation. A key route or epoch change, a missing binding
    and a wrong manifest or fence write nothing; a failure after the publish leaves the new revision
    as an unadopted candidate and the root naming the previous one. Nothing calls it yet; adopting or
    discarding a candidate on retry (R2B), bind, clear and takeover stay later slices. PR #991.

  • R-15 Gate 4D: add advance_live_migration, which commits a root whose live-migration
    binding is the journal owner's next revision. Under the root lock it requires the advance to be
    the CAS successor of the committed binding (same operation and fence, revision + 1) and the
    named manifest generation to be durable, authenticated and equal to the binding's digest, and it
    syncs the journal directory before the root is published. It writes no journal byte and nothing
    in the crate calls it yet; bind, clear, takeover and the promote-plus-advance coordinator stay
    later slices. PR #989.

  • Dependencies: raise the sharp override floor from 0.35.4 to 0.35.5 for GHSA-wq5f-xc86-pv6w
    (a librsvg memory-safety vulnerability that OSV published on 2026-10-06 and that fails the
    Security Audit on every PR) and cap it below 1.x. The fix has been on npm since 2026-09-27, so
    the 7-day release-age floor is already met. The change is the pnpm-workspace.yaml override plus
    the lockfile: sharp and its @img/sharp-* / libvips binaries move to 0.35.5 / 1.3.4 for
    @huggingface/transformers and miniflare; no package.json or source change. PR #990.

  • R-15 Gate 4D: refuse a stale caller's journal promote before any I/O.
    promote_manifest_fenced and promote_page_fenced now take the root's committed
    LiveMigration and admit only that binding's owner at its next manifest revision, or its
    current manifest for a page. A durable r+1 does not make r+2 admissible. No root read or
    write, no deletion, no key_epoch change; a stale copy of the binding and the cross-process
    CAS stay open. PR #988.

  • Dependencies: align @ai-sdk/google 4.0.76β†’4.0.85 and @ai-sdk/openai 4.0.53β†’4.0.80
    on one shared floor, @ai-sdk/provider 4.0.19 and @ai-sdk/provider-utils 5.0.51.
    undici stays 7.29.1, which already satisfies ^7.29.0 and the workspace override
    >=7.29.1 <8. @ai-sdk/react, ai, and @ai-sdk/gateway keep their existing pins.
    Supersedes Dependabot #979 and #981. PR #987.

  • R-15 Gate 4D: resume from the journal manifest generation named by the committed
    root LiveMigration. A newer durable generation is retained and not adopted.
    Only a missing copy of that exact generation is recovery-required. Root binding
    advance, R2B, and R4 stay later slices. PR #967.

  • Dependencies: move worldscript-secure-storage OS entropy from getrandom 0.2.17 to 0.3.4.
    The 0.3 line renames that call to fill. An entropy-source failure still maps to
    RandomnessUnavailable, with no fallback PRNG. Rust 1.77.2 and the other RustCrypto
    crates stay unchanged. Supersedes Dependabot #883.

  • Dependencies: pair @tauri-apps/plugin-updater and tauri-plugin-updater on 2.12.0.
    Updater 2.13 requires Rust 1.90, above the desktop crate's Rust 1.77.2, so the
    one-sided npm 2.13 bump is not the admitted pair. Node runtime, updater pubkey,
    and endpoints are unchanged. PR #901.

  • Dependencies: bump devDependency @types/node from 25.9.2 to 26.6.3.
    Runtime engines.node stays >=22.19.0. Lockfile updates are the types
    package and its peer-resolution keys. Supersedes Dependabot #891. PR #964.

  • R-15 Gate 4B operations harness: nonblocking admission that must become
    available after a holder is released now uses a bounded retry. Direct
    try_acquire remains where the test proves the guard is unavailable now.
    Production admission semantics are unchanged. PR #963.

  • CI: pin pnpm/setup v3.0.0 in .github/actions/setup/action.yml and the
    workflow-policy job, with node-version-file: false so v3 does not install Node
    from .nvmrc ahead of actions/setup-node. The base-owned
    reviewer-governance-trust.yml stays on the admitted v2.1.0 pin until a
    protected-transition predecessor authorizes the exact byte change. Supersedes
    the incomplete Dependabot pin in #893. PR #963.

  • Vercel retention: ordinary mode still requires a full authenticated Preview
    reconciliation after every successful merge to main before dependent mutation.
    An explicitly declared bounded dependency/toolchain maintenance train may defer
    only Preview artifact housekeeping, while exact-main CI/CD, CodeQL, Production
    READY, canonical HTTP, and alias/promotion/rollback checks stay mandatory after
    every train merge. The train is fail-closed at 12 merges or 24 hours measured
    from the UTC instant MERGE_1 is recorded, and one
    full reconciliation is required before non-train mutation resumes. The
    introducing transition lets #963 be that train's first merge when the durable
    record is complete before merge; a later pull request cannot reuse the
    bootstrap, and a failed resulting-main gate returns the train to ordinary
    reconciliation. PR #963.

  • R-15 Gate 4B operations integration harness: poll nonblocking admitted writes (write_poll /
    write_until_admitted) so legal shared-admission Ok(None) is not treated as a committed write in
    gate4b_operations_test. PR #962.

  • R-15 Gate 4D B1 with_fence mutex proof closure (#359 / #922): add deterministic unit test
    with_fence_holds_mutex_during_closure so mutex retention is proven through the public fenced entrypoint,
    not only via JournalDurableGuard acquisition. PR #961.

  • R-15 Gate 4D slice B1 journal durable API hardening (#359 / #922): crate-private unfenced
    promote_manifest / promote_page; public mutation only through promote_manifest_fenced and
    promote_page_fenced; deterministic with_fence exclusion test; post-promotion error provenance
    documented. PR #960.

  • R-15 Gate 4D slice B journal durable promotion (#359 / #922): headless Core now durably stages and
    promotes migration journal manifest and page generations via pre-sealed envelopes (including bootstrap
    revision 0 through JournalManifest::seal without re-entering seal_record), plus an in-process
    with_fence mutex boundary that rejects stale fence tokens before I/O. Root LiveMigration binding
    updates and restart reconciliation remain deferred (Slice B2). Production authority is unchanged.
    PR #959.

  • R-15 Gate 4D migration execution state (#957 / #359 / #922): headless Core now implements Β§10.3
    journal phase ordering, fence/revision checks, authoritative manifest revision selection against
    the root live-migration binding, checkpoint revision bumps, and terminal recovery/done transitions.
    Durable journal I/O, record conversion, and root binding updates remain later 4D slices. Production
    authority is unchanged. PR #957.

  • R-15 Gate 4C journal codec (#445 / #922): headless Core now implements the Β§10.1 migration
    journal manifest body, inventory_digest, journal_page_set_digest, canonical paged inventory
    page bodies, and manifest/page envelope seal-open helpers with fail-closed malformed-input refusal.
    No durable journal I/O, live-migration root binding, or enable/rotate state machine is included.
    Production authority is unchanged. PR #956.

  • Cursor Cloud Agent tooling (QNB-193): document Cloud bootstrap, PR/review lifecycle, and
    resulting-main verification semantics; add pnpm run cursor:cloud-doctor for non-secret Git vs
    gh diagnostics. PR #955.

  • R-15 Gate 4B read/snapshot admission (#445): shared admission now covers authenticated
    snapshot/key capture and the complete protected-read handoff; retained views authenticate their
    exact immutable epoch evidence. Locked, migrating and misrouted reads fail closed without legacy
    plaintext fallback. This supersedes the read half of #952 only; mutation/root-recovery/lifecycle
    closure still owns #360. Production authority is unchanged. PR #953.

  • R-15 Gate 4B mutation, root-recovery and lifecycle (#445): protected writes, lock/unlock,
    shutdown, prepared-root step-F recovery, and namespace-scoped snapshot retention now share the
    same admission ordering as read/snapshot capture; Successor A session-binding and rebind semantics
    are preserved. This supersedes the mutation half of frozen #952 only. Production authority is
    unchanged. PR #954.

  • R-15 Gate 4B foundation β€” shared operations and exclusive transitions (#445): the headless
    core now has kernel-backed admission across processes, automatic crash release and typed guards
    that bind the installation and authority-root directories. Root events stay beneath the same
    admission. Protected-operation and lock/unlock integration follow in the next 4B slice; #360
    remains open. No production authority switch is enabled. PR #951.

  • R-15 Gate 4, slice 4A β€” one root writer at a time, across processes (#445): every change to
    the protected-storage root of trust now runs under an operating-system lock, so two app windows
    or processes can never commit from the same starting point, and a process that crashes while
    holding it never leaves it stuck. Nothing reads or writes user data through it yet. PR #950.

  • R-15 Gate 3 complete β€” crash-durable protected storage core (#445): the protected-storage
    core now covers everything Gate 3 requires β€” synced files, never-overwritten generations, synced
    directories where the platform supports it (Windows reports them as not confirmed), startup
    recovery and fault-injection tests on Linux, macOS and Windows β€” for ordinary
    records, and refuses any other record type before writing. What remains is assigned to later gates
    (asset pairs, power-loss qualification, record deletion, and switching the app over). Nothing
    reads or writes user data through it yet. PR #949.

  • R-15 Gate 3, slice 3C part 3c-2b β€” protected writes through the root of trust (#445): a
    protected write now commits each step β€” the intent to write and the finished write β€” through a
    new root of trust, and is reported as durably committed only after the second one. Reads serve
    only the version the root names, an interrupted write is finished or undone on startup from
    authenticated evidence, and an interrupted first write disappears from the record list. Nothing
    reads or writes user data through it yet. PR #947.

  • R-15 Gate 3, slice 3C part 3c-2a β€” the stored record catalog (#445): the protected-storage
    core now stores the record catalog pages next to the root of trust, commits catalog changes
    together with a new root, and lists stored records only from pages that match what that root
    names β€” a tampered, missing or replayed page stops instead of being trusted, and pages left by an
    interrupted change are set aside. Nothing reads or writes user data through it yet. PR #946.

  • R-15 Gate 3, slice 3C part 3c-1 β€” key epochs checked against the root (#445): the
    protected-storage core now stores its key-epoch records and, both when committing a new root of
    trust and when starting up, requires that the root names exactly those records and that its
    active key epoch is the one bound to the key it is protected with. PR #945.

  • R-15 Gate 3, slice 3C part 3b β€” committing the root of trust (#445): the protected-storage
    core can now commit a new root of trust in the order the storage contract requires, finish or
    cleanly abandon a commit that was interrupted at any step, and on startup trust only the root the
    secure key store names β€” repairing a stale pointer instead of following it. Nothing reads or
    writes user data through it yet. PR #944.

  • R-15 Gate 3, slice 3C part 3a β€” root and key-epoch records (#445): the storage contract now
    fixes the exact byte formats for the stored root of trust, the small pointer to it, and the key
    epoch records. The protected-storage core can seal, strictly check and open the root and key-epoch
    records, and encode and strictly check the (unencrypted) pointer. Nothing is
    committed through them yet; the commit sequence and its crash recovery follow. PR #943.

  • R-15 Gate 3, slice 3C part 2 β€” record catalog pages (#445): the storage contract now fixes
    how the authenticated record catalog is split into pages, and the protected-storage core can build,
    strictly check and seal those pages. Listing stored records will rely on this catalog instead of
    trusting folder contents. Nothing reads or writes user data through it yet. PR #942.

  • R-15 Gate 3, slice 3C part 1 β€” authority-root digests (#445): the protected-storage core
    can now compute the digests that tie the whole committed storage state together β€” every record's
    current commit marker, the record catalog, the key epochs and who committed the state β€” exactly as
    the storage contract specifies, with independently checked test vectors. Nothing is persisted or
    committed through it yet; the catalog and the root commit follow. PR #941.

  • R-15 Gate 3, slice 3B part 2 β€” commit protocol and startup recovery (#445): the
    protected-storage core can now commit a new version of a record through its commit markers and
    recover deterministically at startup after an interrupted write: it finishes the write when the
    new version is intact and provably belongs to it; when the evidence shows it is not, it keeps the
    previous version (or, for a record's very first write, leaves no version) and sets the rejected
    bytes aside without deleting them; and when a read or sync fails, it decides nothing until retried. Nothing reads or writes user data through it yet, and the final
    authority-root commit follows in slice 3C. PR #940.

  • R-15 Gate 3, slice 3B part 1 β€” commit-marker codec (#445): the protected-storage core can
    now encode, strictly decode and seal the commit marker that records which version of a record is
    current, exactly as the storage contract specifies, with byte-level test vectors. Nothing reads
    or writes user data through it yet; the commit write protocol and startup recovery follow in the
    next slice. PR #937.

  • Security: basic-ftp override floor raised (#938): a fresh OSV finding (GHSA-c475-qrg2-pj4r,
    fixed in basic-ftp 6.2.1) in the Lighthouse CI tooling is resolved by raising the dependency
    floor. It affects development tooling only, not the shipped app. PR #939.

  • Docs truth: stricter R-15 gate status check (#935): each line in the status block must now be
    a single gate entry, and the block ends only at a closing fence on its own line. Code examples
    containing a literal comment marker no longer hide the text after them, comments spanning lines no
    longer split a phrase, lists such as "Gates 4 and 3 slice 3B" are checked item by item, and an entry
    with several problems reports each of them. PR #936.

  • Docs truth: R-15 gate status is now checked automatically (#933): the storage contract carries
    one machine-readable status line per encryption gate, and the documentation check fails when the
    migration ledger disagrees with it or when current text calls an already-implemented gate "not
    admitted", a contradiction reviewers had to catch by hand in four consecutive pull requests. The
    README test-metrics synchronizer also covers one more line it previously missed. PR #934.

  • R-15 Gate 3, slice 3A β€” durable staging and promotion (#445): the protected-storage core can
    now write a new encrypted version of a record next to the previous one: it seals the record in
    memory, writes only the encrypted bytes to a temporary staging file, flushes it to disk, checks
    it, then links it into place under a new version name without ever replacing an existing
    version, checks it again, and flushes the folder on Linux and macOS (on Windows the folder flush
    is reported as unconfirmed rather than assumed). The previous version is never touched and no
    plaintext is written; every failed step is reported with the files it left, and a staging file
    that fails its check is kept for later recovery. Tests inject failures at each file-system step
    on Linux, macOS and Windows; real power-loss evidence and deciding which version counts come
    later. Nothing in the app uses this yet, and the production authority switch stays off. PR #930.

  • R-15 Gate 2 closure β€” record-class disposition (#445): every protected record class now has
    exactly one recorded disposition from the storage contract, and the record codec only seals and
    opens classes that are meant to become protected records. API credentials and the browser
    storage key-derivation salt and passphrase check keep their own separate protection and are
    refused, so they never end up in an ordinary protected record; a class without a recorded
    disposition is refused too. Nothing in the app uses this yet, and the production authority
    switch stays off. PR #929.

  • R-15 Gate 2, slice A β€” identity-bound record codec (#445): a protected record can now be
    sealed and opened through its typed identity, so its authenticated data always comes from the
    contract identity rather than hand-assembled fields. Moving encrypted data to another record,
    project, installation or record class, changing any authenticated byte, or using a wrong key is
    refused, and malformed or legacy bytes are rejected before decryption. Asset pairs derive their
    members from the same ID parts, keeping IDs that contain : exact. Nothing in the app uses this
    yet, and the production authority switch stays off. PR #928.

  • R-15 Gate 2, slice 1 β€” typed record-identity registry (#445): every protected record class
    now has a typed identity built only from that class's registered template, so a record addressed
    through it always carries the contract's logical ID and project binding in the authenticated
    data. Existing project and record IDs keep their exact spelling even when they contain :,
    and an index version must be one the current format supports. Malformed parts (an empty part,
    control characters, a non-canonical number, or a malformed installation ID) are refused, never
    repaired. Tests prove that data sealed under one identity never opens under another. Nothing in
    the app uses this yet, and the production authority switch stays off. PR #917.

  • Security: axios and DOMPurify override floors raised (#918): fresh OSV findings on the
    locked versions are fixed. The development-only axios (pulled in by wait-on) now resolves to
    1.20.0 for 12 advisories on 1.19.0, and the shipped HTML sanitizer DOMPurify moves to 3.4.16
    for GHSA-p98j-92pf-mc4p. Both fixed releases are older than the 7-day release-age window, so no
    exception was added, and no other dependency changes. PR #919.

  • R-15 Gate 1b-platform, slice D β€” OS secure-store adapter (#445): behind the opt-in
    platform-keystore feature, the protected-storage core can now keep its anchor, key index and
    keys in the real operating-system store: the macOS Keychain, the Windows Credential Manager, and
    on Linux the Secret Service only (never the non-persistent kernel keyring, and never a silent
    in-memory fallback). A missing, locked or unreachable store refuses protected mode instead of
    using a weaker store. A new CI job runs the full key lifecycle against the real store on Linux,
    macOS and Windows, proves the refusal on Linux without a Secret Service, and builds at the minimum
    Rust version; its Linux helper keeps the throwaway keyring in a private temporary home. Nothing in
    the app uses this yet, and the production authority switch stays off. PR #916.

  • R-15 Gate 1b-platform, slice C2 β€” durable anchor transitions and the full key provider
    (#445):
    the secure-store authority now applies the two-phase root-anchor transitions
    (prepare, commit, abort) as one validated read, the exact pure transition, one replacement and an
    exact read-back. Prepare only accepts a root key the store issued and can resolve; an exact
    commit replay and an abort with nothing prepared write nothing; conflicts and refused writes leave
    the stored anchor unchanged. SecureStoreRuntime implements the complete KeyProvider: after its
    own confirmed commit of a root it prepared itself an unlocked session rebinds to it, while a root
    committed elsewhere (even one that keeps the same key, or one the session merely replays) still
    needs a new unlock, a changed root whose key
    is missing is Locked for a stale session rather than KeyLost, and an epoch key another writer
    provisioned is not adopted without unlock. No platform adapter, app wiring, or production authority
    switch is included. PR #915.

  • R-15 Gate 1b-platform, slice C1 β€” runtime key handles (#445): SecureStoreRuntime adds the
    runtime half of the key-provider contract over the durable authority from slice B. unlock loads
    key handles only after a complete authority read and grants nothing if that read fails, the key
    index changes meanwhile, or the final state check reports a lost or inconsistent root.
    resolve/resolve_ref answer Locked while locked, resolve only a route the current index
    issues (never a search), and check the stored item on every call, so a replaced key is
    RecoveryRequired and a deleted one KeyLost, even if it was already missing at unlock. Every
    resolution is refused once the committed root key is lost or replaced, while a lost key of a
    merely prepared root transition is not reported as root loss; an unlock is bound to the exact
    committed root it saw, so an anchor that loses that root is a rollback (RecoveryRequired) and
    one that names another root needs a new unlock (Locked). lock drops and zeroizes every
    handle. It only reads the secure store: no anchor transitions, platform adapter, app wiring, or
    production authority switch. PR #914.

  • Post-release v1.29.1 truth sync: removed the release-candidate markers from README.md and
    CHANGELOG.md now that the v1.29.1 tag and GitHub Release are published, and recorded the real
    release evidence in AUDIT.md, TODO.md and docs/RELEASE-V1.29.1-EVIDENCE.md (see ## [1.29.1]
    below and #872).