Skip to content

refactor(file-types): unify FilePath as Zod-branded canonical path#16740

Open
EurFelux wants to merge 21 commits into
mainfrom
eurfelux/refactor/file-type
Open

refactor(file-types): unify FilePath as Zod-branded canonical path#16740
EurFelux wants to merge 21 commits into
mainfrom
eurfelux/refactor/file-type

Conversation

@EurFelux

@EurFelux EurFelux commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

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-side canonicalizeExternalPath() wrapper.
  • CanonicalFilePath — the intersection FilePath & CanonicalExternalPath, used on ExternalEntrySchema.externalPath.

AbsolutePathSchema only refined input shape; downstream sites repeated canonicalizeExternalPath() by hand and a long tail of read sites needed as FilePath casts.

After this PR — a two-tier model:

  • FilePath — a runtime-validated Zod brand produced by FilePathSchema that validates absolute-path shape only and does not mutate its input (FilePathSchema.parse(x) === x):
    export const FilePathSchema = z
      .string().min(1)
      .refine((s) => !s.includes('\0'), 'must not contain null bytes')
      .refine((s) => s.startsWith('/') || /^[A-Za-z]:[/\\]/.test(s), 'must be an absolute filesystem path')
      .brand<'FilePath'>()
    export type FilePath = z.infer<typeof FilePathSchema>
  • CanonicalFilePath = FilePath & { readonly __canonical } — a FilePath in the byte-faithful, lexically-resolved form (segment-resolve + trailing-strip + Windows drive-upcase — NOT Unicode-normalized). Its sole producer is canonicalizeFilePath() (@shared/utils/file/canonicalize.ts); it is used exactly where a stable dedup/persistence key is needed — the file_entry.externalPath column and the FileEntryService.findByExternalPath / findCaseInsensitivePeers / setExternalPathAndName signatures. ExternalEntrySchema.externalPath validates a stored value is already in that form and brands it CanonicalFilePath.

externalPath is 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 path ENOENTs → 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 (see docs/references/file/file-manager-architecture.md §1.2 "Rejected: Unicode (NFC) normalization"). The DanglingCache watcher matches events by raw byte equality accordingly, with check() (which stats the byte-faithful path) as the correctness baseline.

