Skip to content

Commit 2e0cd92

Browse files
diegorvclaude
andcommitted
fix(properties): de-duplicate canonical keys on parse to stop panel crash
Context: parseFrontmatterProperties feeds propertiesStore (via parseAndSetProperties), which PropertiesView renders with a keyed {#each (property.key)}. computeAndCache canonicalizes every frontmatter key (e.g. the alias `color` -> `_color`). Problem: When a note's frontmatter contains both an alias and its canonical twin (`color` + `_color`, `icon` + `_icon`, etc. -- common from external editors, sync tools, or pre-migration notes), both keys canonicalize to the same name, so the parser emitted two `_color` Property entries. The keyed {#each} then saw two items with key `_color`, and Svelte's reconciler threw each_key_duplicate in both dev and production. With no svelte:boundary in src/lib, the throw broke the entire Properties panel render. serializeProperties already de-duplicated via dedupeCanonicalKeys, but the parse path did not, so the store held the collision. Solution: Apply the existing dedupeCanonicalKeys to the parsed properties inside computeAndCache, before caching and returning. De-dup runs once per unique frontmatter (on cache miss), so the cached value and every consumer get a single entry per canonical key. Resolution matches serializeProperties: first populated value wins, collisions are logged (never silent). Idempotent for collision-free frontmatter, so all existing parse results are unchanged. Behavior: - `color: red` + `_color: blue` -> single `{ key: '_color', value: 'red' }` entry (first appearance wins); panel renders instead of crashing. - Collision-free frontmatter: unchanged. Files: - src/lib/features/properties/properties.logic.ts:146-151 — dedupeCanonicalKeys applied in computeAndCache before caching/returning. - src/tests/lib/features/properties/properties.logic.test.ts — regression test asserting alias + canonical twin collapse to one entry. - tasks/todo/bug-hunt-fixes.md — H5 marked done. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 07e772d commit 2e0cd92

3 files changed

Lines changed: 19 additions & 3 deletions

File tree

src/lib/features/properties/properties.logic.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,13 @@ function computeAndCache(rawFrontmatter: string): Property[] {
145145
}
146146
properties.push(convertToProperty(canonicalizeKey(rawKey), value));
147147
}
148-
parseCache.set(rawFrontmatter, Object.freeze(properties.map(cloneProperty)));
148+
// Collapse alias/canonical twins (e.g. `color` + `_color`) into one entry per
149+
// canonical key. Two entries with the same canonical key would otherwise reach
150+
// the store and crash PropertiesView's keyed {#each} (each_key_duplicate).
151+
const deduped = dedupeCanonicalKeys(properties);
152+
parseCache.set(rawFrontmatter, Object.freeze(deduped.map(cloneProperty)));
149153
evictLru();
150-
return properties;
154+
return deduped;
151155
}
152156

153157
function evictLru(): void {

src/tests/lib/features/properties/properties.logic.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ describe('parseFrontmatterProperties', () => {
184184
});
185185
});
186186

187+
it('collapses an alias and its canonical twin into a single entry', () => {
188+
// `color` and `_color` both canonicalize to `_color`. Without de-dup the
189+
// store would hold two `_color` entries and PropertiesView's keyed {#each}
190+
// throws each_key_duplicate, breaking the whole panel.
191+
const content = '---\ncolor: red\n_color: blue\n---\nBody';
192+
const props = parseFrontmatterProperties(content);
193+
const colorProps = props.filter((p) => p.key === '_color');
194+
expect(colorProps).toHaveLength(1);
195+
// First appearance wins (matches serializeProperties' dedupe semantics).
196+
expect(colorProps[0]).toEqual({ key: '_color', value: 'red', type: 'text' });
197+
});
198+
187199
it('returns empty array when no frontmatter', () => {
188200
expect(parseFrontmatterProperties('Just text')).toEqual([]);
189201
});

tasks/todo/bug-hunt-fixes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This batch fixes the **7 HIGH** bugs (one commit each). Medium + Low are backlog
1111
- [x] H2: auto-move unarchive hardcodes `"_archive"` suffix -> never fires for other `archiveTo` destinations. `src/lib/features/auto-move/type-lifecycle-rules.ts:22-32`. Fix: derive suffix from resolved `metadata.archiveTo` tail, not literal.
1212
- [x] H3: collection parser — method/field chaining on a function-call result broken (`now().format()`, `today().date()`). `src/lib/features/collection/expression/parser.ts:130-143,206-212`. Fix: structural `methodCall` node holding receiver ASTNode, not flattened dotted string.
1313
- [x] H4: collection filter — single-row `not` group collapses to bare positive expression, inverting filter + persisting inverted YAML. `src/lib/features/collection/toolbar/filter.logic.ts:146-148`. Fix: shortcut only when `conjunction !== 'not'`.
14-
- [ ] H5: properties panel crashes (Svelte `each_key_duplicate`) when frontmatter has alias + canonical twin (`color` + `_color`). `src/lib/features/properties/PropertiesView.svelte:92-96,221`. Fix: `dedupeCanonicalKeys` on parse path (or key=index).
14+
- [x] H5: properties panel crashes (Svelte `each_key_duplicate`) when frontmatter has alias + canonical twin (`color` + `_color`). `src/lib/features/properties/PropertiesView.svelte:92-96,221`. Fix: `dedupeCanonicalKeys` on parse path (or key=index).
1515
- [ ] H6: kanban drag-drop misplaces cards (DOM filtered index spliced into full array) when a filter is active -> corrupts `.kanban`. `src/lib/plugins/kanban/KanbanView.svelte:372-393`. Fix: disable drag while filtered, or map filtered->absolute index.
1616
- [ ] H7: Table of Contents empty for CRLF files (`\r` breaks HEADING_RE). `src/lib/plugins/table-of-contents/toc.logic.ts:63-93` (root: `wikilink/navigation.logic.ts:2`). Fix: split `/\r?\n/` or strip `\r`; also fixes wikilink `#heading` jump on CRLF docs.
1717

0 commit comments

Comments
 (0)