feat: replace 'Is a' / 'is_a' with 'type' as the canonical frontmatter key

- Rust Frontmatter struct now parses both 'type' (new) and 'Is A'/'is_a' (legacy),
  with 'type' taking precedence via resolve_type()
- Added migrate_is_a_to_type() vault-wide migration (runs on startup, also exposed as Tauri command)
- Frontend: buildNoteContent and resolveNewType now emit 'type:' in YAML
- Inspector SKIP_KEYS hides 'is_a', 'Is A', 'type', 'title' from property list
  (TypeRow already renders the type with its dedicated UI)
- frontmatterToEntryPatch handles both 'type' and legacy 'is_a' keys
- Mock data updated: all YAML content uses 'type:' instead of 'is_a:'/'Is A:'
- Tests updated to expect 'type:' in generated frontmatter

Product decision: internal VaultEntry field stays as `isA` (TS) / `is_a` (Rust)
to minimize blast radius. The user-facing change is the YAML key and inspector.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-02-23 15:24:57 +01:00
parent d54c956082
commit c8e87f0ab9
6 changed files with 196 additions and 51 deletions

View File

@@ -166,12 +166,12 @@ describe('entryMatchesTarget', () => {
describe('buildNoteContent', () => {
it('generates frontmatter with status for regular types', () => {
const content = buildNoteContent('My Note', 'Note', 'Active')
expect(content).toBe('---\ntitle: My Note\nis_a: Note\nstatus: Active\n---\n\n# My Note\n\n')
expect(content).toBe('---\ntitle: My Note\ntype: Note\nstatus: Active\n---\n\n# My Note\n\n')
})
it('omits status when null', () => {
const content = buildNoteContent('AI', 'Topic', null)
expect(content).toBe('---\ntitle: AI\nis_a: Topic\n---\n\n# AI\n\n')
expect(content).toBe('---\ntitle: AI\ntype: Topic\n---\n\n# AI\n\n')
})
})
@@ -181,7 +181,7 @@ describe('resolveNewNote', () => {
expect(entry.path).toBe('/Users/luca/Laputa/project/my-project.md')
expect(entry.isA).toBe('Project')
expect(entry.status).toBe('Active')
expect(content).toContain('is_a: Project')
expect(content).toContain('type: Project')
expect(content).toContain('status: Active')
})
@@ -208,13 +208,14 @@ describe('resolveNewType', () => {
expect(entry.path).toBe('/Users/luca/Laputa/type/recipe.md')
expect(entry.isA).toBe('Type')
expect(entry.status).toBeNull()
expect(content).toContain('Is A: Type')
expect(content).toContain('type: Type')
expect(content).toContain('# Recipe')
})
})
describe('frontmatterToEntryPatch', () => {
it.each([
['type', 'Project', { isA: 'Project' }],
['is_a', 'Project', { isA: 'Project' }],
['status', 'Done', { status: 'Done' }],
['color', 'red', { color: 'red' }],