Skip to content

lib: avoid excluding symlinks in recursive fs.readdir with filetypes - #55714

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
juanarbol:juan/fs-readdir
Nov 25, 2024
Merged

nodejs-github-bot merged 1 commit into
nodejs:mainfrom
juanarbol:juan/fs-readdir

Conversation

@juanarbol

Copy link
Copy Markdown
Member

Fixes: #52663

@nodejs-github-bot nodejs-github-bot added fs Issues and PRs related to file-system APIs and the fs module. needs-ci PRs that need a full CI run. labels Nov 4, 2024
@juanarbol juanarbol added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 4, 2024
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 4, 2024
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codecov

codecov Bot commented Nov 4, 2024

Copy link
Copy Markdown

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 88.42%. Comparing base (6af5c4e) to head (a8a6aa0).
Report is 827 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #55714      +/-   ##
==========================================
+ Coverage   88.41%   88.42%   +0.01%     
==========================================
  Files         654      654              
  Lines      187757   187863     +106     
  Branches    36129    36148      +19     
==========================================
+ Hits       166003   166119     +116     
+ Misses      14997    14982      -15     
- Partials     6757     6762       +5     
Files with missing lines Coverage Δ
lib/fs.js 98.26% <100.00%> (+0.11%) ⬆️

... and 54 files with indirect coverage changes

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Ethan-Arrowood

Copy link
Copy Markdown
Contributor

Please add a test; otherwise lgtm

@Ethan-Arrowood Ethan-Arrowood self-assigned this Nov 6, 2024
@juanarbol

Copy link
Copy Markdown
Member Author

Please add a test; otherwise lgtm

Will do

@juanarbol
juanarbol force-pushed the juan/fs-readdir branch 2 times, most recently from 0330a1d to 10a71b9 Compare November 14, 2024 01:36
@juanarbol

Copy link
Copy Markdown
Member Author

@Ethan-Arrowood there we go!

Comment thread test/parallel/test-fs-readdir-types-symlinks.js Outdated
Comment thread test/parallel/test-fs-readdir-types-symlinks.js Outdated
Fixes: nodejs#52663
Signed-off-by: Juan José Arboleda <soyjuanarbol@gmail.com>
@juanarbol

Copy link
Copy Markdown
Member Author

@Ethan-Arrowood you were right, I've addressed your suggestions and tested the thing w/ main branch build.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@juanarbol

Copy link
Copy Markdown
Member Author

ping @Ethan-Arrowood

@Ethan-Arrowood Ethan-Arrowood left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@Ethan-Arrowood Ethan-Arrowood added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 21, 2024
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 21, 2024
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@juanarbol juanarbol added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 24, 2024
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 24, 2024
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

nodejs-github-bot commented Nov 24, 2024

Copy link
Copy Markdown
Collaborator

@juanarbol juanarbol added the commit-queue PRs queued for automated landing through the Commit Queue. label Nov 25, 2024
@nodejs-github-bot nodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Nov 25, 2024
aduh95 pushed a commit that referenced this pull request Dec 13, 2024
Fixes: #52663
Signed-off-by: Juan José Arboleda <soyjuanarbol@gmail.com>
PR-URL: #55714
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
Reviewed-By: James M Snell <jasnell@gmail.com>
ruyadorno pushed a commit that referenced this pull request Jan 5, 2025
Fixes: #52663
Signed-off-by: Juan José Arboleda <soyjuanarbol@gmail.com>
PR-URL: #55714
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
Reviewed-By: James M Snell <jasnell@gmail.com>
codebytere added a commit to electron/electron that referenced this pull request Jan 28, 2025
nodejs/node#55714

This doesn't currently play well with ASAR - this should be fixed in a follow up
codebytere added a commit to electron/electron that referenced this pull request Jan 28, 2025
nodejs/node#55714

This doesn't currently play well with ASAR - this should be fixed in a follow up
codebytere added a commit to electron/electron that referenced this pull request Jan 28, 2025
nodejs/node#55714

This doesn't currently play well with ASAR - this should be fixed in a follow up
jkleinsc pushed a commit to electron/electron that referenced this pull request Jan 29, 2025
* chore: bump node in DEPS to v22.13.1

* chore: fixup GN build file

* nodejs/node#55529
* nodejs/node#55798
* nodejs/node#55530

* module: simplify --inspect-brk handling

nodejs/node#55679

* src: fix outdated js2c.cc references

nodejs/node#56133

* crypto: include openssl/rand.h explicitly

nodejs/node#55425

* build: use variable for crypto dep path

nodejs/node#55928

* crypto: fix RSA_PKCS1_PADDING error message

nodejs/node#55629

* build: use variable for simdutf path

nodejs/node#56196

* test,crypto: make crypto tests work with BoringSSL

nodejs/node#55491

* fix: suppress clang -Wdeprecated-declarations in libuv

libuv/libuv#4486

* deps: update libuv to 1.49.1

nodejs/node#55114

* test: make test-node-output-v8-warning more flexible

nodejs/node#55401

* [v22.x] Revert "v8: enable maglev on supported architectures"

nodejs/node#54384

* fix: potential WIN32_LEAN_AND_MEAN redefinition

c-ares/c-ares#869

* deps: update nghttp2 to 1.64.0

nodejs/node#55559

* src: provide workaround for container-overflow

nodejs/node#55591

* build: use variable for simdutf path

nodejs/node#56196

* chore: fixup patch indices

* fixup! module: simplify --inspect-brk handling

* lib: fix fs.readdir recursive async

nodejs/node#56041

* lib: avoid excluding symlinks in recursive fs.readdir with filetypes

nodejs/node#55714

This doesn't currently play well with ASAR - this should be fixed in a follow up

* test: disable CJS permission test for config.main

This has diverged as a result of our revert of
src,lb: reducing C++ calls of esm legacy main resolve

* fixup! lib: fix fs.readdir recursive async

* deps: update libuv to 1.49.1

nodejs/node#55114

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
codebytere added a commit to electron/electron that referenced this pull request Feb 3, 2025
nodejs/node#55714

This doesn't currently play well with ASAR - this should be fixed in a follow up
codebytere added a commit to electron/electron that referenced this pull request Feb 10, 2025
nodejs/node#55714

This doesn't currently play well with ASAR - this should be fixed in a follow up
codebytere added a commit to electron/electron that referenced this pull request Feb 10, 2025
nodejs/node#55714

This doesn't currently play well with ASAR - this should be fixed in a follow up
codebytere added a commit to electron/electron that referenced this pull request Feb 14, 2025
nodejs/node#55714

This doesn't currently play well with ASAR - this should be fixed in a follow up
codebytere added a commit to electron/electron that referenced this pull request Feb 14, 2025
* chore: bump node in DEPS to v22.13.0

* chore: bump node in DEPS to v22.13.1

* src: move evp stuff to ncrypto

nodejs/node#54911

* crypto: add Date fields for validTo and validFrom

nodejs/node#54159

* module: fix discrepancy between .ts and .js

nodejs/node#54461

* esm: do not interpret "main" as a URL

nodejs/node#55003

* src: modernize likely/unlikely hints

nodejs/node#55155

* chore: update patch indices

* crypto: add validFromDate and validToDate fields to X509Certificate

nodejs/node#54159

* chore: fixup perfetto patch

* fix: clang warning in simdjson

* src: add receiver to fast api callback methods

nodejs/node#54408

* chore: fixup revert patch

* fixup! esm: do not interpret "main" as a URL

* fixup! crypto: add Date fields for validTo and validFrom

* fix: move ArrayBuffer test patch

* src: fixup Error.stackTraceLimit during snapshot building

nodejs/node#55121

* fix: bad rebase

* chore: fixup amaro

* chore: address feedback from review

* src: revert filesystem::path changes

nodejs/node#55015

* chore: fixup GN build file

* nodejs/node#55529
* nodejs/node#55798
* nodejs/node#55530

* module: simplify --inspect-brk handling

nodejs/node#55679

* src: fix outdated js2c.cc references

nodejs/node#56133

* crypto: include openssl/rand.h explicitly

nodejs/node#55425

* build: use variable for crypto dep path

nodejs/node#55928

* crypto: fix RSA_PKCS1_PADDING error message

nodejs/node#55629

* build: use variable for simdutf path

nodejs/node#56196

* test,crypto: make crypto tests work with BoringSSL

nodejs/node#55491

* fix: suppress clang -Wdeprecated-declarations in libuv

libuv/libuv#4486

* deps: update libuv to 1.49.1

nodejs/node#55114

* test: make test-node-output-v8-warning more flexible

nodejs/node#55401

* [v22.x] Revert "v8: enable maglev on supported architectures"

nodejs/node#54384

* fix: potential WIN32_LEAN_AND_MEAN redefinition

c-ares/c-ares#869

* deps: update nghttp2 to 1.64.0

nodejs/node#55559

* src: provide workaround for container-overflow

nodejs/node#55591

* build: use variable for simdutf path

nodejs/node#56196

* chore: fixup patch indices

* fixup! module: simplify --inspect-brk handling

* lib: fix fs.readdir recursive async

nodejs/node#56041

* lib: avoid excluding symlinks in recursive fs.readdir with filetypes

nodejs/node#55714

This doesn't currently play well with ASAR - this should be fixed in a follow up

* test: disable CJS permission test for config.main

This has diverged as a result of our revert of
src,lb: reducing C++ calls of esm legacy main resolve

* fixup! lib: fix fs.readdir recursive async

* deps: update libuv to 1.49.1

