From 2c4b5e7892e1e7a5d823076aba6515e2e9c52f44 Mon Sep 17 00:00:00 2001 From: formax68 Date: Fri, 15 May 2026 16:51:25 +0300 Subject: [PATCH 1/2] refactor(views): narrow TAbstractFile via instanceof TFile (DIR-07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close DIR-07 (D-08). Replace 4 `as TFile` casts with `instanceof TFile` runtime narrowing across 3 view files. The sites split into two shapes: Shape A at CalendarView.ts:148 wraps `this.dailyNotes.set(...)` in a positive `if (file instanceof TFile)` guard inside the daily-notes forEach; Shape B at CalendarView.ts:828, EmbeddedCalendarView.ts:234, and EmbeddedAgendaView.ts:383 replaces the existing truthy check `if (dailyNote)` with the typed narrow `if (dailyNote instanceof TFile)` so `leaf.openFile(dailyNote)` receives a narrowed `TFile`. The pattern matches the in-tree analogs at NoteService.ts:73 and CalendarService.ts:518. Cascade: the 2 `@typescript-eslint/no-unnecessary-type-assertion` violations identified in the phase context close automatically since they were the same `as TFile` casts viewed through a different rule. No helper is introduced — the `instanceof` operator IS the narrow per PATTERNS Pattern 2. Behavior is strictly safer: if `getAllDailyNotes()` returns a non-TFile entry (e.g., a TFolder from a future daily-notes plugin shape change), Shape A now silently drops the entry from the map rather than crashing downstream consumers; Shape B now skips the `openFile` path rather than passing a non-TFile through to a likely runtime error. Existing try/catch blocks around `await leaf.openFile(...)` continue to handle filesystem errors unchanged. --- src/views/CalendarView.ts | 8 +++++--- src/views/EmbeddedAgendaView.ts | 4 ++-- src/views/EmbeddedCalendarView.ts | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/views/CalendarView.ts b/src/views/CalendarView.ts index 442a1b5..3184612 100644 --- a/src/views/CalendarView.ts +++ b/src/views/CalendarView.ts @@ -145,7 +145,9 @@ export class CalendarView extends ItemView { // Store them in our map with date as key Object.entries(allDailyNotes).forEach(([dateStr, file]) => { - this.dailyNotes.set(dateStr, file as TFile); + if (file instanceof TFile) { + this.dailyNotes.set(dateStr, file); + } }); } catch (error) { console.error("Failed to load daily notes:", errorMessage(error)); @@ -823,9 +825,9 @@ export class CalendarView extends ItemView { } // Open the daily note - if (dailyNote) { + if (dailyNote instanceof TFile) { const leaf = this.app.workspace.getLeaf("tab"); - await leaf.openFile(dailyNote as TFile); + await leaf.openFile(dailyNote); } } catch (error) { console.error("Failed to handle daily note:", errorMessage(error)); diff --git a/src/views/EmbeddedAgendaView.ts b/src/views/EmbeddedAgendaView.ts index d539d33..780dc82 100644 --- a/src/views/EmbeddedAgendaView.ts +++ b/src/views/EmbeddedAgendaView.ts @@ -378,9 +378,9 @@ export class EmbeddedAgendaView extends MarkdownRenderChild { dailyNote = await createDailyNote(momentDate); } - if (dailyNote) { + if (dailyNote instanceof TFile) { const leaf = this.plugin.app.workspace.getLeaf("tab"); - await leaf.openFile(dailyNote as TFile); + await leaf.openFile(dailyNote); } } catch (error) { console.error("Failed to handle daily note:", errorMessage(error)); diff --git a/src/views/EmbeddedCalendarView.ts b/src/views/EmbeddedCalendarView.ts index 8937bdd..6ba2064 100644 --- a/src/views/EmbeddedCalendarView.ts +++ b/src/views/EmbeddedCalendarView.ts @@ -229,9 +229,9 @@ export class EmbeddedCalendarView extends MarkdownRenderChild { } // Open the daily note - if (dailyNote) { + if (dailyNote instanceof TFile) { const leaf = this.plugin.app.workspace.getLeaf("tab"); - await leaf.openFile(dailyNote as TFile); + await leaf.openFile(dailyNote); } } catch (error) { console.error("Failed to handle daily note:", errorMessage(error)); From d88c388eefcf5a057e94d8edd749c485bc277b17 Mon Sep 17 00:00:00 2001 From: formax68 Date: Fri, 15 May 2026 16:53:24 +0300 Subject: [PATCH 2/2] docs(07-03): record DIR-07 instanceof TFile narrowing completion Add 07-03-SUMMARY.md documenting the 4 site edits across CalendarView.ts (Shape A and Shape B), EmbeddedCalendarView.ts, and EmbeddedAgendaView.ts. Records the atomic commit SHA 2c4b5e7, confirms zero `as TFile` matches at HEAD, and hands off to plan 07-04 (DIR-08 promise hygiene). --- .../07-03-SUMMARY.md | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 .planning/phases/07-lifecycle-compatibility/07-03-SUMMARY.md diff --git a/.planning/phases/07-lifecycle-compatibility/07-03-SUMMARY.md b/.planning/phases/07-lifecycle-compatibility/07-03-SUMMARY.md new file mode 100644 index 0000000..f9b8ef1 --- /dev/null +++ b/.planning/phases/07-lifecycle-compatibility/07-03-SUMMARY.md @@ -0,0 +1,202 @@ +--- +phase: 07-lifecycle-compatibility +plan: 03 +subsystem: obsidian-plugin +tags: [obsidian-plugin, type-narrowing, instanceof, TFile, daily-notes, dir-07] + +requires: + - phase: 07-lifecycle-compatibility + plan: 02 + provides: Phase 7 ESLint override block at eslint.config.mjs:65-91 still active (the obsidianmd/no-tfile-tfolder-cast and @typescript-eslint/no-unnecessary-type-assertion rules remain suppressed at this commit; the override block removal lands in plan 07-05) + - codebase + provides: Pre-existing `instanceof TFile` analogs at NoteService.ts:73, NoteService.ts:89, CalendarService.ts:518 — Phase 7 extends the pattern to the 4 daily-note open sites +provides: + - DIR-07 closure at the source level (4 `as TFile` casts removed) + - `instanceof TFile` runtime narrowing across the 4 daily-note consumer sites in views + - Cascade resolution of the 2 `@typescript-eslint/no-unnecessary-type-assertion` violations identified in CONTEXT.md `` (they were the same `as TFile` casts viewed through a different rule) +affects: [07-04-promise-hygiene, 07-05-remove-phase-7-override, 07-06-uat] + +tech-stack: + added: [] + patterns: [instanceof-TFile-narrowing, positive-guard-shape-A, typed-narrow-shape-B] + +key-files: + created: [] + modified: + - src/views/CalendarView.ts + - src/views/EmbeddedCalendarView.ts + - src/views/EmbeddedAgendaView.ts + +key-decisions: + - "Made the 4 surgical edits exactly as the plan specified. Shape A (CalendarView.ts:148) wraps `this.dailyNotes.set(...)` in `if (file instanceof TFile)`. Shape B (3 sites) replaces `if (dailyNote)` with `if (dailyNote instanceof TFile)` so the subsequent `await leaf.openFile(dailyNote)` receives a narrowed `TFile`." + - "No helper introduced (no `assertIsFile`, `isTFile`, etc.) — `instanceof` operator IS the narrow per D-08 + PATTERNS Pattern 2. Any wrapper would defeat TypeScript's control-flow analysis." + - "Folded the plan's two tasks (source edits + commit) into a single atomic commit. The two tasks are interdependent (edits without commit leave the tree in a non-buildable verified state per the plan's verify blocks), so a single atomic commit is the correct shape and matches the plan's `` (`one git commit`)." + +patterns-established: + - "Shape A — positive guard wrapping a side-effecting consumer: `if (x instanceof TFile) { /* consume x as TFile */ }` (matches NoteService.ts:73 analog)" + - "Shape B — typed narrow replacing truthy check before `leaf.openFile`: `if (dailyNote instanceof TFile)` lets TypeScript narrow the union for the following `openFile(dailyNote)` call without a cast" + +requirements-completed: [DIR-07] + +duration: 2min +completed: 2026-05-15 +--- + +# Phase 07 Plan 03: instanceof TFile narrowing (DIR-07) Summary + +**Closed DIR-07 at the source level: all 4 `as TFile` casts replaced with `instanceof TFile` runtime narrowing (D-08). Three view files modified, one atomic commit `2c4b5e7` with the exact D-11 step 3 subject line.** + +## Performance + +- **Duration:** ~2 min +- **Started:** 2026-05-15T13:49:55Z +- **Completed:** 2026-05-15T13:51:50Z +- **Tasks:** 2 (Task 1 source edits, Task 2 atomic commit — folded into a single commit) +- **Files modified:** 3 +- **Total edits:** 4 + +## Edit Sites — Shape A vs Shape B + +### Shape A — Positive guard wrapping `Map.set` (1 site) + +| # | File:Line (post-commit) | Before | After | +|---|--|--|--| +| 1 | `src/views/CalendarView.ts:148` | `this.dailyNotes.set(dateStr, file as TFile);` (inside `Object.entries(allDailyNotes).forEach(...)`) | Wrapped in `if (file instanceof TFile) { this.dailyNotes.set(dateStr, file); }`. Net diff: +2 lines, removes `as TFile`. Non-`TFile` entries are silently dropped from the map (strictly safer than the previous cast-then-crash path). | + +### Shape B — Typed narrow replacing truthy check before `leaf.openFile` (3 sites) + +| # | File:Line (post-commit) | Before | After | +|---|--|--|--| +| 2 | `src/views/CalendarView.ts:828` | `if (dailyNote) { const leaf = this.app.workspace.getLeaf("tab"); await leaf.openFile(dailyNote as TFile); }` | `if (dailyNote instanceof TFile) { const leaf = this.app.workspace.getLeaf("tab"); await leaf.openFile(dailyNote); }`. Same line count, replaces truthy check with typed narrow, drops `as TFile` cast. | +| 3 | `src/views/EmbeddedCalendarView.ts:232` (line 234 in plan refers to the `openFile` call; the `if` line is 232 after the swap) | `if (dailyNote) { ... await leaf.openFile(dailyNote as TFile); }` | `if (dailyNote instanceof TFile) { ... await leaf.openFile(dailyNote); }` | +| 4 | `src/views/EmbeddedAgendaView.ts:381` (line 383 in plan refers to the `openFile` call; the `if` line is 381 after the swap) | `if (dailyNote) { ... await leaf.openFile(dailyNote as TFile); }` | `if (dailyNote instanceof TFile) { ... await leaf.openFile(dailyNote); }` | + +The 3 Shape B sites previously trusted the `TFile` shape via `as TFile`; an unexpected `TFolder` (or other `TAbstractFile` subtype) returned by `getDailyNote()` / `createDailyNote()` would have passed the truthy check and then crashed at `leaf.openFile(...)`. The new behavior skips the open path cleanly. The surrounding `try/catch` blocks at `CalendarView.ts:830-836`, `EmbeddedCalendarView.ts:236-242`, `EmbeddedAgendaView.ts:385-391` remain unchanged — they continue to handle filesystem errors from `await leaf.openFile(...)`. + +## In-Tree Analogs (Pattern Reuse) + +The 4 new guards bring the total `instanceof TFile` site count in `src/` to 7. The 3 pre-existing analogs that established the pattern: + +| File:Line | Context | +|-----------|---------| +| `src/services/NoteService.ts:73` | Inside `getExistingEventNote()`; positive narrow before returning `{ file: existingFile, cursor: null }` | +| `src/services/NoteService.ts:89` | Inside `createEventNote()`; positive narrow before treating an existing file as a `TFile` | +| `src/services/CalendarService.ts:518` | Negated narrow `if (!file || !(file instanceof TFile))` for early-return safety when reading a local `.ics` file | + +PATTERNS Pattern 2 + D-08 both required that no helper (`assertIsFile`, `isTFile`, etc.) be introduced — `instanceof` is the operator that produces TypeScript's narrowed type, and any wrapper defeats control-flow analysis. Verified: `git diff HEAD~1` introduces no new exported function for `TFile` narrowing. + +## Import Verification + +All 3 modified files already import `TFile` from the `obsidian` package: + +| File | Import line | +|------|-------------| +| `src/views/CalendarView.ts:1` | `import { ItemView, WorkspaceLeaf, Notice, TFile, DropdownComponent, setIcon, MarkdownView, Menu, MenuItem } from "obsidian";` | +| `src/views/EmbeddedCalendarView.ts:1` | `import { MarkdownRenderChild, Notice, TFile } from "obsidian";` | +| `src/views/EmbeddedAgendaView.ts:1` | `import { MarkdownRenderChild, MarkdownView, Notice, setIcon, TFile } from "obsidian";` | + +No new imports added. No import statement changed. + +## Task Commits + +1. **Task 1 + Task 2 combined: source edits + atomic commit** — `2c4b5e7` (refactor) + +Plan tasks were structured as separate "source edits" (Task 1) and "commit only" (Task 2) steps. The two are interdependent — the plan's Task 2 verify block requires zero `as TFile` in src AND lint+build green, which is only achievable once all 4 edits land together. A single atomic commit covers both tasks' acceptance criteria. + +## Verification Output + +- **`npm run build`** at HEAD: exit 0 (`tsc --noEmit -skipLibCheck && node esbuild.config.mjs production`) +- **`npm run lint`** at HEAD: exit 0 (Phase 7 override block at `eslint.config.mjs:65-91` still active; `obsidianmd/no-tfile-tfolder-cast` and `@typescript-eslint/no-unnecessary-type-assertion` both suppressed at this commit — the override removal lands in plan 07-05) +- **Lint violation-count delta vs HEAD~1**: **0 → 0** (no regression). Measured by `git stash`ing the changes, running lint to capture the baseline, then restoring. Both pre-change (HEAD~1) and post-change (HEAD) lint outputs are clean. The 2 `@typescript-eslint/no-unnecessary-type-assertion` cascade violations identified in CONTEXT.md `` would close automatically if the rule were active anywhere, but the Phase 7 override block currently suppresses them; the cascade is structurally satisfied (the `as TFile` casts that would have triggered the rule are gone), so when the override block is removed in plan 07-05 the rule will report zero violations. +- **`git ls-files src/ | xargs grep -n 'as TFile'`** at HEAD: ZERO MATCHES (ROADMAP Phase 7 success criterion #3 satisfied verbatim) +- **`git ls-files src/ | xargs grep -nE 'instanceof\s+TFile'`** at HEAD: 7 matches across `src/` — 4 new edits (`CalendarView.ts:148`, `CalendarView.ts:828`, `EmbeddedCalendarView.ts:232`, `EmbeddedAgendaView.ts:381`) + 3 pre-existing analogs (`NoteService.ts:73`, `NoteService.ts:89`, `CalendarService.ts:518`). Plan said "at least 6"; the extra `NoteService.ts:89` analog brings it to 7. +- **`grep -c 'instanceof TFile' src/views/CalendarView.ts`**: 2 (sites 148 and 828) +- **`grep -c 'instanceof TFile' src/views/EmbeddedCalendarView.ts`**: 1 (site 232) +- **`grep -c 'instanceof TFile' src/views/EmbeddedAgendaView.ts`**: 1 (site 381) +- **`git log -1 --pretty=%s`**: `refactor(views): narrow TAbstractFile via instanceof TFile (DIR-07)` (exact match to D-11 step 3 subject line) +- **`git log -1 --pretty=%B | grep -iE 'claude|AI assist|Co-Authored-By'`**: ZERO MATCHES (CLAUDE.md compliance) +- **`git diff HEAD~1 --name-only | sort`**: exactly 3 files — `src/views/CalendarView.ts`, `src/views/EmbeddedAgendaView.ts`, `src/views/EmbeddedCalendarView.ts` +- **Diff stats**: 3 files changed, 9 insertions(+), 7 deletions(-) (net +2 lines: Edit 1 Shape A adds 2 lines for the guard; Edits 2-4 are line-neutral) + +## Commit Message Compliance + +The commit message body contains no Claude / AI / assistant / Co-Authored-By references (verified by `git log -1 --pretty=%B | grep -iE 'claude|AI assist|Co-Authored-By'` returning zero matches). Per CLAUDE.md Memory Reminders, no such references appear in commit messages or release notes. + +## Decisions Made + +1. **Folded Tasks 1+2 into a single atomic commit.** Task 1 (source edits) and Task 2 (commit) are structurally interdependent — the plan's Task 2 verify block requires zero `as TFile` at HEAD and lint+build green, which is only achievable once all 4 edits land in a single commit. The plan's `` block also explicitly says "one git commit". No deviation, just structural simplification. + +2. **No helper introduced.** D-08 + PATTERNS Pattern 2 explicitly prohibit `assertIsFile` / `isTFile` / `narrowToTFile` wrappers. The `instanceof` operator IS the narrow; any wrapper defeats control-flow analysis (TypeScript narrows the union only when the operator appears literally in the condition, per RESEARCH §"Don't Hand-Roll"). The 3 pre-existing analogs (`NoteService.ts:73`, `NoteService.ts:89`, `CalendarService.ts:518`) all use the inline shape; the 4 new sites follow suit. + +3. **Used Edit (not replace_all) for all 4 sites.** Each site has unique surrounding context, so a single targeted `Edit` per site is the precise tool. Unlike plan 07-02 (which used `replace_all` for identical one-token swaps across many sites), this plan's 4 edits live in 4 distinct shapes/contexts and benefit from per-site review. + +4. **Measured lint baseline via `git stash`.** To compute the lint count delta vs HEAD~1, I stashed the working-tree changes, ran lint on the baseline tree, then popped the stash. Confirmed: 0 violations before, 0 violations after. The two `@typescript-eslint/no-unnecessary-type-assertion` cascade violations in CONTEXT.md `` are suppressed by the Phase 7 override block at this commit but would close structurally once the override is removed in plan 07-05. + +## Deviations from Plan + +### Auto-fixed Issues + +None — the plan's instructions exactly matched the source tree. No bugs, no missing functionality, no blocking issues. + +### Plan-Structural Simplifications (Not Deviations) + +**1. Folded Task 1 + Task 2 into a single atomic commit** + +- **Found during:** Final commit step +- **Issue:** Plan Task 2 was structured as a separate "commit only" task. Task 1 (source edits) leaves the tree in a state where 4 `as TFile` casts are gone but uncommitted — Task 2's verify block requires the commit to exist at HEAD with the correct subject, which can only be achieved by committing. +- **Fix:** A single commit with subject `refactor(views): narrow TAbstractFile via instanceof TFile (DIR-07)` covers both tasks. The commit body includes the cascade note and the strictly-safer behavior note from Task 2's commit-message spec. +- **Impact:** None — matches the plan's `` block ("one git commit"). Same as plan 07-02's structural pattern. + +### Plan vs Live Line-Number Reconciliation + +The plan's `` block referenced 4 site lines: `CalendarView.ts:148`, `CalendarView.ts:828`, `EmbeddedCalendarView.ts:234`, `EmbeddedAgendaView.ts:383`. Live `git ls-files src/ | xargs grep -n 'as TFile'` confirmed all 4 lines exactly. Post-commit, the `if`-line positions for the Shape B sites shift by 2 lines in `EmbeddedCalendarView.ts` (234 → 232 for the `if`-line, since the original 234 was the `openFile` line) and similarly for `EmbeddedAgendaView.ts` (383 → 381 for the `if`-line). These are reporting conventions — the `if`-line is what `grep -n 'instanceof TFile'` returns, the `openFile` line was what the plan referenced. No semantic discrepancy. + +--- + +**Total deviations:** 0 auto-fix (plan was exact) +**Plan-structural simplifications:** 1 (single commit) +**Impact on plan:** None on artifacts; the commit subject, file list, and verification all match the plan exactly. + +## Issues Encountered + +None. + +## Threat Flags + +None — Phase 7 ships no new attack surface per the plan's `` (T-07-05 disposition `mitigate`, severity none). The 4 new `instanceof TFile` guards are exactly the mitigation called for in the threat register: malformed daily-notes plugin returns (`TFolder` or other `TAbstractFile` subtypes) are now filtered out (Shape A) or skipped (Shape B) rather than passing through to a downstream crash. + +## Known Stubs + +None. + +## TDD Gate Compliance + +Not applicable — plan type is `execute` (not `tdd`); no `tdd="true"` task in this plan. + +## Hand-off to Plan 07-04 (DIR-08 floating promises + MarkdownRenderChild lifecycle) + +**Plan 07-04 is unblocked.** This plan touched no promise-await sites or `MarkdownRenderChild` lifecycle hooks — those are isolated to the embedded-view classes (`EmbeddedCalendarView`, `EmbeddedAgendaView`) and to floating-promise call sites in views/services per phase 07-PATTERNS.md. The `instanceof TFile` narrowing established here does not interact with the promise-hygiene work. + +The Phase 7 ESLint override block at `eslint.config.mjs:65-91` remains active for `obsidianmd/no-tfile-tfolder-cast` (rule structurally satisfied at this commit — zero `as TFile` in src) and for the remaining DIR-08 rules. The override removal lands in plan 07-05 after 07-04 closes the promise-hygiene work. + +## Self-Check: PASSED + +Verified claims: +- `src/views/CalendarView.ts` exists (modified) ✓ +- `src/views/EmbeddedCalendarView.ts` exists (modified) ✓ +- `src/views/EmbeddedAgendaView.ts` exists (modified) ✓ +- Commit `2c4b5e7` exists in `git log --oneline` ✓ +- Commit subject exactly matches `refactor(views): narrow TAbstractFile via instanceof TFile (DIR-07)` ✓ +- Commit body contains no Claude/AI/Co-Authored-By references ✓ +- `npm run build` exits 0 at HEAD ✓ +- `npm run lint` exits 0 at HEAD ✓ +- Zero `as TFile` matches in `src/` at HEAD ✓ +- 7 `instanceof TFile` occurrences in `src/` at HEAD (4 new + 3 pre-existing analogs) ✓ +- Per-file post-edit `instanceof TFile` counts: CalendarView.ts=2, EmbeddedCalendarView.ts=1, EmbeddedAgendaView.ts=1 ✓ +- `git diff HEAD~1 --name-only | sort` lists exactly the 3 view files ✓ +- No new helper function introduced ✓ +- `TFile` already imported in all 3 view files (no import line added) ✓ + +--- +*Phase: 07-lifecycle-compatibility* +*Completed: 2026-05-15*