From 4f54849991b40811a01bd2c60bc12936c38daf2f Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 8 Apr 2026 08:02:06 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20add=20ADRs=200046=E2=80=930049=20for=20?= =?UTF-8?q?starter=20vault=20clone,=20regex=20filters,=20relative=20date?= =?UTF-8?q?=20filters,=20per-note=20icon=20(guard=20=E2=80=94=20from=20com?= =?UTF-8?q?mits=20b0950c92,=20c19cefe9,=2058dfb039,=2066a91274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0046-starter-vault-cloned-from-github.md | 36 ++++++++++++ ...7-regex-mode-for-view-filter-conditions.md | 34 ++++++++++++ ...lative-date-expressions-in-view-filters.md | 46 ++++++++++++++++ docs/adr/0049-per-note-icon-property.md | 55 +++++++++++++++++++ docs/adr/README.md | 4 ++ 5 files changed, 175 insertions(+) create mode 100644 docs/adr/0046-starter-vault-cloned-from-github.md create mode 100644 docs/adr/0047-regex-mode-for-view-filter-conditions.md create mode 100644 docs/adr/0048-relative-date-expressions-in-view-filters.md create mode 100644 docs/adr/0049-per-note-icon-property.md diff --git a/docs/adr/0046-starter-vault-cloned-from-github.md b/docs/adr/0046-starter-vault-cloned-from-github.md new file mode 100644 index 00000000..a493f3ec --- /dev/null +++ b/docs/adr/0046-starter-vault-cloned-from-github.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0046" +title: "Starter vault cloned from GitHub at runtime — no bundled content" +status: active +date: 2026-04-08 +--- + +## Context + +Laputa ships an optional "Getting Started" vault to help new users understand types, properties, wikilinks, and relationships. Previously, all starter content (markdown files, view YAMLs) was stored inside the app repo under `getting-started-vault/` and written to disk via `create_getting_started_vault()`. This created friction: updating sample content required a new app release, the content grew stale quickly, and the bundled files added noise to the main repo. + +## Decision + +**The Getting Started vault is no longer bundled in the app repo. On first launch, if the user selects "Get started with a template", the app clones the public starter repo (`https://github.com/refactoringhq/laputa-getting-started.git`) into a user-chosen folder using the existing git clone infrastructure.** + +- `getting_started.rs` now holds only the public repo URL constant and delegates to `clone_public_repo()`. +- The `getting-started-vault/` directory has been removed from the app repo. +- `create_getting_started_vault(targetPath)` takes an explicit target path (chosen via folder picker) instead of defaulting to Documents/Getting Started. +- Clone failures show a user-friendly error with an inline "Retry download" button (`canRetryTemplate`, `retryCreateVault`). +- A `clone_public_repo()` function was added to `github/clone.rs` to clone unauthenticated public repos without injecting OAuth tokens or configuring remote auth. + +## Options considered + +- **Option A — Keep bundled content (status quo)**: Simple, works offline. Downside: content tied to app release cycle, repo noise, growing file count. +- **Option B — Clone from GitHub at runtime (chosen)**: Content is always current; starter vault can be updated without an app release; removes ~25 markdown files + YAML from the main repo. Downside: requires network on first use; failure modes need UX handling (retry flow added). +- **Option C — Download a zip archive**: Avoids a git clone, smaller payload. Downside: loses the clean git history in the cloned vault; adds a zip extraction code path. + +## Consequences + +- New users need a network connection when selecting the template option. The empty vault and open-folder paths remain fully offline. +- The starter repo (`laputa-getting-started`) becomes a separate maintenance artifact. +- `LAPUTA_GETTING_STARTED_REPO_URL` env var allows overriding the URL in tests without hitting GitHub. +- Onboarding UX now distinguishes three creation modes: `creatingAction: 'template' | 'empty' | null`, each with distinct button state and status copy. +- Retry UX: `lastTemplatePath` is cached in `useOnboarding` so users can retry a failed clone to the same folder without re-picking it. +- Re-evaluation trigger: if offline-first support becomes a priority, consider bundling a minimal vault again or shipping a fallback zip. diff --git a/docs/adr/0047-regex-mode-for-view-filter-conditions.md b/docs/adr/0047-regex-mode-for-view-filter-conditions.md new file mode 100644 index 00000000..97d6a83a --- /dev/null +++ b/docs/adr/0047-regex-mode-for-view-filter-conditions.md @@ -0,0 +1,34 @@ +--- +type: ADR +id: "0047" +title: "Regex mode for view filter conditions" +status: active +date: 2026-04-08 +--- + +## Context + +The view filter engine (ADR 0040) supports operators like `contains`, `equals`, `not_contains`, `not_equals` with literal string matching. Power users who want pattern-based filtering (e.g., "all notes whose title matches a date pattern", "any property matching a URL regex") cannot express this with literals alone. + +## Decision + +**A `regex: true` flag is added to `FilterCondition`. When set, the `value` field is interpreted as a case-insensitive regular expression (via `regex::RegexBuilder` in Rust, and the native JS `RegExp` in TypeScript) for the operators that support it: `contains`, `equals`, `not_contains`, `not_equals`.** + +- Regex is opt-in: the `regex` field defaults to `false` and is skipped during serialization when false (no noise in existing `.yml` files). +- If the regex fails to compile, the condition evaluates to `false` rather than throwing. +- For relationship fields, the regex is tested against all candidate forms: the raw wikilink string, the inner stem, and the alias (if present). +- The `FilterBuilder` UI gains a regex toggle icon button next to value inputs for supported operators. +- TypeScript `viewFilters.ts` mirrors the same regex logic for client-side evaluation. + +## Options considered + +- **Option A — Add regex operator variants** (`regex_equals`, `regex_contains`): More explicit in YAML. Downside: doubles the operator set; no clear path to combine regex with `not_contains`. +- **Option B — Per-condition `regex: bool` flag (chosen)**: Composable with existing operators; minimal schema change; serialization skips the field when false so existing views are unaffected. +- **Option C — Full query language** (e.g., JMESPath or SQL `WHERE`): Maximum power. Out of scope; would replace rather than extend the filter engine. + +## Consequences + +- New dependency: `regex` crate in Rust (already present for other vault modules; no net new dep). +- Filter YAML files that use `regex: true` require Laputa ≥ this version to evaluate correctly; older versions silently ignore the flag (falling back to `regex: false` default via `#[serde(default)]`). +- Regex evaluation has a small performance cost vs. literal matching. No memoization of compiled regexes per evaluation call — acceptable given vault sizes (< 10k notes). +- Re-evaluation trigger: if regex performance becomes measurable, cache compiled `Regex` objects keyed by pattern string. diff --git a/docs/adr/0048-relative-date-expressions-in-view-filters.md b/docs/adr/0048-relative-date-expressions-in-view-filters.md new file mode 100644 index 00000000..b58dbd01 --- /dev/null +++ b/docs/adr/0048-relative-date-expressions-in-view-filters.md @@ -0,0 +1,46 @@ +--- +type: ADR +id: "0048" +title: "Relative date expressions in view filter conditions" +status: active +date: 2026-04-08 +--- + +## Context + +The view filter engine (ADR 0040) supports `before` and `after` operators but previously compared values as raw strings, meaning users had to write absolute ISO dates (e.g., `2026-04-01`) that became stale immediately. Views like "notes modified in the last 7 days" required updating the date manually every week. + +## Decision + +**The `before` and `after` filter operators now accept relative date expressions in addition to absolute ISO dates. Both the Rust backend and the TypeScript client independently parse the expression before comparing.** + +### Supported syntax + +| Expression | Meaning | +|---|---| +| `today` | Start of the current day (00:00 UTC) | +| `yesterday` | Start of yesterday | +| `tomorrow` | Start of tomorrow | +| `N days ago` / `N weeks ago` / `N months ago` / `N years ago` | Past relative | +| `in N days` / `in N weeks` / `in N months` / `in N years` | Future relative | + +Word-form amounts are also accepted: `one`, `two`, `three`, … `twelve`. + +### Architecture + +- **Rust** (`views.rs`): `parse_date_filter_timestamp()` resolves both field values and condition values to `i64` timestamps before comparing. Falls back gracefully when a value cannot be parsed. +- **TypeScript** (`utils/filterDates.ts`): `parseDateFilterInput()` and `toDateFilterTimestamp()` mirror the same logic for client-side filter evaluation. `date-fns` is used for date arithmetic. +- Both implementations use "start of day UTC" (00:00:00) as the anchor for relative expressions, consistent with how note creation/modification dates are stored. + +## Options considered + +- **Option A — Store and evaluate absolute dates only**: No parsing cost. Downside: views become stale; users must update dates manually. +- **Option B — Relative expressions resolved at evaluation time (chosen)**: Views stay perpetually current ("last 7 days" always means last 7 days). Downside: parallel implementation in Rust and TypeScript must stay in sync. +- **Option C — Pre-resolve relative expressions to absolute dates on save**: Expressions are human-readable when authoring but stored as ISO strings. Downside: view files drift; loses the relative intent. + +## Consequences + +- Relative expressions are evaluated at query time using the server/client clock. A view evaluated at 23:59 and 00:01 may return different results for "today". +- Both parsers share the same resolution anchor (start-of-day UTC). Timezone-sensitive relative expressions (e.g., "yesterday in Tokyo") are not supported. +- Existing `.yml` files with absolute ISO dates continue to work unchanged — the parser first tries ISO format before attempting relative parsing. +- Re-evaluation trigger: if timezone-aware relative dates become a user need, the expression syntax and anchor logic need revisiting. diff --git a/docs/adr/0049-per-note-icon-property.md b/docs/adr/0049-per-note-icon-property.md new file mode 100644 index 00000000..75929781 --- /dev/null +++ b/docs/adr/0049-per-note-icon-property.md @@ -0,0 +1,55 @@ +--- +type: ADR +id: "0049" +title: "Per-note icon property (_icon on individual notes)" +status: active +date: 2026-04-08 +--- + +## Context + +Laputa already supports type-level icons via the `_icon` system property on type documents (ADR 0008). Every note of a given type inherits the type's icon. Users needed a way to give individual notes a distinct visual identity without changing the type — e.g., marking a specific project with a rocket emoji, or a key person with a star icon — without creating a new type just for one note. + +## Decision + +**The `_icon` system property (already used by type documents) is now also supported on regular notes. When a note has an `_icon` value, it overrides the inherited type icon in all UI surfaces. The value may be an emoji, a Phosphor icon name, or an HTTP(S) image URL.** + +### Resolution logic + +`resolveNoteIcon(icon)` in `utils/noteIcon.ts` returns a discriminated union: + +| Kind | Condition | +|---|---| +| `none` | Value is empty/null | +| `emoji` | Value passes `isEmoji()` | +| `image` | Value is an HTTP(S) URL | +| `phosphor` | Value matches a registered Phosphor icon name | + +The `NoteTitleIcon` component renders the correct element for each kind (span, ``, or Phosphor SVG component). + +### UI surfaces updated + +- Editor breadcrumb bar (clicking the icon opens the `_icon` property editor) +- Note list items (`NoteItem`) +- Search panel results +- Relationship chips (shows icons on wikilink chips) +- Sidebar type sections +- Backlinks / ReferencedBy panels +- Inspector pinned area + +### Editing + +A custom event (`laputa:focus-note-icon-property`) is dispatched from the breadcrumb bar click to focus the `_icon` field in the Properties panel without scrolling. The field uses the existing property editor UI. + +## Options considered + +- **Option A — Separate `_note_icon` property**: Avoids ambiguity with the type-level `_icon`. Downside: two names for the same concept depending on context; complicates the resolver. +- **Option B — Reuse `_icon` on notes (chosen)**: Consistent with existing convention (ADR 0008); type docs and note docs follow the same schema. The distinction between type-level and note-level is determined by `is_a: Type` in the frontmatter, not by a different property name. +- **Option C — Inline emoji in note title**: Zero-friction. Downside: title is also the filename (ADR 0044); emojis in filenames cause filesystem/git pain. + +## Consequences + +- `_icon` on a note overrides the type icon everywhere. A note with no `_icon` continues to inherit the type icon (no behavior change for existing vaults). +- The icon resolver (`resolveNoteIcon`) is shared between note icons and type icons; future changes to icon resolution affect both. +- `iconRegistry.ts` grows with any new Phosphor icon additions — currently loaded eagerly. If the icon set grows large, lazy loading or a build-time icon map should be considered. +- Re-evaluation trigger: if users request per-note color (the `_color` system property currently only applies to types), the same resolution pattern can be extended. diff --git a/docs/adr/README.md b/docs/adr/README.md index a6939312..f6a0ac97 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -101,3 +101,7 @@ proposed → active → superseded | [0043](0043-reactive-vault-state-on-save.md) | Reactive vault state: editor changes propagate immediately to all UI | active | | [0044](0044-h1-as-title-primary-source.md) | H1 as primary title source — filename as stable identifier | active | | [0045](0045-permanent-delete-no-trash.md) | Permanent delete with confirm modal — no Trash system | active | +| [0046](0046-starter-vault-cloned-from-github.md) | Starter vault cloned from GitHub at runtime — no bundled content | active | +| [0047](0047-regex-mode-for-view-filter-conditions.md) | Regex mode for view filter conditions | active | +| [0048](0048-relative-date-expressions-in-view-filters.md) | Relative date expressions in view filter conditions | active | +| [0049](0049-per-note-icon-property.md) | Per-note icon property (_icon on individual notes) | active |