nodejs/node#55114

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
jelmore1674 pushed a commit to jelmore1674/build-changelog that referenced this pull request Mar 19, 2025
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [node](https://nodejs.org) ([source](https://github.com/nodejs/node)) | engines | minor | [`~22.12.0` -> `~22.13.0`](https://renovatebot.com/diffs/npm/node/v22.12.0/v22.13.0) |

---

### Release Notes

<details>
<summary>nodejs/node (node)</summary>

### [`v22.13.0`](https://github.com/nodejs/node/releases/tag/v22.13.0): 2025-01-07, Version 22.13.0 &#x27;Jod&#x27; (LTS), @&#8203;ruyadorno

[Compare Source](nodejs/node@v22.12.0...v22.13.0)

##### Notable Changes

##### Stabilize Permission Model

Upgrades the Permission Model status from Active Development to Stable.

Contributed by Rafael Gonzaga [#&#8203;56201](nodejs/node#56201)

##### Graduate WebCryptoAPI [`Ed25519`](nodejs/node@Ed25519) and X25519 algorithms as stable

Following the merge of Curve25519 into the [Web Cryptography API Editor's Draft](https://w3c.github.io/webcrypto/) the `Ed25519` and `X25519` algorithm identifiers are now stable and will no longer emit an ExperimentalWarning upon use.

Contributed by (Filip Skokan) [#&#8203;56142](nodejs/node#56142)

##### Other Notable Changes

-   \[[`05d6227a88`](nodejs/node@05d6227a88)] - **(SEMVER-MINOR)** **assert**: add partialDeepStrictEqual (Giovanni Bucci) [#&#8203;54630](nodejs/node#54630)
-   \[[`a933103499`](nodejs/node@a933103499)] - **(SEMVER-MINOR)** **cli**: implement --trace-env and --trace-env-\[js|native]-stack (Joyee Cheung) [#&#8203;55604](nodejs/node#55604)
-   \[[`ba9d5397de`](nodejs/node@ba9d5397de)] - **(SEMVER-MINOR)** **dgram**: support blocklist in udp (theanarkh) [#&#8203;56087](nodejs/node#56087)
-   \[[`f6d0c01303`](nodejs/node@f6d0c01303)] - **doc**: stabilize util.styleText (Rafael Gonzaga) [#&#8203;56265](nodejs/node#56265)
-   \[[`34c68827af`](nodejs/node@34c68827af)] - **doc**: move typescript support to active development (Marco Ippolito) [#&#8203;55536](nodejs/node#55536)
-   \[[`dd14b80350`](nodejs/node@dd14b80350)] - **doc**: add LJHarb to collaborators (Jordan Harband) [#&#8203;56132](nodejs/node#56132)
-   \[[`5263086169`](nodejs/node@5263086169)] - **(SEMVER-MINOR)** **doc**: add report version and history section (Chengzhong Wu) [#&#8203;56130](nodejs/node#56130)
-   \[[`8cb3c2018d`](nodejs/node@8cb3c2018d)] - **(SEMVER-MINOR)** **doc**: sort --report-exclude alphabetically (Rafael Gonzaga) [#&#8203;55788](nodejs/node#55788)
-   \[[`55239a48b6`](nodejs/node@55239a48b6)] - **(SEMVER-MINOR)** **doc,lib,src,test**: unflag sqlite module (Colin Ihrig) [#&#8203;55890](nodejs/node#55890)
-   \[[`7cbe3de1d8`](nodejs/node@7cbe3de1d8)] - **(SEMVER-MINOR)** **module**: only emit require(esm) warning under --trace-require-module (Joyee Cheung) [#&#8203;56194](nodejs/node#56194)
-   \[[`6575b76042`](nodejs/node@6575b76042)] - **(SEMVER-MINOR)** **module**: add module.stripTypeScriptTypes (Marco Ippolito) [#&#8203;55282](nodejs/node#55282)
-   \[[`bacfe6d5c9`](nodejs/node@bacfe6d5c9)] - **(SEMVER-MINOR)** **net**: support blocklist in net.connect (theanarkh) [#&#8203;56075](nodejs/node#56075)
-   \[[`b47888d390`](nodejs/node@b47888d390)] - **(SEMVER-MINOR)** **net**: support blocklist for net.Server (theanarkh) [#&#8203;56079](nodejs/node#56079)
-   \[[`566f0a1d25`](nodejs/node@566f0a1d25)] - **(SEMVER-MINOR)** **net**: add SocketAddress.parse (James M Snell) [#&#8203;56076](nodejs/node#56076)
-   \[[`ed7eab1421`](nodejs/node@ed7eab1421)] - **(SEMVER-MINOR)** **net**: add net.BlockList.isBlockList(value) (James M Snell) [#&#8203;56078](nodejs/node#56078)
-   \[[`ea4891856d`](nodejs/node@ea4891856d)] - **(SEMVER-MINOR)** **process**: deprecate `features.{ipv6,uv}` and `features.tls_*` (René) [#&#8203;55545](nodejs/node#55545)
-   \[[`01eb308f26`](nodejs/node@01eb308f26)] - **(SEMVER-MINOR)** **report**: fix typos in report keys and bump the version (Yuan-Ming Hsu) [#&#8203;56068](nodejs/node#56068)
-   \[[`97c38352d0`](nodejs/node@97c38352d0)] - **(SEMVER-MINOR)** **sqlite**: aggregate constants in a single property (Edigleysson Silva (Edy)) [#&#8203;56213](nodejs/node#56213)
-   \[[`b4041e554a`](nodejs/node@b4041e554a)] - **(SEMVER-MINOR)** **sqlite**: add `StatementSync.prototype.iterate` method (tpoisseau) [#&#8203;54213](nodejs/node#54213)
-   \[[`2e3ca1bbdd`](nodejs/node@2e3ca1bbdd)] - **(SEMVER-MINOR)** **src**: add cli option to preserve env vars on diagnostic reports (Rafael Gonzaga) [#&#8203;55697](nodejs/node#55697)
-   \[[`bcfe9c80fc`](nodejs/node@bcfe9c80fc)] - **(SEMVER-MINOR)** **util**: add sourcemap support to getCallSites (Marco Ippolito) [#&#8203;55589](nodejs/node#55589)

##### Commits

-   \[[`e9024779c0`](nodejs/node@e9024779c0)] - **assert**: make Maps be partially compared in partialDeepStrictEqual (Giovanni Bucci) [#&#8203;56195](nodejs/node#56195)
-   \[[`4c13d8e587`](nodejs/node@4c13d8e587)] - **assert**: make partialDeepStrictEqual work with ArrayBuffers (Giovanni Bucci) [#&#8203;56098](nodejs/node#56098)
-   \[[`a4fa31a86e`](nodejs/node@a4fa31a86e)] - **assert**: optimize partial comparison of two `Set`s (Antoine du Hamel) [#&#8203;55970](nodejs/node#55970)
-   \[[`05d6227a88`](nodejs/node@05d6227a88)] - **(SEMVER-MINOR)** **assert**: add partialDeepStrictEqual (Giovanni Bucci) [#&#8203;54630](nodejs/node#54630)
-   \[[`5e1321abd7`](nodejs/node@5e1321abd7)] - **buffer**: document concat zero-fill (Duncan) [#&#8203;55562](nodejs/node#55562)
-   \[[`be5ba7c648`](nodejs/node@be5ba7c648)] - **build**: set DESTCPU correctly for 'make binary' on loongarch64 (吴小白) [#&#8203;56271](nodejs/node#56271)
-   \[[`38cf37ee2d`](nodejs/node@38cf37ee2d)] - **build**: fix missing fp16 dependency in d8 builds (Joyee Cheung) [#&#8203;56266](nodejs/node#56266)
-   \[[`dbb7557455`](nodejs/node@dbb7557455)] - **build**: add major release action (Rafael Gonzaga) [#&#8203;56199](nodejs/node#56199)
-   \[[`27cc90f3be`](nodejs/node@27cc90f3be)] - **build**: fix C string encoding for `PRODUCT_DIR_ABS` (Anna Henningsen) [#&#8203;56111](nodejs/node#56111)
-   \[[`376561c2b4`](nodejs/node@376561c2b4)] - **build**: use variable for simdutf path (Shelley Vohr) [#&#8203;56196](nodejs/node#56196)
-   \[[`126ae15000`](nodejs/node@126ae15000)] - **build**: allow overriding clang usage (Shelley Vohr) [#&#8203;56016](nodejs/node#56016)
-   \[[`97bb8f7c76`](nodejs/node@97bb8f7c76)] - **build**: remove defaults for create-release-proposal (Rafael Gonzaga) [#&#8203;56042](nodejs/node#56042)
-   \[[`a8fb1a06f3`](nodejs/node@a8fb1a06f3)] - **build**: set node_arch to target_cpu in GN (Shelley Vohr) [#&#8203;55967](nodejs/node#55967)
-   \[[`9f48ca27f1`](nodejs/node@9f48ca27f1)] - **build**: use variable for crypto dep path (Shelley Vohr) [#&#8203;55928](nodejs/node#55928)
-   \[[`e47ccd2287`](nodejs/node@e47ccd2287)] - **build**: fix GN build for sqlite (Cheng) [#&#8203;55912](nodejs/node#55912)
-   \[[`8d70b99a5a`](nodejs/node@8d70b99a5a)] - **build**: compile bundled simdutf conditionally (Jakub Jirutka) [#&#8203;55886](nodejs/node#55886)
-   \[[`826fd35242`](nodejs/node@826fd35242)] - **build**: compile bundled simdjson conditionally (Jakub Jirutka) [#&#8203;55886](nodejs/node#55886)
-   \[[`1015b22085`](nodejs/node@1015b22085)] - **build**: compile bundled ada conditionally (Jakub Jirutka) [#&#8203;55886](nodejs/node#55886)
-   \[[`77e2869ca6`](nodejs/node@77e2869ca6)] - **build**: use glob for dependencies of out/Makefile (Richard Lau) [#&#8203;55789](nodejs/node#55789)
-   \[[`a933103499`](nodejs/node@a933103499)] - **(SEMVER-MINOR)** **cli**: implement --trace-env and --trace-env-\[js|native]-stack (Joyee Cheung) [#&#8203;55604](nodejs/node#55604)
-   \[[`72e8e0684e`](nodejs/node@72e8e0684e)] - **crypto**: graduate WebCryptoAPI [`Ed25519`](nodejs/node@Ed25519) and X25519 algorithms as stable (Filip Skokan) [#&#8203;56142](nodejs/node#56142)
-   \[[`fe2b344ddb`](nodejs/node@fe2b344ddb)] - **crypto**: ensure CryptoKey usages and algorithm are cached objects (Filip Skokan) [#&#8203;56108](nodejs/node#56108)
-   \[[`9ee9f524a7`](nodejs/node@9ee9f524a7)] - **crypto**: allow non-multiple of 8 in SubtleCrypto.deriveBits (Filip Skokan) [#&#8203;55296](nodejs/node#55296)
-   \[[`76f242d993`](nodejs/node@76f242d993)] - **deps**: update nghttp3 to 1.6.0 (Node.js GitHub Bot) [#&#8203;56258](nodejs/node#56258)
-   \[[`c7ff2ea6b5`](nodejs/node@c7ff2ea6b5)] - **deps**: update simdutf to 5.6.4 (Node.js GitHub Bot) [#&#8203;56255](nodejs/node#56255)
-   \[[`04230be1ef`](nodejs/node@04230be1ef)] - **deps**: update libuv to 1.49.2 (Luigi Pinca) [#&#8203;56224](nodejs/node#56224)
-   \[[`88589b85b7`](nodejs/node@88589b85b7)] - **deps**: update c-ares to v1.34.4 (Node.js GitHub Bot) [#&#8203;56256](nodejs/node#56256)
-   \[[`5c2e0618f3`](nodejs/node@5c2e0618f3)] - **deps**: define V8\_PRESERVE_MOST as no-op on Windows (Stefan Stojanovic) [#&#8203;56238](nodejs/node#56238)
-   \[[`9f8f3c9658`](nodejs/node@9f8f3c9658)] - **deps**: update sqlite to 3.47.2 (Node.js GitHub Bot) [#&#8203;56178](nodejs/node#56178)
-   \[[`17b6931d3b`](nodejs/node@17b6931d3b)] - **deps**: update ngtcp2 to 1.9.1 (Node.js GitHub Bot) [#&#8203;56095](nodejs/node#56095)
-   \[[`22b453b619`](nodejs/node@22b453b619)] - **deps**: upgrade npm to 10.9.2 (npm team) [#&#8203;56135](nodejs/node#56135)
-   \[[`d7eb41b382`](nodejs/node@d7eb41b382)] - **deps**: update sqlite to 3.47.1 (Node.js GitHub Bot) [#&#8203;56094](nodejs/node#56094)
-   \[[`669c722aa9`](nodejs/node@669c722aa9)] - **deps**: update zlib to 1.3.0.1-motley-82a5fec (Node.js GitHub Bot) [#&#8203;55980](nodejs/node#55980)
-   \[[`f61a0454d2`](nodejs/node@f61a0454d2)] - **deps**: update corepack to 0.30.0 (Node.js GitHub Bot) [#&#8203;55977](nodejs/node#55977)
-   \[[`d98bf0b891`](nodejs/node@d98bf0b891)] - **deps**: update ngtcp2 to 1.9.0 (Node.js GitHub Bot) [#&#8203;55975](nodejs/node#55975)
-   \[[`fc362624bf`](nodejs/node@fc362624bf)] - **deps**: update simdutf to 5.6.3 (Node.js GitHub Bot) [#&#8203;55973](nodejs/node#55973)
-   \[[`f61dcc4df4`](nodejs/node@f61dcc4df4)] - **deps**: upgrade npm to 10.9.1 (npm team) [#&#8203;55951](nodejs/node#55951)
-   \[[`bfe7982491`](nodejs/node@bfe7982491)] - **deps**: update zlib to 1.3.0.1-motley-7e2e4d7 (Node.js GitHub Bot) [#&#8203;54432](nodejs/node#54432)
-   \[[`d714367ef8`](nodejs/node@d714367ef8)] - **deps**: update simdjson to 3.10.1 (Node.js GitHub Bot) [#&#8203;54678](nodejs/node#54678)
-   \[[`ccc9b105ec`](nodejs/node@ccc9b105ec)] - **deps**: update simdutf to 5.6.2 (Node.js GitHub Bot) [#&#8203;55889](nodejs/node#55889)
-   \[[`ba9d5397de`](nodejs/node@ba9d5397de)] - **(SEMVER-MINOR)** **dgram**: support blocklist in udp (theanarkh) [#&#8203;56087](nodejs/node#56087)
-   \[[`7ddbf94849`](nodejs/node@7ddbf94849)] - **dgram**: check udp buffer size to avoid fd leak (theanarkh) [#&#8203;56084](nodejs/node#56084)
-   \[[`360d68de0f`](nodejs/node@360d68de0f)] - **doc**: fix color contrast issue in light mode (Rich Trott) [#&#8203;56272](nodejs/node#56272)
-   \[[`f6d0c01303`](nodejs/node@f6d0c01303)] - **doc**: stabilize util.styleText (Rafael Gonzaga) [#&#8203;56265](nodejs/node#56265)
-   \[[`9436c3c949`](nodejs/node@9436c3c949)] - **doc**: clarify util.aborted resource usage (Kunal Kumar) [#&#8203;55780](nodejs/node#55780)
-   \[[`b1cec2cef9`](nodejs/node@b1cec2cef9)] - **doc**: add esm examples to node:repl (Alfredo González) [#&#8203;55432](nodejs/node#55432)
-   \[[`d6a84cf781`](nodejs/node@d6a84cf781)] - **doc**: add esm examples to node:readline (Alfredo González) [#&#8203;55335](nodejs/node#55335)
-   \[[`a11ac1c0f2`](nodejs/node@a11ac1c0f2)] - **doc**: fix 'which' to 'that' and add commas (Selveter Senitro) [#&#8203;56216](nodejs/node#56216)
-   \[[`5331df7911`](nodejs/node@5331df7911)] - **doc**: fix winget config path (Alex Yang) [#&#8203;56233](nodejs/node#56233)
-   \[[`7a8071b43c`](nodejs/node@7a8071b43c)] - **doc**: add esm examples to node:tls (Alfredo González) [#&#8203;56229](nodejs/node#56229)
-   \[[`7d8c1e72d5`](nodejs/node@7d8c1e72d5)] - **doc**: add esm examples to node:perf_hooks (Alfredo González) [#&#8203;55257](nodejs/node#55257)
-   \[[`ea53c4b1ae`](nodejs/node@ea53c4b1ae)] - **doc**: `sea.getRawAsset(key)` always returns an ArrayBuffer (沈鸿飞) [#&#8203;56206](nodejs/node#56206)
-   \[[`7a94100a3e`](nodejs/node@7a94100a3e)] - **doc**: update announce documentation for releases (Rafael Gonzaga) [#&#8203;56200](nodejs/node#56200)
-   \[[`44c4e57e32`](nodejs/node@44c4e57e32)] - **doc**: update blog link to /vulnerability (Rafael Gonzaga) [#&#8203;56198](nodejs/node#56198)
-   \[[`5e5b4b0cbd`](nodejs/node@5e5b4b0cbd)] - **doc**: call out import.meta is only supported in ES modules (Anton Kastritskii) [#&#8203;56186](nodejs/node#56186)
-   \[[`a83de32d35`](nodejs/node@a83de32d35)] - **doc**: add ambassador message - benefits of Node.js (Michael Dawson) [#&#8203;56085](nodejs/node#56085)
-   \[[`bb880dd21a`](nodejs/node@bb880dd21a)] - **doc**: fix incorrect link to style guide (Yuan-Ming Hsu) [#&#8203;56181](nodejs/node#56181)
-   \[[`39ce902e58`](nodejs/node@39ce902e58)] - **doc**: fix c++ addon hello world sample (Edigleysson Silva (Edy)) [#&#8203;56172](nodejs/node#56172)
-   \[[`19c72c4acc`](nodejs/node@19c72c4acc)] - **doc**: update blog release-post link (Ruy Adorno) [#&#8203;56123](nodejs/node#56123)
-   \[[`b667cc4669`](nodejs/node@b667cc4669)] - **doc**: fix module.md headings (Chengzhong Wu) [#&#8203;56131](nodejs/node#56131)
-   \[[`34c68827af`](nodejs/node@34c68827af)] - **doc**: move typescript support to active development (Marco Ippolito) [#&#8203;55536](nodejs/node#55536)
-   \[[`c4a97d810b`](nodejs/node@c4a97d810b)] - **doc**: mention `-a` flag for the release script (Ruy Adorno) [#&#8203;56124](nodejs/node#56124)
-   \[[`dd14b80350`](nodejs/node@dd14b80350)] - **doc**: add LJHarb to collaborators (Jordan Harband) [#&#8203;56132](nodejs/node#56132)
-   \[[`2feb0781ed`](nodejs/node@2feb0781ed)] - **doc**: add create-release-action to process (Rafael Gonzaga) [#&#8203;55993](nodejs/node#55993)
-   \[[`71f6263942`](nodejs/node@71f6263942)] - **doc**: rename file to advocacy-ambassador-program.md (Tobias Nießen) [#&#8203;56046](nodejs/node#56046)
-   \[[`8efa240500`](nodejs/node@8efa240500)] - **doc**: remove unused import from sample code (Blended Bram) [#&#8203;55570](nodejs/node#55570)
-   \[[`e64cef8bf4`](nodejs/node@e64cef8bf4)] - **doc**: add FAQ to releases section (Rafael Gonzaga) [#&#8203;55992](nodejs/node#55992)
-   \[[`4bb0f30f92`](nodejs/node@4bb0f30f92)] - **doc**: move history entry to class description (Luigi Pinca) [#&#8203;55991](nodejs/node#55991)
-   \[[`6d02bd6873`](nodejs/node@6d02bd6873)] - **doc**: add history entry for textEncoder.encodeInto() (Luigi Pinca) [#&#8203;55990](nodejs/node#55990)
-   \[[`e239382ed8`](nodejs/node@e239382ed8)] - **doc**: improve GN build documentation a bit (Shelley Vohr) [#&#8203;55968](nodejs/node#55968)
-   \[[`78b6aef6bc`](nodejs/node@78b6aef6bc)] - **doc**: fix deprecation codes (Filip Skokan) [#&#8203;56018](nodejs/node#56018)
-   \[[`474bf80a44`](nodejs/node@474bf80a44)] - **doc**: remove confusing and outdated sentence (Luigi Pinca) [#&#8203;55988](nodejs/node#55988)
-   \[[`57381076c5`](nodejs/node@57381076c5)] - **doc**: deprecate passing invalid types in `fs.existsSync` (Carlos Espa) [#&#8203;55892](nodejs/node#55892)
-   \[[`e529cf6b26`](nodejs/node@e529cf6b26)] - **doc**: add doc for PerformanceObserver.takeRecords() (skyclouds2001) [#&#8203;55786](nodejs/node#55786)
-   \[[`a6ef0f6f6e`](nodejs/node@a6ef0f6f6e)] - **doc**: add vetted courses to the ambassador benefits (Matteo Collina) [#&#8203;55934](nodejs/node#55934)
-   \[[`63526049f2`](nodejs/node@63526049f2)] - **doc**: order `node:crypto` APIs alphabetically (Julian Gassner) [#&#8203;55831](nodejs/node#55831)
-   \[[`36080b7b61`](nodejs/node@36080b7b61)] - **doc**: doc how to add message for promotion (Michael Dawson) [#&#8203;55843](nodejs/node#55843)
-   \[[`12b2ad4287`](nodejs/node@12b2ad4287)] - **doc**: add esm example for zlib (Leonardo Peixoto) [#&#8203;55946](nodejs/node#55946)
-   \[[`352daac296`](nodejs/node@352daac296)] - **doc**: fix typo (Alex Yang) [#&#8203;56125](nodejs/node#56125)
-   \[[`6e7e9a126d`](nodejs/node@6e7e9a126d)] - **doc**: document approach for building wasm in deps (Michael Dawson) [#&#8203;55940](nodejs/node#55940)
-   \[[`0b3ac05422`](nodejs/node@0b3ac05422)] - **doc**: remove RedYetiDev from triagers team (Aviv Keller) [#&#8203;55947](nodejs/node#55947)
-   \[[`20be5e2f80`](nodejs/node@20be5e2f80)] - **doc**: add esm examples to node:timers (Alfredo González) [#&#8203;55857](nodejs/node#55857)
-   \[[`3ba9b57436`](nodejs/node@3ba9b57436)] - **doc**: fix relative path mention in --allow-fs (Rafael Gonzaga) [#&#8203;55791](nodejs/node#55791)
-   \[[`3e6b3a9a8b`](nodejs/node@3e6b3a9a8b)] - **doc**: include git node release --promote to steps (Rafael Gonzaga) [#&#8203;55835](nodejs/node#55835)
-   \[[`5bdfde8dc6`](nodejs/node@5bdfde8dc6)] - **doc**: add history entry for import assertion removal (Antoine du Hamel) [#&#8203;55883](nodejs/node#55883)
-   \[[`c842146c05`](nodejs/node@c842146c05)] - **doc**: add a note on console stream behavior (Gireesh Punathil) [#&#8203;55616](nodejs/node#55616)
-   \[[`5263086169`](nodejs/node@5263086169)] - **(SEMVER-MINOR)** **doc**: add report version and history section (Chengzhong Wu) [#&#8203;56130](nodejs/node#56130)
-   \[[`8cb3c2018d`](nodejs/node@8cb3c2018d)] - **(SEMVER-MINOR)** **doc**: sort --report-exclude alphabetically (Rafael Gonzaga) [#&#8203;55788](nodejs/node#55788)
-   \[[`55239a48b6`](nodejs/node@55239a48b6)] - **(SEMVER-MINOR)** **doc,lib,src,test**: unflag sqlite module (Colin Ihrig) [#&#8203;55890](nodejs/node#55890)
-   \[[`04d7c7a349`](nodejs/node@04d7c7a349)] - **fs**: make mutating `options` in Callback `readdir()` not affect results (LiviaMedeiros) [#&#8203;56057](nodejs/node#56057)
-   \[[`92bcd528e7`](nodejs/node@92bcd528e7)] - **fs**: make mutating `options` in Promises `readdir()` not affect results (LiviaMedeiros) [#&#8203;56057](nodejs/node#56057)
-   \[[`3a55bd9448`](nodejs/node@3a55bd9448)] - **fs**: lazily load ReadFileContext (Gürgün Dayıoğlu) [#&#8203;55998](nodejs/node#55998)
-   \[[`0331b3fdd3`](nodejs/node@0331b3fdd3)] - **fs,win**: fix readdir for named pipe (Hüseyin Açacak) [#&#8203;56110](nodejs/node#56110)
-   \[[`79152b54e9`](nodejs/node@79152b54e9)] - **http**: add setDefaultHeaders option to http.request (Tim Perry) [#&#8203;56112](nodejs/node#56112)
-   \[[`19782855a8`](nodejs/node@19782855a8)] - **http**: don't emit error after destroy (Robert Nagy) [#&#8203;55457](nodejs/node#55457)
-   \[[`8494512c17`](nodejs/node@8494512c17)] - **http2**: remove duplicate codeblock (Vitaly Aminev) [#&#8203;55915](nodejs/node#55915)
-   \[[`d2f82223d1`](nodejs/node@d2f82223d1)] - **http2**: support ALPNCallback option (ZYSzys) [#&#8203;56187](nodejs/node#56187)
-   \[[`2616f1247a`](nodejs/node@2616f1247a)] - **http2**: fix memory leak caused by premature listener removing (ywave620) [#&#8203;55966](nodejs/node#55966)
-   \[[`598fe048f2`](nodejs/node@598fe048f2)] - **lib**: remove redundant global regexps (Gürgün Dayıoğlu) [#&#8203;56182](nodejs/node#56182)
-   \[[`a3c8739530`](nodejs/node@a3c8739530)] - **lib**: clean up persisted signals when they are settled (Edigleysson Silva (Edy)) [#&#8203;56001](nodejs/node#56001)
-   \[[`11144ab158`](nodejs/node@11144ab158)] - **lib**: handle Float16Array in node:v8 serdes (Bartek Iwańczuk) [#&#8203;55996](nodejs/node#55996)
-   \[[`81c94a32e4`](nodejs/node@81c94a32e4)] - **lib**: disable default memory leak warning for AbortSignal (Lenz Weber-Tronic) [#&#8203;55816](nodejs/node#55816)
-   \[[`68dda61420`](nodejs/node@68dda61420)] - **lib**: add validation for options in compileFunction (Taejin Kim) [#&#8203;56023](nodejs/node#56023)
-   \[[`d2007aec28`](nodejs/node@d2007aec28)] - **lib**: fix `fs.readdir` recursive async (Rafael Gonzaga) [#&#8203;56041](nodejs/node#56041)
-   \[[`0571d5556f`](nodejs/node@0571d5556f)] - **lib**: avoid excluding symlinks in recursive fs.readdir with filetypes (Juan José) [#&#8203;55714](nodejs/node#55714)
-   \[[`843943d0ce`](nodejs/node@843943d0ce)] - **meta**: bump github/codeql-action from 3.27.0 to 3.27.5 (dependabot\[bot]) [#&#8203;56103](nodejs/node#56103)
-   \[[`1529027f03`](nodejs/node@1529027f03)] - **meta**: bump actions/checkout from 4.1.7 to 4.2.2 (dependabot\[bot]) [#&#8203;56102](nodejs/node#56102)
-   \[[`8e265de9f5`](nodejs/node@8e265de9f5)] - **meta**: bump step-security/harden-runner from 2.10.1 to 2.10.2 (dependabot\[bot]) [#&#8203;56101](nodejs/node#56101)
-   \[[`0fba3a3b9b`](nodejs/node@0fba3a3b9b)] - **meta**: bump actions/setup-node from 4.0.3 to 4.1.0 (dependabot\[bot]) [#&#8203;56100](nodejs/node#56100)
-   \[[`2e3fdfdb19`](nodejs/node@2e3fdfdb19)] - **meta**: add releasers as CODEOWNERS to proposal action (Rafael Gonzaga) [#&#8203;56043](nodejs/node#56043)
-   \[[`7cbe3de1d8`](nodejs/node@7cbe3de1d8)] - **(SEMVER-MINOR)** **module**: only emit require(esm) warning under --trace-require-module (Joyee Cheung) [#&#8203;56194](nodejs/node#56194)
-   \[[`8a5429c9b3`](nodejs/node@8a5429c9b3)] - **module**: prevent main thread exiting before esm worker ends (Shima Ryuhei) [#&#8203;56183](nodejs/node#56183)
-   \[[`6575b76042`](nodejs/node@6575b76042)] - **(SEMVER-MINOR)** **module**: add module.stripTypeScriptTypes (Marco Ippolito) [#&#8203;55282](nodejs/node#55282)
-   \[[`0794861bc3`](nodejs/node@0794861bc3)] - **module**: simplify ts under node_modules check (Marco Ippolito) [#&#8203;55440](nodejs/node#55440)
-   \[[`28a11adf14`](nodejs/node@28a11adf14)] - **module**: mark evaluation rejection in require(esm) as handled (Joyee Cheung) [#&#8203;56122](nodejs/node#56122)
-   \[[`bacfe6d5c9`](nodejs/node@bacfe6d5c9)] - **(SEMVER-MINOR)** **net**: support blocklist in net.connect (theanarkh) [#&#8203;56075](nodejs/node#56075)
-   \[[`566f0a1d25`](nodejs/node@566f0a1d25)] - **(SEMVER-MINOR)** **net**: add SocketAddress.parse (James M Snell) [#&#8203;56076](nodejs/node#56076)
-   \[[`ed7eab1421`](nodejs/node@ed7eab1421)] - **(SEMVER-MINOR)** **net**: add net.BlockList.isBlockList(value) (James M Snell) [#&#8203;56078](nodejs/node#56078)
-   \[[`b47888d390`](nodejs/node@b47888d390)] - **(SEMVER-MINOR)** **net**: support blocklist for net.Server (theanarkh) [#&#8203;56079](nodejs/node#56079)
-   \[[`481770a38f`](nodejs/node@481770a38f)] - **node-api**: allow napi_delete_reference in finalizers (Chengzhong Wu) [#&#8203;55620](nodejs/node#55620)
-   \[[`2beb4f1f8c`](nodejs/node@2beb4f1f8c)] - **permission**: ignore internalModuleStat on module loading (Rafael Gonzaga) [#&#8203;55797](nodejs/node#55797)
-   \[[`ea4891856d`](nodejs/node@ea4891856d)] - **(SEMVER-MINOR)** **process**: deprecate `features.{ipv6,uv}` and `features.tls_*` (René) [#&#8203;55545](nodejs/node#55545)
-   \[[`c907b2f358`](nodejs/node@c907b2f358)] - **quic**: update more QUIC implementation (James M Snell) [#&#8203;55986](nodejs/node#55986)
-   \[[`43c25e2e0d`](nodejs/node@43c25e2e0d)] - **quic**: multiple updates to quic impl (James M Snell) [#&#8203;55971](nodejs/node#55971)
-   \[[`01eb308f26`](nodejs/node@01eb308f26)] - **(SEMVER-MINOR)** **report**: fix typos in report keys and bump the version (Yuan-Ming Hsu) [#&#8203;56068](nodejs/node#56068)
-   \[[`1cfa31fb82`](nodejs/node@1cfa31fb82)] - **sea**: only assert snapshot main function for main threads (Joyee Cheung) [#&#8203;56120](nodejs/node#56120)
-   \[[`97c38352d0`](nodejs/node@97c38352d0)] - **(SEMVER-MINOR)** **sqlite**: aggregate constants in a single property (Edigleysson Silva (Edy)) [#&#8203;56213](nodejs/node#56213)
-   \[[`2268c1ea8b`](nodejs/node@2268c1ea8b)] - **sqlite**: add support for custom functions (Colin Ihrig) [#&#8203;55985](nodejs/node#55985)
-   \[[`f5c6955722`](nodejs/node@f5c6955722)] - **sqlite**: support `db.loadExtension` (Alex Yang) [#&#8203;53900](nodejs/node#53900)
-   \[[`9a60bea6b7`](nodejs/node@9a60bea6b7)] - **sqlite**: deps include `sqlite3ext.h` (Alex Yang) [#&#8203;56010](nodejs/node#56010)
-   \[[`b4041e554a`](nodejs/node@b4041e554a)] - **(SEMVER-MINOR)** **sqlite**: add `StatementSync.prototype.iterate` method (tpoisseau) [#&#8203;54213](nodejs/node#54213)
-   \[[`2889e8da04`](nodejs/node@2889e8da04)] - **src**: fix outdated js2c.cc references (Chengzhong Wu) [#&#8203;56133](nodejs/node#56133)
-   \[[`5ce020b0c9`](nodejs/node@5ce020b0c9)] - **src**: use spaceship operator in SocketAddress (James M Snell) [#&#8203;56059](nodejs/node#56059)
-   \[[`a32fa30847`](nodejs/node@a32fa30847)] - **src**: add missing qualifiers to env.cc (Yagiz Nizipli) [#&#8203;56062](nodejs/node#56062)
-   \[[`974b7b61ef`](nodejs/node@974b7b61ef)] - **src**: use std::string_view for process emit fns (Yagiz Nizipli) [#&#8203;56086](nodejs/node#56086)
-   \[[`4559fac862`](nodejs/node@4559fac862)] - **src**: remove dead code in async_wrap (Gerhard Stöbich) [#&#8203;56065](nodejs/node#56065)
-   \[[`e42e4b20be`](nodejs/node@e42e4b20be)] - **src**: avoid copy on getV8FastApiCallCount (Yagiz Nizipli) [#&#8203;56081](nodejs/node#56081)
-   \[[`c188660e8b`](nodejs/node@c188660e8b)] - **src**: fix check fd (theanarkh) [#&#8203;56000](nodejs/node#56000)
-   \[[`d894cb76ff`](nodejs/node@d894cb76ff)] - **src**: safely remove the last line from dotenv (Shima Ryuhei) [#&#8203;55982](nodejs/node#55982)
-   \[[`2ca9f4b65a`](nodejs/node@2ca9f4b65a)] - **src**: fix kill signal on Windows (Hüseyin Açacak) [#&#8203;55514](nodejs/node#55514)
-   \[[`2e3ca1bbdd`](nodejs/node@2e3ca1bbdd)] - **(SEMVER-MINOR)** **src**: add cli option to preserve env vars on dr (Rafael Gonzaga) [#&#8203;55697](nodejs/node#55697)
-   \[[`359fff1c4e`](nodejs/node@359fff1c4e)] - **src,build**: add no user defined deduction guides of CTAD check (Chengzhong Wu) [#&#8203;56071](nodejs/node#56071)
-   \[[`57bb983215`](nodejs/node@57bb983215)] - **(SEMVER-MINOR)** **src,lib**: stabilize permission model (Rafael Gonzaga) [#&#8203;56201](nodejs/node#56201)
-   \[[`d352b0465a`](nodejs/node@d352b0465a)] - **stream**: commit pull-into descriptors after filling from queue (Mattias Buelens) [#&#8203;56072](nodejs/node#56072)
-   \[[`eef9bd1bf6`](nodejs/node@eef9bd1bf6)] - **test**: remove test-sqlite-statement-sync flaky designation (Luigi Pinca) [#&#8203;56051](nodejs/node#56051)
-   \[[`8718135a5d`](nodejs/node@8718135a5d)] - **test**: use --permission over --experimental-permission (Rafael Gonzaga) [#&#8203;56239](nodejs/node#56239)
-   \[[`9c68d4f180`](nodejs/node@9c68d4f180)] - **test**: remove exludes for sea tests on PPC (Michael Dawson) [#&#8203;56217](nodejs/node#56217)
-   \[[`c5d0472968`](nodejs/node@c5d0472968)] - **test**: fix test-abortsignal-drop-settled-signals flakiness (Edigleysson Silva (Edy)) [#&#8203;56197](nodejs/node#56197)
-   \[[`4adf518689`](nodejs/node@4adf518689)] - **test**: move localizationd data from `test-icu-env` to external file (Livia Medeiros) [#&#8203;55618](nodejs/node#55618)
-   \[[`02383b4267`](nodejs/node@02383b4267)] - **test**: update WPT for url to [`6fa3fe8`](nodejs/node@6fa3fe8a92) (Node.js GitHub Bot) [#&#8203;56136](nodejs/node#56136)
-   \[[`0e24eebf24`](nodejs/node@0e24eebf24)] - **test**: remove `hasOpenSSL3x` utils (Antoine du Hamel) [#&#8203;56164](nodejs/node#56164)
-   \[[`381e705385`](nodejs/node@381e705385)] - **test**: update streams wpt (Mattias Buelens) [#&#8203;56072](nodejs/node#56072)
-   \[[`ad107ca0d9`](nodejs/node@ad107ca0d9)] - **test**: remove test-fs-utimes flaky designation (Luigi Pinca) [#&#8203;56052](nodejs/node#56052)
-   \[[`e15c5dab79`](nodejs/node@e15c5dab79)] - **test**: ensure `cli.md` is in alphabetical order (Antoine du Hamel) [#&#8203;56025](nodejs/node#56025)
-   \[[`d0302e7d2d`](nodejs/node@d0302e7d2d)] - **test**: update WPT for WebCryptoAPI to [`3e3374e`](nodejs/node@3e3374efde) (Node.js GitHub Bot) [#&#8203;56093](nodejs/node#56093)
-   \[[`a0b1e8f400`](nodejs/node@a0b1e8f400)] - **test**: update WPT for WebCryptoAPI to [`76dfa54`](nodejs/node@76dfa54e5d) (Node.js GitHub Bot) [#&#8203;56093](nodejs/node#56093)
-   \[[`211f058a12`](nodejs/node@211f058a12)] - **test**: move test-worker-arraybuffer-zerofill to parallel (Luigi Pinca) [#&#8203;56053](nodejs/node#56053)
-   \[[`c52bc5d71c`](nodejs/node@c52bc5d71c)] - **test**: update WPT for url to [`67880a4`](nodejs/node@67880a4) (Node.js GitHub Bot) [#&#8203;55999](nodejs/node#55999)
-   \[[`1a78bde8d4`](nodejs/node@1a78bde8d4)] - **test**: make HTTP/1.0 connection test more robust (Arne Keller) [#&#8203;55959](nodejs/node#55959)
-   \[[`ff7b1445a0`](nodejs/node@ff7b1445a0)] - **test**: convert readdir test to use test runner (Thomas Chetwin) [#&#8203;55750](nodejs/node#55750)
-   \[[`b296b5a4e4`](nodejs/node@b296b5a4e4)] - **test**: make x509 crypto tests work with BoringSSL (Shelley Vohr) [#&#8203;55927](nodejs/node#55927)
-   \[[`97458ad74b`](nodejs/node@97458ad74b)] - **test**: fix determining lower priority (Livia Medeiros) [#&#8203;55908](nodejs/node#55908)
-   \[[`bb4aa7a296`](nodejs/node@bb4aa7a296)] - **test,crypto**: update WebCryptoAPI WPT (Filip Skokan) [#&#8203;55997](nodejs/node#55997)
-   \[[`fb98fa4967`](nodejs/node@fb98fa4967)] - **test_runner**: refactor Promise chain in run() (Colin Ihrig) [#&#8203;55958](nodejs/node#55958)
-   \[[`18c94961f8`](nodejs/node@18c94961f8)] - **test_runner**: refactor build Promise in Suite() (Colin Ihrig) [#&#8203;55958](nodejs/node#55958)
-   \[[`bf3967fd3a`](nodejs/node@bf3967fd3a)] - **test_runner**: simplify hook running logic (Colin Ihrig) [#&#8203;55963](nodejs/node#55963)
-   \[[`8c065dc61e`](nodejs/node@8c065dc61e)] - **test_runner**: mark context.plan() as stable (Colin Ihrig) [#&#8203;55895](nodejs/node#55895)
-   \[[`8ff082cf48`](nodejs/node@8ff082cf48)] - **test_runner**: mark snapshot testing as stable (Colin Ihrig) [#&#8203;55897](nodejs/node#55897)
-   \[[`7ae125cef4`](nodejs/node@7ae125cef4)] - **tools**: fix `node:` enforcement for docs (Antoine du Hamel) [#&#8203;56284](nodejs/node#56284)
-   \[[`0b489116a3`](nodejs/node@0b489116a3)] - **tools**: update github_reporter to 1.7.2 (Node.js GitHub Bot) [#&#8203;56205](nodejs/node#56205)
-   \[[`5306819fac`](nodejs/node@5306819fac)] - **tools**: add REPLACEME check to workflow (Mert Can Altin) [#&#8203;56251](nodejs/node#56251)
-   \[[`4e3cab44cb`](nodejs/node@4e3cab44cb)] - **tools**: use `github.actor` instead of bot username for release proposals (Antoine du Hamel) [#&#8203;56232](nodejs/node#56232)
-   \[[`3e8938463a`](nodejs/node@3e8938463a)] - ***Revert*** "**tools**: disable automated libuv updates" (Luigi Pinca) [#&#8203;56223](nodejs/node#56223)
-   \[[`98ea499e36`](nodejs/node@98ea499e36)] - **tools**: update gyp-next to 0.19.1 (Anna Henningsen) [#&#8203;56111](nodejs/node#56111)
-   \[[`2e76cd2a8b`](nodejs/node@2e76cd2a8b)] - **tools**: fix release proposal linter to support more than 1 folk preparing (Antoine du Hamel) [#&#8203;56203](nodejs/node#56203)
-   \[[`9fa0e41665`](nodejs/node@9fa0e41665)] - **tools**: enable linter for `tools/icu/**` (Livia Medeiros) [#&#8203;56176](nodejs/node#56176)
-   \[[`d6e1efcc59`](nodejs/node@d6e1efcc59)] - **tools**: use commit title as PR title when creating release proposal (Antoine du Hamel) [#&#8203;56165](nodejs/node#56165)
-   \[[`a88e4ce55e`](nodejs/node@a88e4ce55e)] - **tools**: update gyp-next to 0.19.0 (Node.js GitHub Bot) [#&#8203;56158](nodejs/node#56158)
-   \[[`bd0760efbc`](nodejs/node@bd0760efbc)] - **tools**: bump the eslint group in /tools/eslint with 4 updates (dependabot\[bot]) [#&#8203;56099](nodejs/node#56099)
-   \[[`c5b1cf4b12`](nodejs/node@c5b1cf4b12)] - **tools**: improve release proposal PR opening (Antoine du Hamel) [#&#8203;56161](nodejs/node#56161)
-   \[[`12baefb13d`](nodejs/node@12baefb13d)] - **tools**: update `create-release-proposal` workflow (Antoine du Hamel) [#&#8203;56054](nodejs/node#56054)
-   \[[`e6e1495f1a`](nodejs/node@e6e1495f1a)] - **tools**: fix update-undici script (Michaël Zasso) [#&#8203;56069](nodejs/node#56069)
-   \[[`ed635c90da`](nodejs/node@ed635c90da)] - **tools**: allow dispatch of `tools.yml` from forks (Antoine du Hamel) [#&#8203;56008](nodejs/node#56008)
-   \[[`1e628d1f37`](nodejs/node@1e628d1f37)] - **tools**: fix nghttp3 updater script (Antoine du Hamel) [#&#8203;56007](nodejs/node#56007)
-   \[[`1af3599b7e`](nodejs/node@1af3599b7e)] - **tools**: filter release keys to reduce interactivity (Antoine du Hamel) [#&#8203;55950](nodejs/node#55950)
-   \[[`1893be4a9c`](nodejs/node@1893be4a9c)] - **tools**: update WPT updater (Antoine du Hamel) [#&#8203;56003](nodejs/node#56003)
-   \[[`f89bd2ba8a`](nodejs/node@f89bd2ba8a)] - **tools**: add WPT updater for specific subsystems (Mert Can Altin) [#&#8203;54460](nodejs/node#54460)
-   \[[`61901372d5`](nodejs/node@61901372d5)] - **tools**: use tokenless Codecov uploads (Michaël Zasso) [#&#8203;55943](nodejs/node#55943)
-   \[[`312bb4dff8`](nodejs/node@312bb4dff8)] - **tools**: lint js in `doc/**/*.md` (Livia Medeiros) [#&#8203;55904](nodejs/node#55904)
-   \[[`7b476f637c`](nodejs/node@7b476f637c)] - **tools**: add linter for release commit proposals (Antoine du Hamel) [#&#8203;55923](nodejs/node#55923)
-   \[[`22d7017191`](nodejs/node@22d7017191)] - **tools**: fix riscv64 build failed (Lu Yahan) [#&#8203;52888](nodejs/node#52888)
-   \[[`f4f777f4d2`](nodejs/node@f4f777f4d2)] - **tools**: bump cross-spawn from 7.0.3 to 7.0.5 in /tools/eslint (dependabot\[bot]) [#&#8203;55894](nodejs/node#55894)
-   \[[`a648e4c44a`](nodejs/node@a648e4c44a)] - **util**: harden more built-in classes against prototype pollution (Antoine du Hamel) [#&#8203;56225](nodejs/node#56225)
-   \[[`4a1b51b5a9`](nodejs/node@4a1b51b5a9)] - **util**: fix Latin1 decoding to return string output (Mert Can Altin) [#&#8203;56222](nodejs/node#56222)
-   \[[`9e98e86604`](nodejs/node@9e98e86604)] - **util**: do not rely on mutable `Object` and `Function`' `constructor` prop (Antoine du Hamel) [#&#8203;56188](nodejs/node#56188)
-   \[[`374eb415fd`](nodejs/node@374eb415fd)] - **util**: add fast path for Latin1 decoding (Mert Can Altin) [#&#8203;55275](nodejs/node#55275)
-   \[[`bcfe9c80fc`](nodejs/node@bcfe9c80fc)] - **(SEMVER-MINOR)** **util**: add sourcemap support to getCallSites (Marco Ippolito) [#&#8203;55589](nodejs/node#55589)
-   \[[`2aa77c8a8f`](nodejs/node@2aa77c8a8f)] - **v8,tools**: expose experimental wasm revectorize feature (Yolanda-Chen) [#&#8203;54896](nodejs/node#54896)
-   \[[`bfd11d7661`](nodejs/node@bfd11d7661)] - **worker**: fix crash when a worker joins after exit (Stephen Belanger) [#&#8203;56191](nodejs/node#56191)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS45My4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTMuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: https://git.justinelmore.dev/jelmore1674/build-changelog/pulls/72
Co-authored-by: Renovate Bot <renovate-bot@forgejo.justinelmore.dev>
Co-committed-by: Renovate Bot <renovate-bot@forgejo.justinelmore.dev>
inkeep-oss-sync Bot pushed a commit to inkeep/open-knowledge that referenced this pull request Sep 15, 2026
* test(ok): pin empty-base reconcile contract as failing tests

Three empty-base cases pin the reconcile() refusal contract: identical
sides, divergent sides, and the incident shape of stale disk content
against an edited doc must all refuse with reason no-base instead of
returning merged with a self-concatenation. Two guardians pin the safe
empty-side cases that must keep working (empty ours adopts theirs as
clean; empty theirs keeps ours as noop). Two insert-convergence cases
pin that both sides adding the same block, at the same anchor or at the
end, yields it exactly once.

All five contract tests fail on the current implementation, as intended.
The fix lands in a follow-up commit.

PRD-8205

* test(ok): pin integration-tier absent-base reconcile contracts

Extends the PRD-8205 RED set beyond the unit tier with integration
contracts for the surfaces the fix plan touches. The decline-path and
over-multiplied tests drive the real factory, watcher, and durability
state through server-factory.test.ts; the tripwire undefined-base arm
drives the real store path through persistence-tripwire-paste.test.ts.
Two guardian pins (delete-path rescue, branch-switch round trip) pass
today and must keep passing after the fix. No production code changed.

* fix(ok): refuse empty-base reconcile and dedup convergent inserts

reconcile() with base '' and both sides non-empty ran the per-block
merge with zero base blocks: every block from both sides landed in
ops.get(0).insertsBefore and the trailing flush pushed both insert
groups unconditionally, so the outcome was a silent self-concatenation
reported as merged with zero conflicts. A disk update against a loaded
doc whose reconciled base was absent hit exactly this path via the
handler's ?? '' base coercion and doubled the document in the Y.Doc and
on disk.

After the theirs === base noop and ours === base clean short-circuits
(an empty side with an empty base stays a safe noop/clean, so those
must keep winning), a '' base with both sides non-empty now returns
{ kind: 'refused', reason: 'no-base' }.

The same unconditional dual push also duplicated a block both sides
inserted at the same anchor, including the trailing position. Insert
groups are now emitted through a multiset set-minus: theirs loses one
occurrence per identical block in ours, within-side duplicates are
untouched, mirroring the existing modify-convergence semantic that
pushes a convergent edit once.

* fix(ok): register reconcile-over-multiplied checkpoint kind end to end

The over-multiplied refusal mints a checkpoint under a new kind, and the
kind registry is exhaustively derived: ParsedCheckpoint, parseCheckpoint,
CHECKPOINT_KIND_REGISTRY, CHECKPOINT_SAMPLE_BY_KIND, the shadow-repo
InMemoryCheckpointParams union and save switch, CheckpointRetentionPolicy
(maxReconcileOverMultiplied, 50 matching every loss-site sibling),
CheckpointGcResult.deletedReconcileOverMultiplied, and GC_BUCKET_POLICY
(applyTtl) all gain the kind, with the two counters on the metrics
interface plus their zero-inits, increment functions, and reset entries.

The kind's satisfies-gates and the tests that enumerate the registry are
the enforcement surface for this fan-out: shadow-repo.test.ts's
compile-exhaustive seedKind switch seeds the kind (docName n.md), the
bucketing test pins maxReconcileOverMultiplied at 50 alongside its
siblings, and the TimelinePanel surfaced-kind fixture covers it so the
fail-closed registry check keeps deriving.

* fix(ok): decline absent-base reconciles and refuse over-multiplied merges

A disk update on a loaded doc whose reconciled base was absent coerced
the missing base to '' and called reconcile() anyway: with both sides
non-empty the merge produced a silent self-concatenation, applied it to
the Y.Doc, and the doubled content persisted to disk with no conflict,
no loss event, and result=merged at INFO. The same coercion family fed
the rescue dirty-check and the branch-park snapshot.

handleDiskEvent now reads the reconciled base as absence: an absent base
declines to reconcile and routes to applyDiskAuthoritativeIngest, which
captures the live doc synchronously, mints an external-change-rescue
checkpoint of ours through the extracted mintExternalChangeRescue helper
(site union gains disk-update) before any apply, then adopts the disk
bytes via applyToDoc, whose tail heals the missing base. An
acknowledged-empty base now refuses inside reconcile() and the refused
arm routes its no-base reason to the same helper. The rescue dirty-check
classifies absence explicitly (ours !== '' instead of a fabricated ''
base, behavior-preserving) and branch-park skips parking a doc whose
base is absent instead of parking the live content as its own disk
snapshot; restore-time behavior is equivalent (no parked record vs a
fabricated baseline that always restored as no-op).

The merged arm gains an over-multiplied post-condition before apply:
when the merged result exceeds 1.5x the larger input it sets lifecycle
status conflict with reason over-multiplied, counts a conflict, leaves
doc, base, and disk untouched, and fires the reconcile-over-multiplied
detection site (fire counter plus checkpoint-created counter, a
checkpoint of the refused candidate with oursBytes/theirsBytes/
mergedBytes metadata in its own GC bucket, and a loss-ring detector-trip
with which=growth, lostLen = merged length minus the larger input, and
the fnv1a digest). The [reconcile] log line now always carries oursLen
and theirsLen plus newLen and the delta over the larger input whenever
the outcome carries new content, so a future doubling is readable from
INFO alone.

The persistence duplication tripwire no longer fails open when the base
is absent: the reset path's safe disk read (existsSync, realpath,
content-dir check, readFileSync with fallback) is hoisted into
readTripwireResetDiskContent, an absent base classifies the candidate
against the disk content with full parity to the defined-base arm
including the settled-write spare, and the reset apply reuses the
already-read bytes. Unreadable disk classifies against empty and passes
new docs through.

* test(ok): revise reconcile red contract per audit convergence

Drop the over-multiplied integration RED (its growth-refusal
post-condition is removed as a false positive on conflict-free
unions) with its orphaned helpers and imports. Add two unit REDs:
a whitespace-only base with both sides non-empty must refuse with
reason no-base, and the insert dedup must preserve theirs' internal
order while emitting a shared block once.

* fix(ok): widen no-base reconcile guard and preserve remote insert order

The empty-base refusal matched only the exact empty string, so a
whitespace-only base fell through to the zero-anchor concat the guard
exists to prevent; the guard now refuses any base that splits to zero
blocks, hoisting the split so the too-large check and mergeBlocks share
it. The insert-group dedup pushed all of ours before any of theirs, so
a theirs-only block landing after a theirs block matched against ours
was emitted after ours' copy, reversing the remote's internal order;
it is now an order-preserving LCS interleave over the two lists that
emits blocks unique to either side in their own order and common
blocks once at their interleave position, with the existing
MAX_LCS_CELLS bound falling back to plain ours-then-theirs emission.

* fix(ok): remove over-multiplied reconcile refusal scaffolding

A merged union of two conflict-free insertions legitimately approaches
twice the larger input, so the 1.5x growth post-condition refused valid
collaboration, and size cannot distinguish duplication from addition;
the class is guarded instead by the zero-block-base refusal, the
order-preserving insert dedup, and the persistence tripwire. The
checkpoint kind, its parse arm and registry entry and sample, the
retention limit, GC bucket and result counter, the metrics counters
with their increment functions and reset entries, the merged-arm
refusal block with its loss-ring record, and the two registry-
enumerating test fixture entries are removed, restoring those surfaces
to their pre-scaffolding shapes; the changeset drops the over-
multiplied claim.

* fixup! local-review: address findings (pass 1)

* fixup! local-review: address findings (pass 2)

* fixup! local-review: address findings (pass 3)

* fix(ok): address the review of record on the empty-base reconcile branch

The full-scope review of record found two majors and seven minors in the
landed fix passes. The branch-switch reset arm minted a rescue for settled
docs on first-visit switches (the per-branch base map starts empty, so
every loaded doc reads as base-absent); it now mints only when the live
content differs from the disk content about to be applied, matching the
sibling ingest guard. The fail-closed duplication refusal recorded no
store failure, so agent writes got 200 while nothing reached disk;
agent-triggered stores now record an OK_STORE_REFUSED failure mapped to a
503 store-refused problem, and refused content is durably buffered once
per refused-mark transition. Also gates the missing-baseline counter to
lost bases instead of routine first stores, counts and warns when the
insert-dedup LCS cap falls back to the plain union, words the
delete-branch rescue-failure log for its destruction consequence, renames
the ingest log's rescued field to rescueMinted for its queued-not-persisted
semantics, and makes the refusal dispatch exhaustive with a never-typed
default.

* fixup! local-review: address findings (pass 1)

* fixup! local-review: address findings (pass 2)

* fixup! local-review: address findings (pass 3)

* fixup! local-review: address findings (pass 4)

* fixup! local-review: address findings (pass 5)

* test(ok): pin the rename teardown rescue checkpoint kind

The review of record's last open item: the rename row of the mint-site
checkpoint-kind map was the only teardown assignment no test could catch
reverting. The new rename-path rescue test drives a dirty doc through a
disk rename and pins the rescue's checkpoint kind; the one-token revert
of the rename row reds it (mutation verified and restored).

* test(ok): make the hostile-shim gate-runner fixtures PATH-independent

The two hostile-shim tests resolve the fixture's fake pnpm through the
ancestor shell's PATH, where the first entry was the relative
./node_modules/.bin; pnpm run normalizes that entry to the installing
workspace's absolute bin, so under check:drift (and the pre-push hook)
the shim is bypassed, the enumeration succeeds, and the runner exits 0
instead of the expected 2. Both tests now prepend the fixture's own
node_modules/.bin to the child PATH explicitly, making them independent
of how the invoking shell built PATH. Verified red under pnpm run before
and green after; unchanged green under direct vitest (144/144).

* docs(ok): note the refused-no-base conflict reason in the changeset

* docs(ok): bring the PRD-8205 changeset under the changelog budget

* fix(ok): refuse a reconcile whose base blocks are all blank

The no-base guard tested splitMarkdownBlocks(base).length === 0, but the
splitter's mid-loop flush pushes current.join('\n').trim() unconditionally
while its end-of-loop flush gates on truthiness. A base of '\n \n' therefore
split to [''], length 1, and sailed past the guard.

With that base, both sides' modify op resolved to the same first block, so
no conflict was pushed and every remaining block from both sides landed in
the trailing insert group. The call returned merged, not conflicts as the
report assumed, and the disk-event handler applied the near-doubled body to
the live document with no conflict raised and no user-visible signal.

The guard now tests that every base block is blank. every() returns true on
an empty array, so this subsumes the old condition. splitMarkdownBlocks is
left alone because it is exported from the package index.

The existing "whitespace-only base" case used '\n\n', which the trailing
newline strip reduces to '', so it could not fail independently of the
empty-string case above it. It now uses '\n \n', and a sibling case pins a
second whitespace shape against the doubling class directly.

Also carries dedupSkipped on the conflicts variant, which previously existed
only on merged even though both arms build newContent from the same array.

* fix(ok): list and expire nested files in the rescue buffer

The rescue writer builds nested directories: safeRescuePath takes the full
relative doc path and the writer mkdirs its dirname recursively. The only
surface that reads or expires the buffer enumerated it with a flat
readdirSync, so a directory entry failed isSupportedDocFile and any rescue
for a document below the content root's top level was never listed and never
reached the max-age unlink. The 503 detail and the MCP reference promise a
retrievable copy, and for most of a real knowledge base that promise was
false in one direction and the file leaked in the other.

The listing now walks recursively and normalizes platform separators to
forward slashes before stripping the extension, which is the exact inverse of
docNameToRelativePath. Directory entries are rejected by an explicit isFile
check rather than by isSupportedDocFile, which a directory named like a
document passes. Expiry prunes the ancestors it empties, stopping at the
first non-empty one and never above the rescue root.

This also reaches the pre-existing shutdown-rescue leak: rescueDocToShadowBuffer
writes through the same path helper for all three of its contexts, so nested
documents rescued at shutdown were equally unreachable before this change.

Teardown now clears refusedStoreDocNames and storeFailures as well. The clear
is hoisted above the persist branch so the early-return arm gets it too,
which is what the two teardown tests distinguish. Without it a refused
document that was deleted or renamed left a permanent name-keyed entry, and a
later document created with the same name was force-kept loaded and routed
through the refused-doc rescue path at shutdown.

* fix(ok): keep one filesystem fault on one error contract

readTripwireDiskContent returned a single unavailable variant for three
conditions, and the lost-base arm mapped all of them to OK_STORE_REFUSED. The
same conditions are enforced again later in the same onStoreDocument body,
where realpath failure and containment escape route through recordPathFault to
OK_PATH_UNRESOLVABLE. So an escaping symlink returned 400 and "repair the
path" on a document with an acknowledged base, and 503 and "retry" on a
document whose base was absent. An agent cannot write retry logic against a
contract selected by state it cannot observe.

The variant now carries reason 'escape' or 'read-failed'. The escape case
routes through recordPathFault as the store's own guard already does; a read
failure keeps the 503. Both still throw DuplicationBaselineUnavailableError,
so the deferred-store classification is unchanged.

One existing assertion flips to OK_PATH_UNRESOLVABLE. Its neighbour was named
for the unreadable case but provoked the escape, which made the read-failed
arm look covered when nothing pinned it; both are renamed for what they
actually drive, and a new test puts a directory at the document path so
readFileSync throws EISDIR after existsSync and containment both pass.

The report's symlink-cycle example does not reach this arm at all: existsSync
stats through the link chain and returns false on ELOOP, so a cycle exits at
the missing gate.

* fix(ok): bound the shutdown rescue pass and stop overstating the rescue

Three things in the apparatus around the refusal reported more than they knew.

mintExternalChangeRescue returns as soon as it sees a shadow handle, having
only scheduled the checkpoint write inside queueMicrotask, and the ingest then
overwrites live content synchronously. The success log called the result
rescueMinted, which attested only that a handle existed. It is now
rescueScheduled, which is what the value actually carries. Nothing asserted
the old key. The backup ordering and the apply-failure catch are deliberately
unchanged: ordering the write first needs an awaitable variant of a helper
three synchronous call sites share, and the peer catches on main have the same
disposition, so discriminating this one would spread the inconsistency rather
than close it.

The refused-store pass in flushAllStoresAndWait computed each document's
unload budget from a deadline the settle loop had already spent, so after the
first document the rest were handed zero milliseconds and then logged as
having missed a deadline they were never given time to meet. Each iteration
also issued an unconditional synchronous write with no remaining-time check,
against a filesystem these documents were refused on precisely because it was
failing. The pass now checks the remaining budget before each rescue and
breaks to the existing end-of-budget bulk rescue when it is gone, and the
unload deadline has a per-item floor. A document skipped by the new check
emits its own warn rather than vanishing from the shutdown log.

The rescue writer now goes through the traced fs wrappers, per the subtree's
STOP rule and matching the sibling write in document-durability-state. This
buys error attribution on a write that fails, not stall detection: withSpanSync
ends its span in a finally, so a write that never returns emits no span at all.

The insert-dedup skip was reported only on the merged arm, though the conflicts
arm builds its content from the same array and can carry the same LCS-cap
plain-union fallback. The increment and warning move to a small reporter called
on both arms and at the branch-switch parked-WIP site. The metrics call stays
out of reconciliation.ts, which imports one module and should stay pure.

* fix(ok): keep unknown conflict reasons from blanking their siblings

ConflictEntrySchema.reason sits inside an array the client parses whole, and
fetchConflicts returns an empty list with an error on any parse failure. So
widening the reason enum meant one entry carrying refused-no-base would hide
every other conflict in the same response for a client on an older core
bundle: a tab left open across an upgrade, or a pinned integration. It
self-heals on reload, but while it lasts the UI reports nothing to resolve
rather than one unknown item. Both reason fields this PR's widening reaches
now catch to undefined, which the field already allowed. Scope stays on those
two: conflict and conflictKind have the same exposure but came from #4102,
which also dropped a catch from conflictKind, and restoring tolerance on a
required discriminator is that owner's call.

strategiesFor falls through to the full strategy set for refused-no-base. The
set is correct and the PR body gives the reasoning, but nothing checked it, so
the value is now pinned alongside its siblings.

Documents the two disk-authoritative ingest counters and the insert-dedup skip
counter, and names the ingest pair in the bridge README's Fire column as the
sibling rows do.

The checkpoint-kind exception paragraph justified the split by claiming the
disk-update mints fire per differing disk event because the base heals only on
a later store. The code says otherwise: applyExternalChange re-seeds the
reconciled base as its last statement, its catch arm sets it, and the ingest's
catch sets it too, so the next disk event takes the ordinary reconcile path.
The real bound is about one mint per base-absence episode per document, and
the burst source is a first-visit branch switch installing a fresh empty
scope. The paragraph now says that. The kind keeps its own registry entry and
GC bucket, which a correct reading still supports.

* fix(ok): stop the block splitter from emitting a phantom empty block

splitMarkdownBlocks flushed mid-loop without the truthiness gate its
end-of-loop sibling already had, so any run of three or more blank lines
produced an empty string block. splitMarkdownBlocks('a\n\n\n\nb') returned
['a', '', 'b'].

That block is unmatched, so it reached the merge as real content. With
base 'A\n\nB\n', ours 'A\n\nC\n' and theirs 'A\n\n\n\nC\n', where both
sides make the same edit and the only difference is one extra blank line
on disk, reconcile returned a conflict whose theirs side was the empty
string instead of a clean merge. external-change.ts raises a user visible
conflict and returns NOT_RECONCILED on that outcome, so the phantom block
both manufactured a false conflict and dropped the user's disk write.

The guard that refuses an all blank base stays and remains correct: with
the splitter total, a blank base now yields no blocks at all and every()
on an empty array is true.

A differential over 20000 randomized documents confirms the new splitter
output equals the old output with empty strings filtered out, and nothing
else changes. Fence, tilde fence, table and blockquote handling are byte
identical.

* fix(ok): rescue live-only content before the disk-authoritative ingest adopts

The ingest minted its rescue checkpoint inside queueMicrotask and then
called applyToDoc synchronously. A microtask cannot run until the current
synchronous execution finishes, so the overwrite always completed before
the rescue write was even initiated. This was deterministic, not a race,
and the boolean the mint returned meant scheduled rather than durable.

The branch's own test pinned the consequence: with the shadow tree made
unwritable, live content was destroyed, zero rescue checkpoints existed,
and the reconcile line still reported result=clean with
rescueScheduled=true.

The ingest now takes a synchronous rescue-buffer write before the adopt
and keeps its definite rescued or lost answer. The git checkpoint mint
stays fire and forget as the richer artifact, since it was never the
durability guarantee. Reporting follows the outcome: the log carries
rescue=not-needed, rescued or lost, a lost rescue is emitted at warn as
result=clean-unrescued rather than clean, and
diskAuthoritativeIngestUnrescuedCount counts it.

An earlier round declined this on the ground that ordering the write
needed an awaitable variant of a helper three synchronous call sites
share. That was wrong. The mint's return value is consumed at exactly one
site and applyDiskAuthoritativeIngest was already async with both call
sites already awaiting it, so no such refactor was required.

Residual, stated rather than closed: when the shadow directory is wholly
unwritable both writes fail and the live-only content is still lost.
Closing that means refusing the inbound adopt, which relocates the loss to
the disk side and needs the user-visible channel this branch's Key
decisions rejected. The case is no longer silent.

* fix(ok): contain the rescue expiry sweep to the rescue directory

Making the rescue walk recursive changed the blast radius of its expiry
sweep. readdirSync with recursive:true descends symlinked subdirectories,
and containment was established only by string: resolve() for the unlink
and a startsWith() prefix test for the ancestor prune. A directory symlink
under the shadow rescue dir therefore yielded entries whose real targets
sat outside the rescue tree, and anything past the 24h threshold had its
real file unlinked and the real directories that held it removed.

Before the recursive walk this was not reachable: the only symlink a
top-level listing could produce is a leaf, and unlinkSync on a leaf
symlink removes the link rather than the target. The recursive walk is
what turns a link into a path prefix.

The sweep now resolves the rescue directory once, resolves each entry, and
skips with a warning any entry whose real location falls outside it. The
destructive calls operate on the canonical path, which also turns the
existing prune prefix test into a real containment check.

Note for future readers: withFileTypes:true is not a fix for this. The
asymmetry where it stopped recursion at a symlink was Node 21 behavior and
was resolved toward consistency by nodejs/node#55714. On the Node 24 this
repo pins, both forms descend, verified directly.

rmdirSync and the adjacent unlinkSync now go through the fs-traced
wrappers, per the subtree rule that server-side disk writes carry an fs
span. withSpanSync re-throws, so the prune loop's ENOTEMPTY terminator is
unaffected. The listing also carries Cache-Control: no-store on both of
its exits, matching the sibling handlers, now that its body is a recursive
inventory of document paths rather than a top-level slice.

* refactor(ok): make the tripwire baseline fault dispatch exhaustive

The escape versus read-failed split was consumed by an if/else whose
second arm keyed on agentTriggeredStore, a different predicate, so a third
member of the reason union would silently take the retryable 503
disposition rather than failing the compile.

This branch already argues the opposite discipline one union over:
wireReasonForRefusal uses a never-typed default precisely so a new
refusal reason cannot take an else branch, and it exists because a third
reason silently taking the wrong branch is the bug that was just fixed
there.

Dispatch is now a reason-first switch closed by a never binding. Behavior
is identical for both current members. The guard was checked rather than
assumed: adding a third member to the union yields
error TS2322: Type '"probe-third"' is not assignable to type 'never'
on the never line, and the probe was reverted.

* test(ok): pin the unknown-reason tolerance on the conflict-content schema

Two reason fields gained .catch(undefined) so one unrecognized value from
a newer server degrades per field instead of failing the whole parse, but
only the conflict-list site was pinned. The unpinned one is the schema
DiffViewBoundary parses: a parse failure there returns null and drops that
document's diff view, which is exactly the surface a version-skewed client
reaches when opening the conflict this branch's new reason produces.

The test is site specific, not incidental. Removing .catch(undefined) from
the conflict-content field alone fails this test and leaves the existing
conflict-list pin green.

* docs(ok): correct counter and checkpoint-kind claims that contradict the code

Three documentation claims written in the previous round describe behavior
the code shipped alongside them does not have.

The reconcileInsertDedupSkipped row said the counter is reached only on a
merged outcome. There are three increment sites, not one. The disk-event
reconcile and the park-restore pass both count merged and conflicts, and a
test on this branch asserts the conflicts case. The row now states the
rule that actually holds across all three: the counter follows whether the
dedup-skipped content is adopted. server-factory applies its conflicts
content, so it counts; external-change raises the conflict and returns
without adopting, so it does not.

The persistenceDuplicationBaselineRefusals row still pointed every reader
at the 503 and its retry-after-repair remediation. After the fault split,
a symlink escape returns 400 path-escape, whose own detail says retrying
will not help. The row now splits by outcome and quotes each response. It
also listed three baseline faults where the union carries two, so the
parenthetical named a distinction the code does not make.

The external-change-rescue exception paragraph justified the disk-update
kind split on a branch-switch burst of disk-update mints. The mapping
sends branch-switch to the teardown kind, and the handler's loop re-seeds
each document's base in the same synchronous pass, so a first-visit switch
bursts the bucket the split protects rather than the one it creates. The
premise was inverted, and the split is kept because the corrected premise
justifies it more directly than the old one did. The Fire column's claim
that a mint fires only for a dirty live doc is corrected too: only the
pre-intake mint is gated, and the apply-failure mint is not.

Adds the row for the counter that reports an ingest which adopted disk
bytes over live-only content with no durable backup.

* fix(ok): name the rescue call site on every buffer-write failure

rescueDocToShadowBuffer takes a context naming which of its four call
sites invoked it, but only the shadow-unavailable exit put that context
in the structured log object. The other three exits, and the success
line, carried it in the message string or not at all, so an operator
reading the structured stream could not tell whether a lost rescue came
from the fail-closed store refusal, the disk-authoritative ingest, the
shutdown refused-store pass or the flush timeout.

Cloud review raised this as an ingest-only gap on the apply-failure arm.
It was never ingest-only: the same blind spot covered all four callers.

No control flow or return value changes.

* test(ok): pin rescue call-site attribution and the ingest disjointness

Two pins on the disk-authoritative ingest.

The first extends the existing all-rescue-writes-fail test to assert the
write-failure error names its call site. Observed RED before the source
change: the structured object carried docName alone.

The second pins a decision cloud review asked us to reverse. It drives
the compound case -- rescue buffer lost AND applyToDoc throwing -- by
composing the two induction mechanisms already in this file, the shadow
chmod 0o500 and the getXmlFragment throw. It asserts the apply-failure
counter increments, the unrescued counter does NOT, no reconcile line is
emitted, and the live content survives.

The review proposed widening diskAuthoritativeIngestUnrescuedCount to
cover the apply-failure arm. That would make the counter's own
documented meaning false: on this arm the apply threw, so the live-only
content is still in the document, while the counter is defined as
content that may be recoverable from nowhere. The two populations are
deliberately disjoint, and this test is what records that rather than
leaving it an unwritten judgment a later change could reverse silently.

* test(ok): pin the unresolvable rescue entry skip in the sweep

The containment fix added two skip branches to the rescue sweep's
per-entry loop; only the isWithinDir one had a case. This drives the
other with a real dangling symlink rather than a mocked throw.

The guard sits at a trust boundary: readdirSync and realpathSync are
separate syscalls, so the enumeration snapshot is stale the instant it
is taken and the producer cannot enforce that every listed entry still
resolves.

The nesting is load-bearing. The dangling entry sits at the rescue-dir
root and the live file one level down, because Node's recursive
readdirSync drains a BFS queue and emits all root entries before any
subdirectory entry. With both at top level the ordering would be
filesystem-dependent and the test's power a coin flip.

Mutation-checked: with the guard replaced by a bare realpathSync the
throw escapes to the catch wrapping the whole loop, the listing is
abandoned rather than the single entry skipped, and the assertion fails.

* docs(ok): correct the mint-gating claim and the rescue counter scoping

The bridge README's Fire column for external-change-rescue-disk-update
said "a mint fires only for a dirty live doc". False on both mints. The
pre-intake mint is gated on liveOnlyContentAtRisk, which is live content
non-empty and different from the incoming disk bytes, not dirtiness
against a base that on this path is absent by construction. The
apply-failure mint sits in the catch arm with no conditional at all, so
the apply-failure counter counts its fires exactly. The branch's own
test already pinned this from outside: one failed ingest leaves two
disk-update checkpoints.

A prior disposition on this PR claimed the cell was corrected. It was
not; only the exception paragraph below it had been.

Two siblings of that claim went with it. The intake-denominator row said
the counter includes intakes where the document was empty or equal to
disk and no checkpoint was minted, which is false under the implication
reading since such an intake still mints if its apply throws. And the
detection-sites intro said the primitive mints from four call sites when
three exist; the four are the keys of CHECKPOINT_KIND_BY_MINT_SITE,
which is the code's own site vocabulary.

Separately, rescueBufferWriteFailures was scoped to "the shutdown/
timeout paths". Two of the writer's four callers are live request paths,
and the skipped-mint category the row already listed is reached only
from live disk-event and branch-switch paths. Of six increment sites
exactly one is shutdown-exclusive. The row now names every context and
its relationship to the unrescued counter. The apply-failure and
unrescued rows now state their disjointness, which was undocumented.

The rescue-reader sentence pointed at src/api-extension.ts, which
contains no rescue code; the reader is the /api/rescue handler in
http/config-system-routes.ts. That half is a pre-existing error,
repaired here because it is the same sentence whose scoping this branch
made stale.

* fix(ok): count an ingest whose apply destroyed live content as unrescued

The disk-authoritative ingest runs inside one Y.Doc transact, and Yjs cannot
roll one back. composeAndWriteRawBody replaces Y.Text('source') with the disk
bytes at bridge-intake.ts:129 and only then rebuilds the fragment at :133, so a
throw after that write leaves the document holding the disk bytes with the
live-only content already gone. The catch arm counted the apply failure and
stopped there, because both the unrescued counter and the clean-unrescued line
sit behind if (ingested). A lost rescue buffer plus a late apply failure
therefore destroyed content in the document and in the buffer and recorded
neither.

The arm now reads the document back instead of inferring survival from where
the throw landed. When the rescue write returned lost and the document no
longer holds the pre-intake content, the intake increments the unrescued
counter and emits result=clean-unrescued with applyFailed=true, which is the
discriminator for the overlap the two counters now have. A throw before the
Y.Text write keeps its existing behaviour exactly, and the pre-existing
compound-failure pin stays green without change. A document that cannot be read
back is counted as a loss, since under-counting is what this closes.

The new pin drives the throw from after the Y.Text write and asserts the state
that was previously unrecorded: the document holds the disk bytes, the live
paragraph is gone, no rescue buffer exists. All five rescue-writer exits now
render the call site in the message, matching the sibling primitive.

* fix(ok): keep one unstat-able entry from truncating the rescue walk

realpathSync and the containment check each warn and continue, but statSync had
no per-entry guard and defaults to throwIfNoEntry: true. An entry removed
between the readdirSync snapshot and its own stat escaped into the block-level
catch, which logs and then still answers 200 with whatever had been collected
first. This handler both enumerates and unlinks expired entries, so two
concurrent GET /api/rescue calls race by construction, and the walk drains
breadth-first, so the nested documents that motivated making it recursive are
what is lost first.

The per-entry tail from statSync through entries.push now sits inside a try
that warns and continues, matching the shape of the two adjacent guards. It is
catch-shaped rather than throwIfNoEntry: false so it covers EACCES, ELOOP and
EIO rather than only the missing-entry case, and it wraps the whole tail because
stat.mtime.toISOString() is a second escape on an out-of-range mtime.

The sweep pin added earlier only detected a regression while readdirSync
happened to emit the root entry first, which Node does not guarantee. It now
asserts the guard's own warning through a capturing logger passed via the
existing log override, so it holds whatever order the entries arrive in. With
the guard mutated out and the fixture inverted, the old array-only assertion
passed while the new warning assertion failed.

* docs(ok): correct the ingest counter correlation and the disjointness claim

The rescueBufferWriteFailures row closed with an unconditional rule: a lost
ingest rescue increments this counter and the unrescued one, so a spike with a
matching rise is the live ingest. The unrescued increment sat behind
if (ingested) while the rescue write runs before the apply, so an ingest whose
rescue was lost and whose apply then threw moved one and not the other. The two
rows three lines below stated that exclusion directly, which left the table
answering one triage question two opposite ways in a single commit.

The row now states the relationship directionally, so a flat unrescued counter
stops reading as an exoneration of the live ingest, and it defers the gate to
the rows that own it rather than restating it. It also warns off the magnitude
comparison: when the shadow repo is unavailable a single ingest increments this
counter twice, at the writer's shadow-unavailable exit and again at the skipped
mint for the same document.

Attribution is documented as two keys rather than one. The writer's four exits
carry context, the skipped mint carries site with a disjoint vocabulary, and the
flush-timeout no-shadow path carries neither, incrementing once per non-reserved
document behind a single aggregate warn. Selecting on either key alone
under-counts that path.

The apply-failure and unrescued rows now describe the overlap the counters
actually have, with applyFailed as its discriminator, and give the read-back as
the reason rather than an assumption about where the throw landed.

* fix(ok): correct the apply-failure recovery rows and pin the rescued arm

The diskAuthoritativeIngestApplyFailures row closed with two sentences
describing the pre-write arm as if it covered both. On a throw past the
Y.Text write the document already holds the disk bytes, so "the live
document never took it" was false and the recovery it implied, reading
the pre-intake content out of the running document, would fail. The row
now splits the two arms and points at the rescue buffer and the
external-change-rescue-disk-update checkpoint instead.

The rescueBufferWriteFailures magnitude example counted two increments
for a shadow-unavailable ingest. The apply-failure arm mints a second
time in the catch, so that ingest increments three times.

The unrescued gate's rescue === 'lost' operand had no case that could
fail on it: every apply-failure case left the other operand decisive.
Adds a post-Y.Text-write throw with the shadow writable, where only that
operand decides. Dropping the operand reds it on the unrescued counter.

* fix(ok): make the apply-failure recovery row executable from what is logged

Row 320's per-arm split told a responder to read the arm off
applyFailed: true. That field is emitted from the unrescued gate alone,
so an apply failure whose rescue buffer was written emits only the
generic error and carries no arm indicator, and its absence does not
mean the live content survived. The row now scopes the field to the
overlap it marks and says the arm is the read-back's answer.

Its recovery pointer also named external-change-rescue-disk-update
without saying which one: a failed ingest mints that kind twice for the
same document, once before the apply carrying the displaced live
content and once in the catch arm carrying the adopted disk bytes. The
row now names the earlier mint, the one whose content differs from the
file on disk.

The rescued-arm pin asserted its negative through two unchecked casts,
so renaming a payload field would have left it passing while observing
nothing. It now selects logged lines by message across warn, info and
error, anchors on the generic error being emitted once, and asserts no
clean-unrescued line for the document.

Adds the missing case for the rescue-listing guard's second escape, an
mtime that cannot be serialized, which the errno-parameterized test
could not reach because it injects at the statSync call.

GitOrigin-RevId: d0c38df40d3d969c92e9a5e3f38b44585ba4d98d
inkeep-oss-sync Bot pushed a commit to inkeep/open-knowledge that referenced this pull request Sep 15, 2026
* test(ok): pin empty-base reconcile contract as failing tests

Three empty-base cases pin the reconcile() refusal contract: identical
sides, divergent sides, and the incident shape of stale disk content
against an edited doc must all refuse with reason no-base instead of
returning merged with a self-concatenation. Two guardians pin the safe
empty-side cases that must keep working (empty ours adopts theirs as
clean; empty theirs keeps ours as noop). Two insert-convergence cases
pin that both sides adding the same block, at the same anchor or at the
end, yields it exactly once.

All five contract tests fail on the current implementation, as intended.
The fix lands in a follow-up commit.

PRD-8205

* test(ok): pin integration-tier absent-base reconcile contracts

Extends the PRD-8205 RED set beyond the unit tier with integration
contracts for the surfaces the fix plan touches. The decline-path and
over-multiplied tests drive the real factory, watcher, and durability
state through server-factory.test.ts; the tripwire undefined-base arm
drives the real store path through persistence-tripwire-paste.test.ts.
Two guardian pins (delete-path rescue, branch-switch round trip) pass
today and must keep passing after the fix. No production code changed.

* fix(ok): refuse empty-base reconcile and dedup convergent inserts

reconcile() with base '' and both sides non-empty ran the per-block
merge with zero base blocks: every block from both sides landed in
ops.get(0).insertsBefore and the trailing flush pushed both insert
groups unconditionally, so the outcome was a silent self-concatenation
reported as merged with zero conflicts. A disk update against a loaded
doc whose reconciled base was absent hit exactly this path via the
handler's ?? '' base coercion and doubled the document in the Y.Doc and
on disk.

After the theirs === base noop and ours === base clean short-circuits
(an empty side with an empty base stays a safe noop/clean, so those
must keep winning), a '' base with both sides non-empty now returns
{ kind: 'refused', reason: 'no-base' }.

The same unconditional dual push also duplicated a block both sides
inserted at the same anchor, including the trailing position. Insert
groups are now emitted through a multiset set-minus: theirs loses one
occurrence per identical block in ours, within-side duplicates are
untouched, mirroring the existing modify-convergence semantic that
pushes a convergent edit once.

* fix(ok): register reconcile-over-multiplied checkpoint kind end to end

The over-multiplied refusal mints a checkpoint under a new kind, and the
kind registry is exhaustively derived: ParsedCheckpoint, parseCheckpoint,
CHECKPOINT_KIND_REGISTRY, CHECKPOINT_SAMPLE_BY_KIND, the shadow-repo
InMemoryCheckpointParams union and save switch, CheckpointRetentionPolicy
(maxReconcileOverMultiplied, 50 matching every loss-site sibling),
CheckpointGcResult.deletedReconcileOverMultiplied, and GC_BUCKET_POLICY
(applyTtl) all gain the kind, with the two counters on the metrics
interface plus their zero-inits, increment functions, and reset entries.

The kind's satisfies-gates and the tests that enumerate the registry are
the enforcement surface for this fan-out: shadow-repo.test.ts's
compile-exhaustive seedKind switch seeds the kind (docName n.md), the
bucketing test pins maxReconcileOverMultiplied at 50 alongside its
siblings, and the TimelinePanel surfaced-kind fixture covers it so the
fail-closed registry check keeps deriving.

* fix(ok): decline absent-base reconciles and refuse over-multiplied merges

A disk update on a loaded doc whose reconciled base was absent coerced
the missing base to '' and called reconcile() anyway: with both sides
non-empty the merge produced a silent self-concatenation, applied it to
the Y.Doc, and the doubled content persisted to disk with no conflict,
no loss event, and result=merged at INFO. The same coercion family fed
the rescue dirty-check and the branch-park snapshot.

handleDiskEvent now reads the reconciled base as absence: an absent base
declines to reconcile and routes to applyDiskAuthoritativeIngest, which
captures the live doc synchronously, mints an external-change-rescue
checkpoint of ours through the extracted mintExternalChangeRescue helper
(site union gains disk-update) before any apply, then adopts the disk
bytes via applyToDoc, whose tail heals the missing base. An
acknowledged-empty base now refuses inside reconcile() and the refused
arm routes its no-base reason to the same helper. The rescue dirty-check
classifies absence explicitly (ours !== '' instead of a fabricated ''
base, behavior-preserving) and branch-park skips parking a doc whose
base is absent instead of parking the live content as its own disk
snapshot; restore-time behavior is equivalent (no parked record vs a
fabricated baseline that always restored as no-op).

The merged arm gains an over-multiplied post-condition before apply:
when the merged result exceeds 1.5x the larger input it sets lifecycle
status conflict with reason over-multiplied, counts a conflict, leaves
doc, base, and disk untouched, and fires the reconcile-over-multiplied
detection site (fire counter plus checkpoint-created counter, a
checkpoint of the refused candidate with oursBytes/theirsBytes/
mergedBytes metadata in its own GC bucket, and a loss-ring detector-trip
with which=growth, lostLen = merged length minus the larger input, and
the fnv1a digest). The [reconcile] log line now always carries oursLen
and theirsLen plus newLen and the delta over the larger input whenever
the outcome carries new content, so a future doubling is readable from
INFO alone.

The persistence duplication tripwire no longer fails open when the base
is absent: the reset path's safe disk read (existsSync, realpath,
content-dir check, readFileSync with fallback) is hoisted into
readTripwireResetDiskContent, an absent base classifies the candidate
against the disk content with full parity to the defined-base arm
including the settled-write spare, and the reset apply reuses the
already-read bytes. Unreadable disk classifies against empty and passes
new docs through.

* test(ok): revise reconcile red contract per audit convergence

Drop the over-multiplied integration RED (its growth-refusal
post-condition is removed as a false positive on conflict-free
unions) with its orphaned helpers and imports. Add two unit REDs:
a whitespace-only base with both sides non-empty must refuse with
reason no-base, and the insert dedup must preserve theirs' internal
order while emitting a shared block once.

* fix(ok): widen no-base reconcile guard and preserve remote insert order

The empty-base refusal matched only the exact empty string, so a
whitespace-only base fell through to the zero-anchor concat the guard
exists to prevent; the guard now refuses any base that splits to zero
blocks, hoisting the split so the too-large check and mergeBlocks share
it. The insert-group dedup pushed all of ours before any of theirs, so
a theirs-only block landing after a theirs block matched against ours
was emitted after ours' copy, reversing the remote's internal order;
it is now an order-preserving LCS interleave over the two lists that
emits blocks unique to either side in their own order and common
blocks once at their interleave position, with the existing
MAX_LCS_CELLS bound falling back to plain ours-then-theirs emission.

* fix(ok): remove over-multiplied reconcile refusal scaffolding

A merged union of two conflict-free insertions legitimately approaches
twice the larger input, so the 1.5x growth post-condition refused valid
collaboration, and size cannot distinguish duplication from addition;
the class is guarded instead by the zero-block-base refusal, the
order-preserving insert dedup, and the persistence tripwire. The
checkpoint kind, its parse arm and registry entry and sample, the
retention limit, GC bucket and result counter, the metrics counters
with their increment functions and reset entries, the merged-arm
refusal block with its loss-ring record, and the two registry-
enumerating test fixture entries are removed, restoring those surfaces
to their pre-scaffolding shapes; the changeset drops the over-
multiplied claim.

* fixup! local-review: address findings (pass 1)

* fixup! local-review: address findings (pass 2)

* fixup! local-review: address findings (pass 3)

* fix(ok): address the review of record on the empty-base reconcile branch

The full-scope review of record found two majors and seven minors in the
landed fix passes. The branch-switch reset arm minted a rescue for settled
docs on first-visit switches (the per-branch base map starts empty, so
every loaded doc reads as base-absent); it now mints only when the live
content differs from the disk content about to be applied, matching the
sibling ingest guard. The fail-closed duplication refusal recorded no
store failure, so agent writes got 200 while nothing reached disk;
agent-triggered stores now record an OK_STORE_REFUSED failure mapped to a
503 store-refused problem, and refused content is durably buffered once
per refused-mark transition. Also gates the missing-baseline counter to
lost bases instead of routine first stores, counts and warns when the
insert-dedup LCS cap falls back to the plain union, words the
delete-branch rescue-failure log for its destruction consequence, renames
the ingest log's rescued field to rescueMinted for its queued-not-persisted
semantics, and makes the refusal dispatch exhaustive with a never-typed
default.

* fixup! local-review: address findings (pass 1)

* fixup! local-review: address findings (pass 2)

* fixup! local-review: address findings (pass 3)

* fixup! local-review: address findings (pass 4)

* fixup! local-review: address findings (pass 5)

* test(ok): pin the rename teardown rescue checkpoint kind

The review of record's last open item: the rename row of the mint-site
checkpoint-kind map was the only teardown assignment no test could catch
reverting. The new rename-path rescue test drives a dirty doc through a
disk rename and pins the rescue's checkpoint kind; the one-token revert
of the rename row reds it (mutation verified and restored).

* test(ok): make the hostile-shim gate-runner fixtures PATH-independent

The two hostile-shim tests resolve the fixture's fake pnpm through the
ancestor shell's PATH, where the first entry was the relative
./node_modules/.bin; pnpm run normalizes that entry to the installing
workspace's absolute bin, so under check:drift (and the pre-push hook)
the shim is bypassed, the enumeration succeeds, and the runner exits 0
instead of the expected 2. Both tests now prepend the fixture's own
node_modules/.bin to the child PATH explicitly, making them independent
of how the invoking shell built PATH. Verified red under pnpm run before
and green after; unchanged green under direct vitest (144/144).

* docs(ok): note the refused-no-base conflict reason in the changeset

* docs(ok): bring the PRD-8205 changeset under the changelog budget

* fix(ok): refuse a reconcile whose base blocks are all blank

The no-base guard tested splitMarkdownBlocks(base).length === 0, but the
splitter's mid-loop flush pushes current.join('\n').trim() unconditionally
while its end-of-loop flush gates on truthiness. A base of '\n \n' therefore
split to [''], length 1, and sailed past the guard.

With that base, both sides' modify op resolved to the same first block, so
no conflict was pushed and every remaining block from both sides landed in
the trailing insert group. The call returned merged, not conflicts as the
report assumed, and the disk-event handler applied the near-doubled body to
the live document with no conflict raised and no user-visible signal.

The guard now tests that every base block is blank. every() returns true on
an empty array, so this subsumes the old condition. splitMarkdownBlocks is
left alone because it is exported from the package index.

The existing "whitespace-only base" case used '\n\n', which the trailing
newline strip reduces to '', so it could not fail independently of the
empty-string case above it. It now uses '\n \n', and a sibling case pins a
second whitespace shape against the doubling class directly.

Also carries dedupSkipped on the conflicts variant, which previously existed
only on merged even though both arms build newContent from the same array.

* fix(ok): list and expire nested files in the rescue buffer

The rescue writer builds nested directories: safeRescuePath takes the full
relative doc path and the writer mkdirs its dirname recursively. The only
surface that reads or expires the buffer enumerated it with a flat
readdirSync, so a directory entry failed isSupportedDocFile and any rescue
for a document below the content root's top level was never listed and never
reached the max-age unlink. The 503 detail and the MCP reference promise a
retrievable copy, and for most of a real knowledge base that promise was
false in one direction and the file leaked in the other.

The listing now walks recursively and normalizes platform separators to
forward slashes before stripping the extension, which is the exact inverse of
docNameToRelativePath. Directory entries are rejected by an explicit isFile
check rather than by isSupportedDocFile, which a directory named like a
document passes. Expiry prunes the ancestors it empties, stopping at the
first non-empty one and never above the rescue root.

This also reaches the pre-existing shutdown-rescue leak: rescueDocToShadowBuffer
writes through the same path helper for all three of its contexts, so nested
documents rescued at shutdown were equally unreachable before this change.

Teardown now clears refusedStoreDocNames and storeFailures as well. The clear
is hoisted above the persist branch so the early-return arm gets it too,
which is what the two teardown tests distinguish. Without it a refused
document that was deleted or renamed left a permanent name-keyed entry, and a
later document created with the same name was force-kept loaded and routed
through the refused-doc rescue path at shutdown.

* fix(ok): keep one filesystem fault on one error contract

readTripwireDiskContent returned a single unavailable variant for three
conditions, and the lost-base arm mapped all of them to OK_STORE_REFUSED. The
same conditions are enforced again later in the same onStoreDocument body,
where realpath failure and containment escape route through recordPathFault to
OK_PATH_UNRESOLVABLE. So an escaping symlink returned 400 and "repair the
path" on a document with an acknowledged base, and 503 and "retry" on a
document whose base was absent. An agent cannot write retry logic against a
contract selected by state it cannot observe.

The variant now carries reason 'escape' or 'read-failed'. The escape case
routes through recordPathFault as the store's own guard already does; a read
failure keeps the 503. Both still throw DuplicationBaselineUnavailableError,
so the deferred-store classification is unchanged.

One existing assertion flips to OK_PATH_UNRESOLVABLE. Its neighbour was named
for the unreadable case but provoked the escape, which made the read-failed
arm look covered when nothing pinned it; both are renamed for what they
actually drive, and a new test puts a directory at the document path so
readFileSync throws EISDIR after existsSync and containment both pass.

The report's symlink-cycle example does not reach this arm at all: existsSync
stats through the link chain and returns false on ELOOP, so a cycle exits at
the missing gate.

* fix(ok): bound the shutdown rescue pass and stop overstating the rescue

Three things in the apparatus around the refusal reported more than they knew.

mintExternalChangeRescue returns as soon as it sees a shadow handle, having
only scheduled the checkpoint write inside queueMicrotask, and the ingest then
overwrites live content synchronously. The success log called the result
rescueMinted, which attested only that a handle existed. It is now
rescueScheduled, which is what the value actually carries. Nothing asserted
the old key. The backup ordering and the apply-failure catch are deliberately
unchanged: ordering the write first needs an awaitable variant of a helper
three synchronous call sites share, and the peer catches on main have the same
disposition, so discriminating this one would spread the inconsistency rather
than close it.

The refused-store pass in flushAllStoresAndWait computed each document's
unload budget from a deadline the settle loop had already spent, so after the
first document the rest were handed zero milliseconds and then logged as
having missed a deadline they were never given time to meet. Each iteration
also issued an unconditional synchronous write with no remaining-time check,
against a filesystem these documents were refused on precisely because it was
failing. The pass now checks the remaining budget before each rescue and
breaks to the existing end-of-budget bulk rescue when it is gone, and the
unload deadline has a per-item floor. A document skipped by the new check
emits its own warn rather than vanishing from the shutdown log.

The rescue writer now goes through the traced fs wrappers, per the subtree's
STOP rule and matching the sibling write in document-durability-state. This
buys error attribution on a write that fails, not stall detection: withSpanSync
ends its span in a finally, so a write that never returns emits no span at all.

The insert-dedup skip was reported only on the merged arm, though the conflicts
arm builds its content from the same array and can carry the same LCS-cap
plain-union fallback. The increment and warning move to a small reporter called
on both arms and at the branch-switch parked-WIP site. The metrics call stays
out of reconciliation.ts, which imports one module and should stay pure.

* fix(ok): keep unknown conflict reasons from blanking their siblings

ConflictEntrySchema.reason sits inside an array the client parses whole, and
fetchConflicts returns an empty list with an error on any parse failure. So
widening the reason enum meant one entry carrying refused-no-base would hide
every other conflict in the same response for a client on an older core
bundle: a tab left open across an upgrade, or a pinned integration. It
self-heals on reload, but while it lasts the UI reports nothing to resolve
rather than one unknown item. Both reason fields this PR's widening reaches
now catch to undefined, which the field already allowed. Scope stays on those
two: conflict and conflictKind have the same exposure but came from #4102,
which also dropped a catch from conflictKind, and restoring tolerance on a
required discriminator is that owner's call.

strategiesFor falls through to the full strategy set for refused-no-base. The
set is correct and the PR body gives the reasoning, but nothing checked it, so
the value is now pinned alongside its siblings.

Documents the two disk-authoritative ingest counters and the insert-dedup skip
counter, and names the ingest pair in the bridge README's Fire column as the
sibling rows do.

The checkpoint-kind exception paragraph justified the split by claiming the
disk-update mints fire per differing disk event because the base heals only on
a later store. The code says otherwise: applyExternalChange re-seeds the
reconciled base as its last statement, its catch arm sets it, and the ingest's
catch sets it too, so the next disk event takes the ordinary reconcile path.
The real bound is about one mint per base-absence episode per document, and
the burst source is a first-visit branch switch installing a fresh empty
scope. The paragraph now says that. The kind keeps its own registry entry and
GC bucket, which a correct reading still supports.

* fix(ok): stop the block splitter from emitting a phantom empty block

splitMarkdownBlocks flushed mid-loop without the truthiness gate its
end-of-loop sibling already had, so any run of three or more blank lines
produced an empty string block. splitMarkdownBlocks('a\n\n\n\nb') returned
['a', '', 'b'].

That block is unmatched, so it reached the merge as real content. With
base 'A\n\nB\n', ours 'A\n\nC\n' and theirs 'A\n\n\n\nC\n', where both
sides make the same edit and the only difference is one extra blank line
on disk, reconcile returned a conflict whose theirs side was the empty
string instead of a clean merge. external-change.ts raises a user visible
conflict and returns NOT_RECONCILED on that outcome, so the phantom block
both manufactured a false conflict and dropped the user's disk write.

The guard that refuses an all blank base stays and remains correct: with
the splitter total, a blank base now yields no blocks at all and every()
on an empty array is true.

A differential over 20000 randomized documents confirms the new splitter
output equals the old output with empty strings filtered out, and nothing
else changes. Fence, tilde fence, table and blockquote handling are byte
identical.

* fix(ok): rescue live-only content before the disk-authoritative ingest adopts

The ingest minted its rescue checkpoint inside queueMicrotask and then
called applyToDoc synchronously. A microtask cannot run until the current
synchronous execution finishes, so the overwrite always completed before
the rescue write was even initiated. This was deterministic, not a race,
and the boolean the mint returned meant scheduled rather than durable.

The branch's own test pinned the consequence: with the shadow tree made
unwritable, live content was destroyed, zero rescue checkpoints existed,
and the reconcile line still reported result=clean with
rescueScheduled=true.

The ingest now takes a synchronous rescue-buffer write before the adopt
and keeps its definite rescued or lost answer. The git checkpoint mint
stays fire and forget as the richer artifact, since it was never the
durability guarantee. Reporting follows the outcome: the log carries
rescue=not-needed, rescued or lost, a lost rescue is emitted at warn as
result=clean-unrescued rather than clean, and
diskAuthoritativeIngestUnrescuedCount counts it.

An earlier round declined this on the ground that ordering the write
needed an awaitable variant of a helper three synchronous call sites
share. That was wrong. The mint's return value is consumed at exactly one
site and applyDiskAuthoritativeIngest was already async with both call
sites already awaiting it, so no such refactor was required.

Residual, stated rather than closed: when the shadow directory is wholly
unwritable both writes fail and the live-only content is still lost.
Closing that means refusing the inbound adopt, which relocates the loss to
the disk side and needs the user-visible channel this branch's Key
decisions rejected. The case is no longer silent.

* fix(ok): contain the rescue expiry sweep to the rescue directory

Making the rescue walk recursive changed the blast radius of its expiry
sweep. readdirSync with recursive:true descends symlinked subdirectories,
and containment was established only by string: resolve() for the unlink
and a startsWith() prefix test for the ancestor prune. A directory symlink
under the shadow rescue dir therefore yielded entries whose real targets
sat outside the rescue tree, and anything past the 24h threshold had its
real file unlinked and the real directories that held it removed.

Before the recursive walk this was not reachable: the only symlink a
top-level listing could produce is a leaf, and unlinkSync on a leaf
symlink removes the link rather than the target. The recursive walk is
what turns a link into a path prefix.

The sweep now resolves the rescue directory once, resolves each entry, and
skips with a warning any entry whose real location falls outside it. The
destructive calls operate on the canonical path, which also turns the
existing prune prefix test into a real containment check.

Note for future readers: withFileTypes:true is not a fix for this. The
asymmetry where it stopped recursion at a symlink was Node 21 behavior and
was resolved toward consistency by nodejs/node#55714. On the Node 24 this
repo pins, both forms descend, verified directly.

rmdirSync and the adjacent unlinkSync now go through the fs-traced
wrappers, per the subtree rule that server-side disk writes carry an fs
span. withSpanSync re-throws, so the prune loop's ENOTEMPTY terminator is
unaffected. The listing also carries Cache-Control: no-store on both of
its exits, matching the sibling handlers, now that its body is a recursive
inventory of document paths rather than a top-level slice.

* refactor(ok): make the tripwire baseline fault dispatch exhaustive

The escape versus read-failed split was consumed by an if/else whose
second arm keyed on agentTriggeredStore, a different predicate, so a third
member of the reason union would silently take the retryable 503
disposition rather than failing the compile.

This branch already argues the opposite discipline one union over:
wireReasonForRefusal uses a never-typed default precisely so a new
refusal reason cannot take an else branch, and it exists because a third
reason silently taking the wrong branch is the bug that was just fixed
there.

Dispatch is now a reason-first switch closed by a never binding. Behavior
is identical for both current members. The guard was checked rather than
assumed: adding a third member to the union yields
error TS2322: Type '"probe-third"' is not assignable to type 'never'
on the never line, and the probe was reverted.

* test(ok): pin the unknown-reason tolerance on the conflict-content schema

Two reason fields gained .catch(undefined) so one unrecognized value from
a newer server degrades per field instead of failing the whole parse, but
only the conflict-list site was pinned. The unpinned one is the schema
DiffViewBoundary parses: a parse failure there returns null and drops that
document's diff view, which is exactly the surface a version-skewed client
reaches when opening the conflict this branch's new reason produces.

The test is site specific, not incidental. Removing .catch(undefined) from
the conflict-content field alone fails this test and leaves the existing
conflict-list pin green.

* docs(ok): correct counter and checkpoint-kind claims that contradict the code

Three documentation claims written in the previous round describe behavior
the code shipped alongside them does not have.

The reconcileInsertDedupSkipped row said the counter is reached only on a
merged outcome. There are three increment sites, not one. The disk-event
reconcile and the park-restore pass both count merged and conflicts, and a
test on this branch asserts the conflicts case. The row now states the
rule that actually holds across all three: the counter follows whether the
dedup-skipped content is adopted. server-factory applies its conflicts
content, so it counts; external-change raises the conflict and returns
without adopting, so it does not.

The persistenceDuplicationBaselineRefusals row still pointed every reader
at the 503 and its retry-after-repair remediation. After the fault split,
a symlink escape returns 400 path-escape, whose own detail says retrying
will not help. The row now splits by outcome and quotes each response. It
also listed three baseline faults where the union carries two, so the
parenthetical named a distinction the code does not make.

The external-change-rescue exception paragraph justified the disk-update
kind split on a branch-switch burst of disk-update mints. The mapping
sends branch-switch to the teardown kind, and the handler's loop re-seeds
each document's base in the same synchronous pass, so a first-visit switch
bursts the bucket the split protects rather than the one it creates. The
premise was inverted, and the split is kept because the corrected premise
justifies it more directly than the old one did. The Fire column's claim
that a mint fires only for a dirty live doc is corrected too: only the
pre-intake mint is gated, and the apply-failure mint is not.

Adds the row for the counter that reports an ingest which adopted disk
bytes over live-only content with no durable backup.

* fix(ok): name the rescue call site on every buffer-write failure

rescueDocToShadowBuffer takes a context naming which of its four call
sites invoked it, but only the shadow-unavailable exit put that context
in the structured log object. The other three exits, and the success
line, carried it in the message string or not at all, so an operator
reading the structured stream could not tell whether a lost rescue came
from the fail-closed store refusal, the disk-authoritative ingest, the
shutdown refused-store pass or the flush timeout.

Cloud review raised this as an ingest-only gap on the apply-failure arm.
It was never ingest-only: the same blind spot covered all four callers.

No control flow or return value changes.

* test(ok): pin rescue call-site attribution and the ingest disjointness

Two pins on the disk-authoritative ingest.

The first extends the existing all-rescue-writes-fail test to assert the
write-failure error names its call site. Observed RED before the source
change: the structured object carried docName alone.

The second pins a decision cloud review asked us to reverse. It drives
the compound case -- rescue buffer lost AND applyToDoc throwing -- by
composing the two induction mechanisms already in this file, the shadow
chmod 0o500 and the getXmlFragment throw. It asserts the apply-failure
counter increments, the unrescued counter does NOT, no reconcile line is
emitted, and the live content survives.

The review proposed widening diskAuthoritativeIngestUnrescuedCount to
cover the apply-failure arm. That would make the counter's own
documented meaning false: on this arm the apply threw, so the live-only
content is still in the document, while the counter is defined as
content that may be recoverable from nowhere. The two populations are
deliberately disjoint, and this test is what records that rather than
leaving it an unwritten judgment a later change could reverse silently.

* test(ok): pin the unresolvable rescue entry skip in the sweep

The containment fix added two skip branches to the rescue sweep's
per-entry loop; only the isWithinDir one had a case. This drives the
other with a real dangling symlink rather than a mocked throw.

The guard sits at a trust boundary: readdirSync and realpathSync are
separate syscalls, so the enumeration snapshot is stale the instant it
is taken and the producer cannot enforce that every listed entry still
resolves.

The nesting is load-bearing. The dangling entry sits at the rescue-dir
root and the live file one level down, because Node's recursive
readdirSync drains a BFS queue and emits all root entries before any
subdirectory entry. With both at top level the ordering would be
filesystem-dependent and the test's power a coin flip.

Mutation-checked: with the guard replaced by a bare realpathSync the
throw escapes to the catch wrapping the whole loop, the listing is
abandoned rather than the single entry skipped, and the assertion fails.

* docs(ok): correct the mint-gating claim and the rescue counter scoping

The bridge README's Fire column for external-change-rescue-disk-update
said "a mint fires only for a dirty live doc". False on both mints. The
pre-intake mint is gated on liveOnlyContentAtRisk, which is live content
non-empty and different from the incoming disk bytes, not dirtiness
against a base that on this path is absent by construction. The
apply-failure mint sits in the catch arm with no conditional at all, so
the apply-failure counter counts its fires exactly. The branch's own
test already pinned this from outside: one failed ingest leaves two
disk-update checkpoints.

A prior disposition on this PR claimed the cell was corrected. It was
not; only the exception paragraph below it had been.

Two siblings of that claim went with it. The intake-denominator row said
the counter includes intakes where the document was empty or equal to
disk and no checkpoint was minted, which is false under the implication
reading since such an intake still mints if its apply throws. And the
detection-sites intro said the primitive mints from four call sites when
three exist; the four are the keys of CHECKPOINT_KIND_BY_MINT_SITE,
which is the code's own site vocabulary.

Separately, rescueBufferWriteFailures was scoped to "the shutdown/
timeout paths". Two of the writer's four callers are live request paths,
and the skipped-mint category the row already listed is reached only
from live disk-event and branch-switch paths. Of six increment sites
exactly one is shutdown-exclusive. The row now names every context and
its relationship to the unrescued counter. The apply-failure and
unrescued rows now state their disjointness, which was undocumented.

The rescue-reader sentence pointed at src/api-extension.ts, which
contains no rescue code; the reader is the /api/rescue handler in
http/config-system-routes.ts. That half is a pre-existing error,
repaired here because it is the same sentence whose scoping this branch
made stale.

* fix(ok): count an ingest whose apply destroyed live content as unrescued

The disk-authoritative ingest runs inside one Y.Doc transact, and Yjs cannot
roll one back. composeAndWriteRawBody replaces Y.Text('source') with the disk
bytes at bridge-intake.ts:129 and only then rebuilds the fragment at :133, so a
throw after that write leaves the document holding the disk bytes with the
live-only content already gone. The catch arm counted the apply failure and
stopped there, because both the unrescued counter and the clean-unrescued line
sit behind if (ingested). A lost rescue buffer plus a late apply failure
therefore destroyed content in the document and in the buffer and recorded
neither.

The arm now reads the document back instead of inferring survival from where
the throw landed. When the rescue write returned lost and the document no
longer holds the pre-intake content, the intake increments the unrescued
counter and emits result=clean-unrescued with applyFailed=true, which is the
discriminator for the overlap the two counters now have. A throw before the
Y.Text write keeps its existing behaviour exactly, and the pre-existing
compound-failure pin stays green without change. A document that cannot be read
back is counted as a loss, since under-counting is what this closes.

The new pin drives the throw from after the Y.Text write and asserts the state
that was previously unrecorded: the document holds the disk bytes, the live
paragraph is gone, no rescue buffer exists. All five rescue-writer exits now
render the call site in the message, matching the sibling primitive.

* fix(ok): keep one unstat-able entry from truncating the rescue walk

realpathSync and the containment check each warn and continue, but statSync had
no per-entry guard and defaults to throwIfNoEntry: true. An entry removed
between the readdirSync snapshot and its own stat escaped into the block-level
catch, which logs and then still answers 200 with whatever had been collected
first. This handler both enumerates and unlinks expired entries, so two
concurrent GET /api/rescue calls race by construction, and the walk drains
breadth-first, so the nested documents that motivated making it recursive are
what is lost first.

The per-entry tail from statSync through entries.push now sits inside a try
that warns and continues, matching the shape of the two adjacent guards. It is
catch-shaped rather than throwIfNoEntry: false so it covers EACCES, ELOOP and
EIO rather than only the missing-entry case, and it wraps the whole tail because
stat.mtime.toISOString() is a second escape on an out-of-range mtime.

The sweep pin added earlier only detected a regression while readdirSync
happened to emit the root entry first, which Node does not guarantee. It now
asserts the guard's own warning through a capturing logger passed via the
existing log override, so it holds whatever order the entries arrive in. With
the guard mutated out and the fixture inverted, the old array-only assertion
passed while the new warning assertion failed.

* docs(ok): correct the ingest counter correlation and the disjointness claim

The rescueBufferWriteFailures row closed with an unconditional rule: a lost
ingest rescue increments this counter and the unrescued one, so a spike with a
matching rise is the live ingest. The unrescued increment sat behind
if (ingested) while the rescue write runs before the apply, so an ingest whose
rescue was lost and whose apply then threw moved one and not the other. The two
rows three lines below stated that exclusion directly, which left the table
answering one triage question two opposite ways in a single commit.

The row now states the relationship directionally, so a flat unrescued counter
stops reading as an exoneration of the live ingest, and it defers the gate to
the rows that own it rather than restating it. It also warns off the magnitude
comparison: when the shadow repo is unavailable a single ingest increments this
counter twice, at the writer's shadow-unavailable exit and again at the skipped
mint for the same document.

Attribution is documented as two keys rather than one. The writer's four exits
carry context, the skipped mint carries site with a disjoint vocabulary, and the
flush-timeout no-shadow path carries neither, incrementing once per non-reserved
document behind a single aggregate warn. Selecting on either key alone
under-counts that path.

The apply-failure and unrescued rows now describe the overlap the counters
actually have, with applyFailed as its discriminator, and give the read-back as
the reason rather than an assumption about where the throw landed.

* fix(ok): correct the apply-failure recovery rows and pin the rescued arm

The diskAuthoritativeIngestApplyFailures row closed with two sentences
describing the pre-write arm as if it covered both. On a throw past the
Y.Text write the document already holds the disk bytes, so "the live
document never took it" was false and the recovery it implied, reading
the pre-intake content out of the running document, would fail. The row
now splits the two arms and points at the rescue buffer and the
external-change-rescue-disk-update checkpoint instead.

The rescueBufferWriteFailures magnitude example counted two increments
for a shadow-unavailable ingest. The apply-failure arm mints a second
time in the catch, so that ingest increments three times.

The unrescued gate's rescue === 'lost' operand had no case that could
fail on it: every apply-failure case left the other operand decisive.
Adds a post-Y.Text-write throw with the shadow writable, where only that
operand decides. Dropping the operand reds it on the unrescued counter.

* fix(ok): make the apply-failure recovery row executable from what is logged

Row 320's per-arm split told a responder to read the arm off
applyFailed: true. That field is emitted from the unrescued gate alone,
so an apply failure whose rescue buffer was written emits only the
generic error and carries no arm indicator, and its absence does not
mean the live content survived. The row now scopes the field to the
overlap it marks and says the arm is the read-back's answer.

Its recovery pointer also named external-change-rescue-disk-update
without saying which one: a failed ingest mints that kind twice for the
same document, once before the apply carrying the displaced live
content and once in the catch arm carrying the adopted disk bytes. The
row now names the earlier mint, the one whose content differs from the
file on disk.

The rescued-arm pin asserted its negative through two unchecked casts,
so renaming a payload field would have left it passing while observing
nothing. It now selects logged lines by message across warn, info and
error, anchors on the generic error being emitted once, and asserts no
clean-unrescued line for the document.

Adds the missing case for the rescue-listing guard's second escape, an
mtime that cannot be serialized, which the errno-parameterized test
could not reach because it injects at the statSync call.

GitOrigin-RevId: d0c38df40d3d969c92e9a5e3f38b44585ba4d98d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fs Issues and PRs related to file-system APIs and the fs module. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

withFileTypes has different behavior in fs.readdir when reading symbolic directories

5 participants