Also:

  • AbsolutePathSchema, CanonicalExternalPath, and the canonicalizeExternalPath wrapper are deleted. FileInfo drops its Omit+intersection hack (FileInfoSchema.parse yields the exact type). FilePathHandleSchema / IPC path schemas are branded at the boundary.
  • A new ESLint rule filepath-brand/no-as-filepath (CI-error / local-warn) forbids as FilePath in production; genuine production casts became FilePathSchema.parse(), and renderer render-time sites degrade gracefully via safeParse and log on failure. Exemptions: tests + the raw-OS-path regimes (watcher/**, tree/**).
  • Path-type reference docs synced.

Fixes #15404

Why we need it and why it was done in this way

The FilePath template 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 inside FilePathSchema (every FilePath NFC-normalized on parse), which mutated read-source paths and comparison keys and had to exempt watcher/** / tree/** / Notes — so FilePath became shape-only, with a separate CanonicalFilePath at the externalPath boundary. 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 — externalPath is 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/CanonicalFilePath are validated + resolved but never Unicode-mutated; a stored path always reaches its file on every filesystem.

The following tradeoffs were made:

  • Shape-only FilePath + explicit CanonicalFilePath factory instead of canonicalize-on-parse — a FilePath never silently changes bytes; only the external-entry boundary canonicalizes. CanonicalFilePath extends FilePath, so it flows anywhere a FilePath is accepted, while FilePath → CanonicalFilePath is a compile error that forces the factory.
  • Reject-don't-repair on the externalPath read 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.
  • Lint guard rather than convention for as FilePath.

The following alternatives were considered and rejected:

  • Canonicalize-by-default inside FilePathSchema — caused NFC-rewrite regressions on read-source paths (NFD-named file import on Linux) and contradicted the watcher/tree exemptions.
  • Any Unicode (NFC) normalization of 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. See docs/references/file/file-manager-architecture.md §1.2 "Rejected: Unicode (NFC) normalization".
  • Keeping canonicalizeExternalPath as 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.

  • FilePathSchema accepts both C:\… and C:/… Windows absolute forms and, being shape-only, preserves them as-is (no separator rewrite). Strict relaxation vs. the old backslash-only AbsolutePathSchema; no previously-valid input is rejected or rewritten.
  • externalPath is stored byte-faithful (no NFC)canonicalizeFilePath does reachability-safe lexical cleanup only (segment-resolve, trailing-strip, drive-upcase). This is a fix: the pre-existing canonicalizeExternalPath NFC-normalized, which broke import/reachability of NFD-named files on normalization-sensitive filesystems (Linux).
  • ensureExternalEntry on a currently-missing path now creates a dangling external entry instead of throwing (ENOENT/ENOTDIR → dangling; genuine FS faults like EACCES still 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 — ensureExternalEntry is Phase-2 and not wired into a renderer flow.
  • UNC paths (\\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 synchronous FileEntryService; no src/shared/file/** flatten; knowledge directory-ingest and the Notes subsystem are intentionally out of scope.

History (read the two phases):

  • Initial build-out (886c546ddd4f5b95): introduce FilePathSchema, unify the types, lint guard, docs, and follow-up polish. NOTE: 19c3ef7e initially canonicalized inside the schema — that approach is reverted by the revision below.
  • Revision after author self-review (548402a45dc16cb2), addressing 15 inline comments:
    • 548402a4 refactor(file-types):the design pivot: FilePath shape-only + CanonicalFilePath sub-brand; canonicalization moved to the externalPath boundary; 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).
    • 6210a601 fix(composer): — guard the edit-resend buildEditedMessageParts call (was an unhandled rejection) and isolate per-attachment parse failures with Promise.allSettled + logging (R3).
    • acdec7cb refactor(file): — log the four renderer safeParse-failure sites (ArtifactPane deduped one-time) (R5–R8).
    • 5dc16cb2 refactor(file): — stale-doc fixes (R9–R11), a null-byte defense-in-depth guard on setExternalPathAndNameTx (R12), getPathStatus non-absolute → missing (R13), UNC limitation doc (R15).
  • Byte-faithful revision (b6d4a096607b4528), from a deeper design discussion (reachability on Linux):
    • b6d4a096 docs(file): — rewrite file-manager-architecture.md §1.2: store externalPath byte-faithful; record the reject rationale for NFC-based dedup.
    • c12a6072 fix(file): — drop NFC normalization from canonicalizeAbsolutePath and the watcher's event matching → externalPath is byte-faithful and reachable on normalization-sensitive filesystems; the watcher matches events by raw byte equality.
    • 607b4528 refactor(file):ensureExternalEntry on a currently-missing path creates a dangling entry (ENOENT/ENOTDIR → dangling) instead of throwing; genuine FS faults (EACCES/…) still throw.

Checklist

  • Branch: This PR targets the correct branch — main for active development, v1 for v1 maintenance fixes
  • PR: The PR description is expressive enough and will help future contributors
  • Code: Write code that humans can understand and Keep it simple
  • Refactor: You have left the code cleaner than you found it (Boy Scout Rule)
  • Upgrade: Impact of this change on upgrade flows was considered and addressed if required
  • Documentation: A user-guide update was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior.
  • Self-review: I have reviewed my own code (e.g., via /gh-pr-review, gh pr diff, or GitHub UI) before requesting review from others

Release note

NONE

Comment thread src/main/services/file/FileManager.ts Outdated
Comment thread src/shared/ipc/schemas/file.ts
Comment thread src/main/services/file/tree/DirectoryTreeManager.ts
Comment thread src/renderer/utils/file/buildFileParts.ts
Comment thread src/shared/data/types/file/fileEntry.ts Outdated
Comment thread src/main/services/file/toFileInfo.ts
Comment thread src/main/data/services/FileEntryService.ts Outdated
Comment thread src/main/utils/file/pathStatus.ts Outdated
Comment thread src/shared/utils/file/url.ts Outdated
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))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[R14] 建议 · 微优化:toSafeFileUrl 对已 branded 的 FilePath 每次调用都再跑一遍 FilePathSchema.parse(),包括每个 <img>/<video> 渲染。

同类冗余:pathResolver.ts:49(internal 分支)、FileManager.ts:926。均幂等无害,只是可避免的工作。非危险分支(isDangerExt)可直接 toFileUrl(absolutePath),仅在 dirnameSimple 改写后的路径上才需重 parse。(dirnameSimpleC:\ 根的修正本身正确且必要。)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[R14] 部分处理:FileManager batch 那处冗余 re-parse 已随 A3 移除(548402a4)。url.ts:236 这处保留 —— FilePath 现在是 shape-only,FilePathSchema.parse 只做一次廉价形状校验(不再跑 canonicalize),"可避免的工作"已从 canonicalize 降到近乎零,留着是为在 dirnameSimple 改写后仍保证 brand 一致。若你仍想省掉,可在非危险分支直接 toFileUrl(absolutePath) —— 说一声我就改。

Comment thread src/shared/types/file/common.ts
EurFelux and others added 17 commits July 5, 2026 20:58
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>
@EurFelux EurFelux force-pushed the eurfelux/refactor/file-type branch from da3f10c to 6b11061 Compare July 5, 2026 22:47
@EurFelux EurFelux marked this pull request as ready for review July 6, 2026 09:02
@EurFelux EurFelux requested a review from a team July 6, 2026 09:02
@EurFelux EurFelux requested review from 0xfullex and DeJeune as code owners July 6, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Brand FilePath with NFC guarantee, unify with CanonicalExternalPath / CanonicalFilePath

1 participant