feat: add title and description fields to capture grammar

Implement | prefix for titles and // separator for descriptions
across the full stack: parser, schema, API, CLI, and web frontend.

- Parser: line-aware extraction for |title, |title // desc,
  // leading desc, body // inline desc. URL-safe (skips :// lines).
  Modifiers (#tag, @time, ^card) extracted from all segments.
- Schema: ALTER TABLE migration adds title, description columns
- DB: Entity/EntityUpdate structs, all CRUD queries updated
- API: title/description on create/update/response, body validation
  relaxed (title-only entries valid)
- CLI: shows title as scan label when present
- Web: parseInput mirrors Go parser, list shows title, detail pane
  renders title + description with double-click inline editing
- Tests: 10 new cases (grammar, entity, API) — 71 total, all pass
This commit is contained in:
2026-05-15 20:52:58 -04:00
parent e708ea5c13
commit f5b46585c3
11 changed files with 683 additions and 159 deletions
+44 -21
View File
@@ -49,6 +49,8 @@ type Entity struct {
CreatedAt time.Time
ModifiedAt time.Time
Body string
Title *string
Description *string
Glyph Glyph
TimeAnchor *string
CompletedAt *time.Time
@@ -85,14 +87,16 @@ func DefaultListParams() ListParams {
}
type EntityUpdate struct {
Body *string
Glyph *Glyph
TimeAnchor *string
ClearTime bool
Pinned *bool
CardType *CardType
CardData *string
Tags *[]string
Body *string
Title *string
Description *string
Glyph *Glyph
TimeAnchor *string
ClearTime bool
Pinned *bool
CardType *CardType
CardData *string
Tags *[]string
}
func (s *Store) Create(e *Entity) error {
@@ -111,13 +115,16 @@ func (s *Store) Create(e *Entity) error {
defer tx.Rollback()
_, err = tx.Exec(`
INSERT INTO entities (id, created_at, modified_at, body, glyph, time_anchor,
completed_at, pinned, deleted_at, card_type, card_data, use_count, last_used_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
INSERT INTO entities (id, created_at, modified_at, body, title, description,
glyph, time_anchor, completed_at, pinned, deleted_at,
card_type, card_data, use_count, last_used_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
e.ID,
e.CreatedAt.Format(time.RFC3339),
e.ModifiedAt.Format(time.RFC3339),
e.Body,
e.Title,
e.Description,
string(e.Glyph),
e.TimeAnchor,
formatTimePtr(e.CompletedAt),
@@ -144,14 +151,17 @@ func (s *Store) Get(id string) (*Entity, error) {
var createdAt, modifiedAt string
var completedAt, deletedAt, lastUsedAt sql.NullString
var timeAnchor, cardType, cardData sql.NullString
var title, description sql.NullString
var pinned int
err := s.db.QueryRow(`
SELECT id, created_at, modified_at, body, glyph, time_anchor,
completed_at, pinned, deleted_at, card_type, card_data, use_count, last_used_at
SELECT id, created_at, modified_at, body, title, description,
glyph, time_anchor, completed_at, pinned, deleted_at,
card_type, card_data, use_count, last_used_at
FROM entities WHERE id = ?`, id).Scan(
&e.ID, &createdAt, &modifiedAt, &e.Body, &e.Glyph, &timeAnchor,
&completedAt, &pinned, &deletedAt, &cardType, &cardData, &e.UseCount, &lastUsedAt,
&e.ID, &createdAt, &modifiedAt, &e.Body, &title, &description,
&e.Glyph, &timeAnchor, &completedAt, &pinned, &deletedAt,
&cardType, &cardData, &e.UseCount, &lastUsedAt,
)
if err == sql.ErrNoRows {
return nil, ErrNotFound
@@ -162,6 +172,8 @@ func (s *Store) Get(id string) (*Entity, error) {
e.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
e.ModifiedAt, _ = time.Parse(time.RFC3339, modifiedAt)
e.Title = nullToPtr(title)
e.Description = nullToPtr(description)
e.TimeAnchor = nullToPtr(timeAnchor)
e.CompletedAt = parseTimePtr(completedAt)
e.Pinned = pinned != 0
@@ -234,9 +246,9 @@ func (s *Store) List(params ListParams) ([]*Entity, error) {
}
query := fmt.Sprintf(`
SELECT e.id, e.created_at, e.modified_at, e.body, e.glyph, e.time_anchor,
e.completed_at, e.pinned, e.deleted_at, e.card_type, e.card_data,
e.use_count, e.last_used_at
SELECT e.id, e.created_at, e.modified_at, e.body, e.title, e.description,
e.glyph, e.time_anchor, e.completed_at, e.pinned, e.deleted_at,
e.card_type, e.card_data, e.use_count, e.last_used_at
FROM entities e
%s
ORDER BY %s %s
@@ -256,18 +268,21 @@ func (s *Store) List(params ListParams) ([]*Entity, error) {
var createdAt, modifiedAt string
var completedAt, deletedAt, lastUsedAt sql.NullString
var timeAnchor, cardType, cardData sql.NullString
var title, description sql.NullString
var pinned int
if err := rows.Scan(
&e.ID, &createdAt, &modifiedAt, &e.Body, &e.Glyph, &timeAnchor,
&completedAt, &pinned, &deletedAt, &cardType, &cardData,
&e.UseCount, &lastUsedAt,
&e.ID, &createdAt, &modifiedAt, &e.Body, &title, &description,
&e.Glyph, &timeAnchor, &completedAt, &pinned, &deletedAt,
&cardType, &cardData, &e.UseCount, &lastUsedAt,
); err != nil {
return nil, err
}
e.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
e.ModifiedAt, _ = time.Parse(time.RFC3339, modifiedAt)
e.Title = nullToPtr(title)
e.Description = nullToPtr(description)
e.TimeAnchor = nullToPtr(timeAnchor)
e.CompletedAt = parseTimePtr(completedAt)
e.Pinned = pinned != 0
@@ -308,6 +323,14 @@ func (s *Store) Update(id string, u *EntityUpdate) error {
sets = append(sets, "body = ?")
args = append(args, *u.Body)
}
if u.Title != nil {
sets = append(sets, "title = ?")
args = append(args, *u.Title)
}
if u.Description != nil {
sets = append(sets, "description = ?")
args = append(args, *u.Description)
}
if u.Glyph != nil {
sets = append(sets, "glyph = ?")
args = append(args, string(*u.Glyph))