docs(05): create phase plan
Phase 05: Search - 3 plans in 2 waves - Wave 1: 05-01 (UI components), 05-02 (filtering logic) — parallel - Wave 2: 05-03 (integration + recent searches + "/" shortcut) - Ready for execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
206
.planning/phases/05-search/05-02-PLAN.md
Normal file
206
.planning/phases/05-search/05-02-PLAN.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
phase: 05-search
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/lib/utils/filterEntries.ts
|
||||
- src/lib/utils/highlightText.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Entries can be filtered by text search (title + content)"
|
||||
- "Entries can be filtered by tag (AND logic for multiple tags)"
|
||||
- "Entries can be filtered by entry type (task/thought)"
|
||||
- "Entries can be filtered by date range"
|
||||
- "Matching text can be highlighted in search results"
|
||||
artifacts:
|
||||
- path: "src/lib/utils/filterEntries.ts"
|
||||
provides: "Pure function to filter entries by SearchFilters"
|
||||
exports: ["filterEntries"]
|
||||
min_lines: 30
|
||||
- path: "src/lib/utils/highlightText.ts"
|
||||
provides: "Function to wrap matching text in mark tags"
|
||||
exports: ["highlightText"]
|
||||
min_lines: 15
|
||||
key_links:
|
||||
- from: "src/lib/utils/filterEntries.ts"
|
||||
to: "SearchFilters type"
|
||||
via: "import"
|
||||
pattern: "import.*SearchFilters.*from.*search"
|
||||
- from: "src/lib/utils/highlightText.ts"
|
||||
to: "mark element"
|
||||
via: "string replacement"
|
||||
pattern: "<mark.*class=.*font-bold"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the filtering logic and text highlighting utilities.
|
||||
|
||||
Purpose: Implement pure functions that filter entries based on SearchFilters criteria and highlight matching text. These are decoupled from UI components for testability.
|
||||
Output: filterEntries.ts and highlightText.ts utilities ready for use in the integrated search view.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/tho/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/tho/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/05-search/05-CONTEXT.md
|
||||
@.planning/phases/05-search/05-RESEARCH.md
|
||||
@src/lib/server/db/schema.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create filterEntries utility function</name>
|
||||
<files>src/lib/utils/filterEntries.ts</files>
|
||||
<action>
|
||||
Create filterEntries.ts with a pure function that filters entries:
|
||||
|
||||
```typescript
|
||||
import type { SearchFilters } from '$lib/types/search';
|
||||
import type { Entry, Tag } from '$lib/server/db/schema';
|
||||
|
||||
interface EntryWithTags extends Entry {
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
export function filterEntries(
|
||||
entries: EntryWithTags[],
|
||||
filters: SearchFilters
|
||||
): EntryWithTags[] {
|
||||
let result = entries;
|
||||
|
||||
// Text search (title + content) - minimum 2 characters
|
||||
if (filters.query.length >= 2) {
|
||||
const query = filters.query.toLowerCase();
|
||||
result = result.filter(e =>
|
||||
(e.title?.toLowerCase().includes(query)) ||
|
||||
e.content.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Tag filter - AND logic (must have ALL selected tags)
|
||||
if (filters.tags.length > 0) {
|
||||
result = result.filter(e =>
|
||||
filters.tags.every(tagName =>
|
||||
e.tags.some(t => t.name.toLowerCase() === tagName.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (filters.type !== 'all') {
|
||||
result = result.filter(e => e.type === filters.type);
|
||||
}
|
||||
|
||||
// Date range filter (using createdAt)
|
||||
if (filters.dateRange.start) {
|
||||
result = result.filter(e => e.createdAt >= filters.dateRange.start!);
|
||||
}
|
||||
if (filters.dateRange.end) {
|
||||
// Include the end date by comparing to end of day
|
||||
const endOfDay = filters.dateRange.end + 'T23:59:59';
|
||||
result = result.filter(e => e.createdAt <= endOfDay);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
Implementation notes:
|
||||
- Case-insensitive text search (lowercase comparison)
|
||||
- Case-insensitive tag matching (tags stored with original case but matched insensitively per 04-01 decision)
|
||||
- AND logic for tags (every selected tag must be present)
|
||||
- Date comparison works with ISO strings (schema uses ISO format)
|
||||
- End date is inclusive (compare to end of day)
|
||||
</action>
|
||||
<verify>TypeScript compiles: `npx tsc --noEmit`</verify>
|
||||
<done>filterEntries function filters entries by query, tags, type, and date range</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create highlightText utility function</name>
|
||||
<files>src/lib/utils/highlightText.ts</files>
|
||||
<action>
|
||||
Create highlightText.ts for search result highlighting:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Wraps matching text in <mark> tags for highlighting.
|
||||
* Returns HTML string to be used with {@html} in Svelte.
|
||||
*
|
||||
* @param text - The text to search within
|
||||
* @param query - The search query to highlight
|
||||
* @returns HTML string with matches wrapped in <mark> tags
|
||||
*/
|
||||
export function highlightText(text: string, query: string): string {
|
||||
// Don't highlight if query is too short
|
||||
if (!query || query.length < 2) return escapeHtml(text);
|
||||
|
||||
// Escape HTML in the original text first
|
||||
const escaped = escapeHtml(text);
|
||||
|
||||
// Escape regex special characters in query
|
||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
||||
|
||||
// Wrap matches in <mark> with bold styling (no background per CONTEXT.md)
|
||||
return escaped.replace(regex, '<mark class="font-bold bg-transparent">$1</mark>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes HTML special characters to prevent XSS.
|
||||
*/
|
||||
function escapeHtml(text: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, char => map[char]);
|
||||
}
|
||||
```
|
||||
|
||||
Security notes:
|
||||
- Always escape HTML before highlighting to prevent XSS
|
||||
- The escapeHtml function runs BEFORE regex replacement
|
||||
- Result is safe to use with {@html} directive
|
||||
|
||||
Styling notes:
|
||||
- Uses font-bold for emphasis (per CONTEXT.md: "bold, not background highlight")
|
||||
- bg-transparent ensures no background color on <mark>
|
||||
</action>
|
||||
<verify>TypeScript compiles: `npx tsc --noEmit`</verify>
|
||||
<done>highlightText function wraps matching text in styled mark tags with XSS protection</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npm run check` passes
|
||||
- filterEntries correctly filters by each criterion
|
||||
- highlightText properly escapes HTML and highlights matches
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- filterEntries handles all filter types (query, tags, type, dateRange)
|
||||
- Tag filtering uses AND logic (entry must have ALL selected tags)
|
||||
- Text search is case-insensitive with 2-character minimum
|
||||
- highlightText escapes HTML to prevent XSS
|
||||
- Highlighted text uses bold styling without background
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/05-search/05-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user