refactor(file-types): unify FilePath as Zod-branded canonical path#16740
Open
EurFelux wants to merge 21 commits into
Open
refactor(file-types): unify FilePath as Zod-branded canonical path#16740EurFelux wants to merge 21 commits into
EurFelux wants to merge 21 commits into
Conversation
EurFelux
commented
Jul 4, 2026
| export function toSafeFileUrl(absolutePath: FilePath, ext: string | null): FileUrlString { | ||
| const effectivePath = isDangerExt(ext) ? dirnameSimple(absolutePath) : absolutePath | ||
| return toFileUrl(effectivePath as FilePath) | ||
| return toFileUrl(FilePathSchema.parse(effectivePath)) |
Collaborator
Author
There was a problem hiding this comment.
[R14] 建议 · 微优化:toSafeFileUrl 对已 branded 的 FilePath 每次调用都再跑一遍 FilePathSchema.parse(),包括每个 <img>/<video> 渲染。
同类冗余:pathResolver.ts:49(internal 分支)、FileManager.ts:926。均幂等无害,只是可避免的工作。非危险分支(isDangerExt)可直接 toFileUrl(absolutePath),仅在 dirnameSimple 改写后的路径上才需重 parse。(dirnameSimple 对 C:\ 根的修正本身正确且必要。)
Collaborator
Author
There was a problem hiding this comment.
[R14] 部分处理:FileManager batch 那处冗余 re-parse 已随 A3 移除(548402a4)。url.ts:236 这处保留 —— FilePath 现在是 shape-only,FilePathSchema.parse 只做一次廉价形状校验(不再跑 canonicalize),"可避免的工作"已从 canonicalize 降到近乎零,留着是为在 dirnameSimple 改写后仍保证 brand 一致。若你仍想省掉,可在非危险分支直接 toFileUrl(absolutePath) —— 说一声我就改。
Add a Zod schema that refines an absolute path, canonicalizes it via the shared canonicalizeAbsolutePath, and brands the output as FilePath. Additive for now; FilePath stays a template literal until the sweep in the next commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
Flip FilePath from a template literal to z.infer<typeof FilePathSchema>. Delete AbsolutePathSchema, CanonicalExternalPath, and CanonicalFilePath; route every schema field, service signature, and IPC boundary through FilePathSchema / FilePath. Drop the canonicalizeExternalPath wrapper and the manual path.resolve in resolvePhysicalPath — canonicalization now happens in the schema transform at the parse boundary. FileInfo/FilePathHandle path fields brand at parse, removing the FileInfo Omit hack. DB schema and IPC wire format unchanged (the brand is a phantom Zod brand). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
Forbid `as FilePath` in production code (CI error / local warn); exempt tests and the raw-OS-path watcher/tree regimes. Convert the canonicalizable production casts to FilePathSchema.parse() and drop the ones made redundant by the branded IPC output schemas. Fix a latent inconsistency in url.ts's dirnameSimple surfaced by the new validation: the Windows-drive-root case dropped its trailing separator (`C:\payload.exe` -> `C:`), which FilePathSchema rejects as non-absolute; the POSIX-root case already preserved it (`/payload.exe` -> `/`). Preserve it for Windows too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
Replace AbsolutePathSchema / CanonicalExternalPath / CanonicalFilePath and the canonicalizeExternalPath wrapper with the single FilePathSchema / FilePath brand throughout the file-manager reference docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
…ernalPath param ArtifactPane's HTML baseUrl computation used FilePathSchema.parse during render, which throws on a non-absolute joined path; switch to safeParse and fall back to undefined (HtmlPreviewFrame's default base) on failure, matching the safeParse guard pattern used by the other v2 FilePath render sites in this branch. FileManager.findByExternalPath took a plain string but internally parsed it with FilePathSchema.parse, which now throws on non-absolute input instead of resolving it — the string param no longer told the truth. Only test callers exist, so retype the param to FilePath (dropping the now-redundant inner parse) and update the integration test to construct FilePath values via FilePathSchema.parse at the call site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
The schema↔type wiring for FileHandle (TODO #1) is not complete: wiring the `path` field to `FilePathSchema` fixed only that field, while `as FileHandle` casts remain at the IPC handlers and `FileManager` (parse boundary), and `Base64String` / `UrlString` still have no runtime schemas. Restore the TODO that tracks this so it isn't lost. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
… brand FilePath no longer canonicalizes on parse: FilePathSchema drops the .transform(canonicalizeAbsolutePath) and now validates absolute-path shape only, returning its input unchanged. This fixes the NFC-rewrite regressions the transform introduced — it broke NFD-named file imports on Linux by rewriting read-source paths, and it rewrote rootPath in a way that contradicted the watcher/tree byte-for-byte exemptions. Canonicalization is now an explicit, separate concept: a new canonicalizeFilePath() factory in @shared/utils/file/canonicalize produces a CanonicalFilePath sub-brand (a FilePath additionally proven canonical), used only at the external-path write/lookup boundary — FileEntryService's findByExternalPath / findCaseInsensitivePeers / setExternalPathAndName(Tx) params, ensureExternal, rename, and FileManager.findByExternalPath all flow CanonicalFilePath. file_entry.externalPath validates canonical-equivalence on read (non-canonical rows are warn-skipped, never silently repaired) and is branded CanonicalFilePath. CanonicalFilePath uses a string-literal brand key (not a unique symbol) so the preload WindowApiType aggregate does not trip TS2527/TS4023. The batch-ensure dedup loop now trusts the branded param and keys its memo on externalPath directly (no FilePathSchema re-parse). The eslint no-as-filepath watcher/tree exemption comment is re-justified on raw-OS-path grounds now that the canonicalize rationale is moot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
buildFilePartsForAttachments used Promise.all + FilePathSchema.parse, so one attachment with a non-absolute/legacy path (e.g. a `data:`- or `http:`-derived FileMetadata) threw and rejected the whole batch, dropping every attachment on the message. Switch to Promise.allSettled: keep the parts that build successfully, and log (loggerService.warn) and skip any attachment that fails instead of failing the batch. Separately, ChatComposer's edit-and-resend path called `await buildEditedMessageParts(draft)` outside the try/catch that wraps `forkAndResend`, so the same throw became an unhandled promise rejection and the resend silently no-op'd with no toast or log. Move the call inside the existing try/catch so a failure hits the same reporting path (logger.warn + toast) as other resend failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
FilePathSchema.safeParse failures in the renderer were silently swallowed with a fallback (undefined URL / legacy entry-id open). Log them so legacy/non-absolute/relative paths are observable instead of failing invisibly: - paintingFileUrl.ts (getPaintingFileUrl): warn before returning undefined. - fileTokenPresentation.tsx (getFilePreviewUrl): warn before returning undefined. - useMessageLeafCapabilities.ts: getFileView warns before yielding previewUrl: undefined; fileMetadataToHandle's legacy entry-id fallback (empty catch) now logs at debug so it's distinguishable from a truly corrupted path. - ArtifactPane.tsx: the HTML-preview baseUrl safeParse runs on every render, so warn once per distinct offending path via a module-level Set (warnedArtifactHtmlBasePaths) instead of warning every render. No success-path behavior changed; only added logging around existing fallbacks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
…Status edges Doc fixes: drop the dead pathResolver.ts realpath/case cross-refs in ipc.ts and canonicalize.ts (that logic now lives solely in internal/entry/create.ts), rewrite the externalPath IPC comment to reflect that FilePath is shape-only and canonicalization happens in ensureExternalEntry via canonicalizeFilePath, correct the toFileInfo.ts comment (FileInfoSchema.parse yields the exact type, no `as FileInfo`, FilePath is a Zod brand not a template literal), document the pre-existing UNC-path limitation on FilePathSchema, and reword the filepath-brand/no-as-filepath eslint rule text to describe shape validation instead of canonicalization. Code: setExternalPathAndNameTx now rejects a null byte in externalPath as defense-in-depth against a forged `as CanonicalFilePath` cast, with a test restoring the "rejects unsafe externalPath BEFORE the SQL UPDATE" coverage. getPathStatus now safeParses the path before the try/catch so a non-absolute/relative/file:// input is classified as missing instead of inaccessible, with a matching test case. Signed-off-by: eurfelux <eurfelux@gmail.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n dedup Rewrite §1.2 to reflect the decided design: `externalPath` is persisted byte-faithful (only reachability-safe lexical cleanup, never Unicode- normalized), so the stored string always reaches the real file — including on normalization-sensitive filesystems (Linux) where an NFC-rewritten path would not exist on disk. Replace the "canonical invariant" + rule-evolution migration discipline with a "Rejected: Unicode (NFC) normalization" section recording the reject rationale (reachability-blocking, unverified premise, rare+cosmetic on macOS, YAGNI). Update the DanglingCache/watcher section: byte-faithful key, raw-byte event matching (no NFC), with check() as the correctness baseline. Implementation (drop NFC from canonicalizeAbsolutePath, watcher raw match, relax ensureExternal existence) follows in subsequent commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
Canonicalization no longer Unicode-NFC-normalizes: `canonicalizeAbsolutePath` now returns the byte-faithful, lexically-resolved form (segment resolve, trailing-strip, Windows drive-upcase) exactly as the OS handed it. An NFC-rewritten path does not exist on disk on normalization-sensitive filesystems (Linux ext4/btrfs), so an NFC-canonical `externalPath` would `fs.stat`/copy `ENOENT` and leave the file unreachable; storing it byte-faithful keeps it reachable on every filesystem. Uniqueness stays byte-exact plus case-fold only. The DirectoryWatcher's DanglingCache reverse index is now keyed byte-faithful, so `handle()` matches chokidar events by raw byte equality — the NFC-normalize bridge that existed only to reach the old NFC-canonical keys is removed together with them. `check()` (which stats the byte-faithful stored path directly) is the correctness baseline, so a missed watcher match only delays a badge update until the next check — benign and self-healing. `CanonicalFilePath` / `canonicalizeFilePath` keep their names; "canonical" now means the lexical, byte-faithful resolved form, explicitly NOT Unicode-normalized. Tests whose premise baked in NFC folding (canonicalize, FileEntryService R4, watcher, create derivation, rename no-op) are inverted to assert byte-faithful behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
An external FileEntry may legitimately reference a path that is not currently on disk — dangling is a first-class state. Replace the hard `fs.stat` existence requirement in `ensureExternal` with a best-effort presence probe: ENOENT/ENOTDIR now creates a dangling entry (seeding a 'missing' DanglingCache observation) instead of throwing, while genuine FS faults (EACCES/EIO/…) still rethrow via raw `.code` discrimination. The `fs.realpath` case-collision probe needs the file on disk, so the whole findCaseInsensitivePeers/resolveCaseCollisionPeer block is gated on presence; a missing case-colliding path is left to the DB `lower(externalPath)` UNIQUE index to reject at INSERT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
…ilePath Two reference docs still described the superseded "FilePathSchema's transform canonicalizes at parse" model (from before FilePath became shape-only): - file-manager-architecture.md §13.3 appendix table said "canonicalizeAbsolutePath (via FilePathSchema's transform)" -> corrected to "(via the canonicalizeFilePath() factory)". - architecture.md ensureExternalEntry row said the entry point "parses it through FilePathSchema, whose transform normalizes it" -> corrected to validate shape via FilePathSchema, then canonicalize via canonicalizeFilePath() inside ensureExternalEntry before the upsert. Addresses self-review R16/R17. The byte-faithful §1.2 rewrite already fixed the in-section references; these two lived in an appendix table and a separate doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
…vatize canonicalizeAbsolutePath
CanonicalFilePath was a hand-written intersection (FilePath & { __canonical })
while FilePath is z.infer of its schema — an asymmetry rooted in an assumed
TS2527/TS4023 hazard for a second brand in preload's WindowApiType aggregate.
An experiment (FilePathSchema.brand<'CanonicalFilePath'>() flowing through
WindowApiType) typechecks clean on node+web+aicore, so the hazard was a local
`unique symbol`, not Zod's brand (which FilePath already uses). Unify the style.
- canonicalize.ts now owns the whole canonical layer (co-located with its
schema, the only cycle-free home since the schema needs both FilePathSchema
and the algorithm; utils -> types is the correct direction):
- CanonicalFilePathSchema = FilePathSchema.refine(isCanonicalFilePath).brand,
assert-only (rejects non-canonical, never repairs) — the "no silent repair"
contract the FileEntry externalPath read path depends on.
- type CanonicalFilePath = z.infer<typeof CanonicalFilePathSchema>.
- isCanonicalFilePath predicate (safe as a refine: false, never throws).
- canonicalizeFilePath() brands via the schema — no `as` cast.
- canonicalizeAbsolutePath is now module-private (dropped from the utils
barrel); its algorithm is covered transitively via canonicalizeFilePath.
- fileEntry.ts externalPath uses CanonicalFilePathSchema directly, dropping the
bespoke refine + `as CanonicalFilePath` transform (R4 semantics preserved).
- common.ts drops the hand-written type + its TS2527 workaround note; the type
and schema now live in / re-export from @shared/utils/file.
- FileEntryService imports CanonicalFilePath from @shared/utils/file.
- canonicalize.test.ts ported to the public surface (canonicalizeFilePath /
CanonicalFilePathSchema / isCanonicalFilePath), incl. assert-only accept/reject.
Both production `as CanonicalFilePath` casts are eliminated (factory + schema),
so the no-as-filepath exemptions for them are gone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: eurfelux <eurfelux@gmail.com>
`CanonicalFilePath` is now the load-bearing dedup-key brand, but only `as FilePath` was lint-guarded. A forged `as CanonicalFilePath` silently bypasses canonicalization and can write a ghost-duplicate key. Extend the rule to report `as CanonicalFilePath` too (distinct message pointing at canonicalizeFilePath()). After the schema refactor there are zero production `as CanonicalFilePath` casts, so no eslint-disable exemptions are needed; test fixtures remain exempt via the existing __tests__/*.test ignores. Addresses self-review R18. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
…failure Rebasing onto main pulled in #16732, which handles a failed edited-message resend by aborting + toasting: buildFilePartsForAttachments uses Promise.all, so any attachment failure rejects the batch, and it added a ChatComposer test asserting forkAndResend is NOT called on failure. This branch's earlier R3 fix took the opposite stance (Promise.allSettled: skip the bad attachment, send the rest), so the two collided during the rebase. Defer to the landed upstream behavior — failing loudly beats silently dropping a file the user attached, and R3's actual concern (an uncaught rejection from the path parse) is already covered by the send-flow try/catch #16732 wraps it in. - buildFilePartsForAttachments: Promise.allSettled -> Promise.all (propagate). - Keep FilePathSchema.parse(attachment.path) for validation instead of upstream's `as FilePath` cast (which the branch's no-as-filepath lint forbids). - buildFileParts.test.ts: the "skips the bad attachment" test becomes an "aborts the batch on a non-absolute path" (rejects) test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: eurfelux <eurfelux@gmail.com>
da3f10c to
6b11061
Compare
eurfelux/refactor/file-type
Signed-off-by: icarus <eurfelux@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
Before this PR: three overlapping filesystem-path types in
src/shared/:FilePath— a template literal (`/${string}` | `${string}:\\${string}`), a type-level hint only, with no runtime guarantee (no null-byte rejection, no absolute-shape enforcement).CanonicalExternalPath— a phantom brand reached only via the main-sidecanonicalizeExternalPath()wrapper.CanonicalFilePath— the intersectionFilePath & CanonicalExternalPath, used onExternalEntrySchema.externalPath.AbsolutePathSchemaonly refined input shape; downstream sites repeatedcanonicalizeExternalPath()by hand and a long tail of read sites neededas FilePathcasts.After this PR — a two-tier model:
FilePath— a runtime-validated Zod brand produced byFilePathSchemathat validates absolute-path shape only and does not mutate its input (FilePathSchema.parse(x) === x):CanonicalFilePath = FilePath & { readonly __canonical }— aFilePathin the byte-faithful, lexically-resolved form (segment-resolve + trailing-strip + Windows drive-upcase — NOT Unicode-normalized). Its sole producer iscanonicalizeFilePath()(@shared/utils/file/canonicalize.ts); it is used exactly where a stable dedup/persistence key is needed — thefile_entry.externalPathcolumn and theFileEntryService.findByExternalPath/findCaseInsensitivePeers/setExternalPathAndNamesignatures.ExternalEntrySchema.externalPathvalidates a stored value is already in that form and brands itCanonicalFilePath.externalPathis stored byte-faithful — never Unicode (NFC) normalized. NFC-normalizing the stored path would make it unreachable on normalization-sensitive filesystems (Linux ext4: an NFD-named file's NFC pathENOENTs → the file becomes unimportable). Unicode-based dedup is therefore deliberately rejected (unverified premise, rare + cosmetic on macOS where APFS folds at access, YAGNI); dedup stays byte-exact + case-fold (seedocs/references/file/file-manager-architecture.md§1.2 "Rejected: Unicode (NFC) normalization"). The DanglingCache watcher matches events by raw byte equality accordingly, withcheck()(which stats the byte-faithful path) as the correctness baseline.Also:
AbsolutePathSchema,CanonicalExternalPath, and thecanonicalizeExternalPathwrapper are deleted.FileInfodrops itsOmit+intersection hack (FileInfoSchema.parseyields the exact type).FilePathHandleSchema/ IPC path schemas are branded at the boundary.filepath-brand/no-as-filepath(CI-error / local-warn) forbidsas FilePathin production; genuine production casts becameFilePathSchema.parse(), and renderer render-time sites degrade gracefully viasafeParseand log on failure. Exemptions: tests + the raw-OS-path regimes (watcher/**,tree/**).Fixes #15404
Why we need it and why it was done in this way
The
FilePathtemplate literal was a lie of omission: it promised "looks absolute" but enforced nothing at runtime. Making it a real Zod brand fixes that. The canonicalization question was resolved across two rounds of review. First: an early iteration canonicalized insideFilePathSchema(everyFilePathNFC-normalized on parse), which mutated read-source paths and comparison keys and had to exemptwatcher/**/tree/**/ Notes — soFilePathbecame shape-only, with a separateCanonicalFilePathat theexternalPathboundary. Second: even that boundary NFC-normalization was found to break reachability on Linux (an NFD file's NFC path does not exist on disk), so it was removed too —externalPathis stored byte-faithful, and the Unicode-normalization idea that issue #15404 framed as an "NFC guarantee" is deliberately not implemented (it would trade certain Linux breakage for an unverified, rare, cosmetic macOS dedup). The net design:FilePath/CanonicalFilePathare validated + resolved but never Unicode-mutated; a stored path always reaches its file on every filesystem.The following tradeoffs were made:
FilePath+ explicitCanonicalFilePathfactory instead of canonicalize-on-parse — aFilePathnever silently changes bytes; only the external-entry boundary canonicalizes.CanonicalFilePath extends FilePath, so it flows anywhere aFilePathis accepted, whileFilePath → CanonicalFilePathis a compile error that forces the factory.externalPathread path — a stored value not already in the byte-faithful lexical form (e.g. containing/../or a trailing slash) is warn-skipped rather than silently rewritten, keeping the lookup key stable. Pinned by a test.as FilePath.The following alternatives were considered and rejected:
FilePathSchema— caused NFC-rewrite regressions on read-source paths (NFD-named file import on Linux) and contradicted the watcher/tree exemptions.externalPath, even scoped to the write boundary — an NFC-rewritten path does not exist on disk on normalization-sensitive filesystems (Linux), so the stored path can't reach its file. Removed entirely; dedup stays byte-exact + case-fold. Seedocs/references/file/file-manager-architecture.md§1.2 "Rejected: Unicode (NFC) normalization".canonicalizeExternalPathas a deprecated alias — rejected per the v2 throwaway principle.Links to places where the discussion took place: #15404 (design proposal). Supersedes the closed #15406.
Breaking changes
Internal v2 refactor. DB schema unchanged, IPC wire format unchanged (the phantom brand serializes as a plain string), renderer-visible behavior unchanged.
FilePathSchemaaccepts bothC:\…andC:/…Windows absolute forms and, being shape-only, preserves them as-is (no separator rewrite). Strict relaxation vs. the old backslash-onlyAbsolutePathSchema; no previously-valid input is rejected or rewritten.externalPathis stored byte-faithful (no NFC) —canonicalizeFilePathdoes reachability-safe lexical cleanup only (segment-resolve, trailing-strip, drive-upcase). This is a fix: the pre-existingcanonicalizeExternalPathNFC-normalized, which broke import/reachability of NFD-named files on normalization-sensitive filesystems (Linux).ensureExternalEntryon a currently-missing path now creates a dangling external entry instead of throwing (ENOENT/ENOTDIR→ dangling; genuine FS faults likeEACCESstill throw). External entries are allowed to reference an absent file by design (DanglingState), so the previous hard existence check was inconsistent. Not user-perceivable yet —ensureExternalEntryis Phase-2 and not wired into a renderer flow.\\server\share) remain rejected by the absolute-shape refine (pre-existing; now documented).Special notes for your reviewer
Supersedes the closed #15406, redone fresh on current
main. Adaptations vs. #15406: adapted to the synchronousFileEntryService; nosrc/shared/file/**flatten; knowledge directory-ingest and the Notes subsystem are intentionally out of scope.History (read the two phases):
886c546d…dd4f5b95): introduceFilePathSchema, unify the types, lint guard, docs, and follow-up polish. NOTE:19c3ef7einitially canonicalized inside the schema — that approach is reverted by the revision below.548402a4…5dc16cb2), addressing 15 inline comments:548402a4refactor(file-types):— the design pivot:FilePathshape-only +CanonicalFilePathsub-brand; canonicalization moved to theexternalPathboundary; batch dedup trusts the branded param (fixes the NFC-rewrite regressions R1/R2, the read-repair semantics R4, and the redundant re-parse R14/A3).6210a601fix(composer):— guard the edit-resendbuildEditedMessagePartscall (was an unhandled rejection) and isolate per-attachment parse failures withPromise.allSettled+ logging (R3).acdec7cbrefactor(file):— log the four renderersafeParse-failure sites (ArtifactPane deduped one-time) (R5–R8).5dc16cb2refactor(file):— stale-doc fixes (R9–R11), a null-byte defense-in-depth guard onsetExternalPathAndNameTx(R12),getPathStatusnon-absolute →missing(R13), UNC limitation doc (R15).b6d4a096…607b4528), from a deeper design discussion (reachability on Linux):b6d4a096docs(file):— rewritefile-manager-architecture.md§1.2: storeexternalPathbyte-faithful; record the reject rationale for NFC-based dedup.c12a6072fix(file):— drop NFC normalization fromcanonicalizeAbsolutePathand the watcher's event matching →externalPathis byte-faithful and reachable on normalization-sensitive filesystems; the watcher matches events by raw byte equality.607b4528refactor(file):—ensureExternalEntryon a currently-missing path creates a dangling entry (ENOENT/ENOTDIR → dangling) instead of throwing; genuine FS faults (EACCES/…) still throw.Checklist
mainfor active development,v1for v1 maintenance fixes/gh-pr-review,gh pr diff, or GitHub UI) before requesting review from othersRelease note