Files
Marcos Paulo 77d538dd97
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: Bases Cards, List, embeds, Map view, Maps settings and inline cell editing
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 04:45:32 -03:00

24 KiB

Tasks: Bases (table, cards, list, map)

One task = one agent's work (OpenCode brief template, specs/agents/opencode-runbook.md). [ ] open. Phases run in order; tasks inside a phase marked "parallel" touch disjoint files. Spec: spec.md (D-numbers below point into it). Rules for every task: AGENTS.md, docs/CODE-MAP.md.

Standing VERIFY (every task adds its own test filter first; the package is named in the task): cargo test -p <pkg> ; cargo clippy -p <pkg> --all-targets -- -D warnings ; rustfmt --edition 2024 <owned files>. UI/i18n tasks add cargo xtask i18n; last task of a phase adds cargo xtask layers and cargo xtask tree. Standing DO NOT: edit Cargo.toml/Cargo.lock, files outside OWNS, git state, cargo fmt --all; add allow attributes; use unwrap/expect/panic/[i] on input-derived data; add a dependency.

Owner/lead step 0 (not an agent task)

Create the empty crate crates/bases (package notescraft-bases, [lints] workspace = true, deps: notescraft-markdown, thiserror), register it in the root [workspace.dependencies] and in xtask/src/layers.rs at L2, add crates/engine/src/bases/ to nothing yet. Answer Open Questions Q1 and Q2 of the spec.

Phase 1 - the pure crate notescraft-bases (no I/O)

  • 1. YAML subset parser and emitter (lossless). OWNS: crates/bases/src/yaml.rs, crates/bases/src/lib.rs (module decl + crate docs), crates/bases/src/error.rs. FILES TO OPEN: crates/markdown/src/frontmatter/yaml.rs (style and the Value naming to avoid clashing), crates/plugins/src/manifest.rs (a small hand-written parser with limits). GOAL: parse(&str) -> Result<Node> with Node { Null, Bool, Num(f64), Str, Seq(Vec<Node>), Map(Vec<(String, Node)>) } (map keeps order), D3 subset, emit(&Node) -> String (2-space indent, quotes only when needed). Errors carry line and column. Limits: 1 MiB, depth 12, 10 000 nodes. TESTS: crates/bases/tests/yaml.rs: every YAML block of Bases syntax.md and the Layouts pages (copied into crates/bases/tests/fixtures/*.yaml) parses and emit(parse(x)) re-parses equal; key order preserved; | and > scalars; quoted formulas with nested quotes; flow lists; depth 13 and 2 MiB refused; a tab indent gives an error with the line. VERIFY: standing, -p notescraft-bases. DO NOT: add a YAML dependency; support anchors/tags.
  • 2. The .base model: filters, formulas, properties, summaries, views. OWNS: crates/bases/src/model.rs, crates/bases/src/base.rs. FILES TO OPEN: crates/bases/src/yaml.rs (task 1), spec.md D3. GOAL: Base::parse(&str) -> Result<Base>, Base::to_yaml() -> String; Filter { Expr(String), And(Vec<Filter>), Or(..), Not(..) }; View { kind: ViewKind (Table|Cards|List|Map|Other(String)), name, limit, filters, group_by, group_order, order, sort, summaries, extra: Vec<(String, Node)> }; unknown keys kept in extra at base and view level, emitted in original order. TESTS: tests/model.rs: round trip of the docs example is byte-equal after normalising comments; type: kanban and an unknown view key survive; limits (100 views, 200 formulas, 4 KiB expression) refused; limit: -1 and direction: SIDEWAYS are errors; missing views yields one default table view in memory (not written until edited). VERIFY: standing. DO NOT: evaluate expressions here.
  • 3. Expression lexer and parser (AST). OWNS: crates/bases/src/expr/mod.rs, crates/bases/src/expr/lex.rs, crates/bases/src/expr/parse.rs, crates/bases/src/expr/ast.rs. FILES TO OPEN: spec.md D5, Bases syntax.md operators section (in the help vault), crates/math/src/lib.rs (a hostile-input-safe parser to copy the depth-counter idea from). GOAL: parse_expr(&str) -> Result<Expr>; precedence and associativity of D5; literals, arrays, objects, member, index, call, method call, regex literal /re/flags, strings with escapes; Expr::deps() -> Deps (set of note.x, file.x, formula.x, this references, to drive lazy evaluation and cycle checks). Depth limit 64, length limit 4 KiB. TESTS: tests/expr_parse.rs: precedence table, a.b[0].c("x"), [latitude, longitude], price > 2.1 && status != "done", unterminated string, 65-deep parentheses refused, 5 000 random byte strings never panic. VERIFY: standing. DO NOT: implement evaluation.
  • 4. Values, types, dates, durations (parallel with 3). OWNS: crates/bases/src/value.rs, crates/bases/src/datetime.rs, crates/bases/src/compare.rs. FILES TO OPEN: spec.md D5 (types, ordering, equality), crates/templates/src/date.rs (civil-date algorithms to copy, NOT to depend on). GOAL: Value enum of D5; Date with ms, has_time; parse_duration("1M 4h"); add_duration, sub_dates; month clamp; compare(a,b) -> Ordering (total order), loose_eq; truthy; to_display_string. TESTS: tests/value.rs: the doc examples (2024-12-01 + 1M + 4h + 3m = 2025-01-01 04:03:00, (now()+1d)-now() = 86400000), Jan 31 + 1M, leap year, negative durations, ordering across types, NaN sorts last, Link == File. VERIFY: standing. DO NOT: read the clock (take now_ms as an argument).
  • 5. Evaluator core with the global, any, number, string and object functions. OWNS: crates/bases/src/eval/mod.rs, crates/bases/src/eval/ops.rs, crates/bases/src/eval/funcs_basic.rs, crates/bases/src/row.rs. FILES TO OPEN: crates/bases/src/expr/ast.rs, crates/bases/src/value.rs, spec.md D1, D4, D6. GOAL: trait NoteSource, RowRef (D1), Env { now_ms, seed, this }, eval(&Expr, &Env, &RowRef, &Formulas) -> Value; operators; lazy if; formula references with cycle detection (FormulaCycle once); functions: date duration if number min max now today random link list isTruthy isType toString escapeHTML html image icon, string (contains containsAll containsAny endsWith isEmpty lower replace repeat reverse slice split startsWith title trim length), number (abs ceil floor round toFixed isEmpty), object (keys values isEmpty). Errors are Value::Error(String), never panic. TESTS: tests/eval_basic.rs with a Vec<TestRow> source: one table-driven test per function (normal, null, wrong type), the ppu and formatted_price examples, cycle, division by zero, if laziness (the dead branch would error). VERIFY: standing. DO NOT: implement list/date/file/link functions (task 6).
  • 6. Date, list, link, file, regex functions. OWNS: crates/bases/src/eval/funcs_list.rs, crates/bases/src/eval/funcs_date.rs, crates/bases/src/eval/funcs_file.rs, crates/bases/src/eval/regex.rs. FILES TO OPEN: crates/bases/src/eval/mod.rs (task 5, the registration table), crates/regex/src/lib.rs (engine to use), Functions.md (help vault) sections Date/List/Link/File/Regexp. GOAL: list contains containsAll containsAny filter flat join map reduce reverse slice sort unique length isEmpty (lambda variables value index acc); date year..millisecond format time relative date isEmpty (format tokens YYYY MM DD HH mm ss plus the Moment subset of notescraft-templates::format semantics, re-implemented); link asFile linksTo; file asLink hasLink hasProperty hasTag inFolder and the file.* fields through RowRef; regex matches, string replace with a regex and $1. TESTS: tests/eval_lists.rs, eval_dates.rs, eval_files.rs: each function per the docs; hasTag matches nested tags; inFolder matches descendants; relative gives "3 days ago"; list(type)[0].asFile().properties.icon with a test source; a pathological regex ((a+)+$) is stopped by the regex crate's step limit. VERIFY: standing. DO NOT: touch eval/mod.rs beyond the single registration line it names.
  • 7. Query pipeline: filter, sort, group, limit, summaries. OWNS: crates/bases/src/query/mod.rs, crates/bases/src/query/summary.rs, crates/bases/src/query/columns.rs. FILES TO OPEN: crates/bases/src/eval/mod.rs, crates/bases/src/model.rs, spec.md D7. GOAL: run(&Base, view, &dyn NoteSource, &Env) -> QueryResult with RowIds, columns (order or defaults file.name), multi-key stable sort evaluating only sort keys, groupBy + groupOrder (null key), limit, default summaries of D6 and custom summaries over values; QueryResult::cell(i, col) lazy; errors counted; dependency analysis so unused properties are never read. TESTS: tests/query.rs: a 12-row fixture with a golden table for filter+sort+group+summary; groupOrder hides groups; null in groupOrder; custom summary values.mean().round(3); 15 000 synthetic rows, 1 filter + 1 sort in < 60 ms (#[ignore] perf test in perf/ per AGENTS, plus a smaller always-on bound); no cell computed for rows not asked for (counter). VERIFY: standing + cargo xtask layers. DO NOT: copy rows; clone Values of the source.
  • 8. Map data model: coordinates, projection, clustering (parallel with 7). OWNS: crates/bases/src/map/mod.rs, crates/bases/src/map/project.rs, crates/bases/src/map/cluster.rs, crates/bases/src/map/color.rs. FILES TO OPEN: spec.md D13, crates/bases/src/value.rs. GOAL: LatLng, parse_coordinates(&Value), project/unproject, tiles_for_viewport(center, zoom, px) -> Vec<TileId>, cluster(markers, zoom, radius), fit, parse_color (#rgb #rrggbb rgb() named var(--color-x) -> enum Color::Rgb | Color::Token(name)), attribution() constants. Out-of-range or non-finite coordinates are skipped and counted. TESTS: tests/map.rs: Paris at zoom 0/10/18 against known tile numbers (48.85837, 2.294481 z=10 -> x=518 y=352), round trip project/unproject within 1e-9, poles beyond 85.0511 rejected, antimeridian, clustering of 360 random markers < 1 ms and cluster counts sum to 360, fit of two points, parse_coordinates for "lat, lng", ["lat","lng"], [lat,lng], garbage. VERIFY: standing. DO NOT: do any I/O or networking.

Phase 2 - index and engine

  • 9. The index keeps properties, size and times. OWNS: crates/index/src/model.rs, crates/index/src/index.rs, crates/index/src/props.rs, crates/index/tests/props.rs, crates/engine/src/vault/index_cache/records.rs, crates/engine/src/vault/index_cache/codec.rs (version bump). FILES TO OPEN: crates/index/src/index.rs (update_note, insert_info), crates/markdown/src/frontmatter/mod.rs (parse, infer_type), crates/engine/src/vault/properties.rs (type names, .obsidian/types.json), spec.md D2. GOAL: NoteInfo.props: Vec<(String, PropValue)>, size: u64, ctime_ms, mtime_ms; Index::note_props(path), Index::property_keys() -> Vec<(String, PropType, usize)>; limits of D2; cache version bump so an old cache rebuilds. TESTS: crates/index/tests/props.rs: keys/values/types, list and date inference, 300 properties capped and flagged, rename keeps props, a 15 000-note build with 8 props adds < 15% time (ignored perf test); engine cache round trip + old-version rejection. VERIFY: cargo test -p notescraft-index -p notescraft-engine index_cache, clippy for both, cargo xtask layers. DO NOT: change search ranking or link resolution.
  • 10. NoteSource over the index and base.read / base.list / base.properties / base.query. OWNS: crates/engine/src/bases/mod.rs, crates/engine/src/bases/source.rs, crates/engine/src/bases/commands.rs, one line in crates/engine/src/lib.rs, one register_bases(reg) call in crates/engine/src/commands.rs, crates/engine/src/ai/assistant_gate.rs (READ list), crates/engine/tests/bases_query.rs, crates/engine/tests/core_commands.rs (CORE_IDS). FILES TO OPEN: crates/engine/src/review/commands.rs (group to copy), crates/engine/src/spec.rs, crates/engine/src/params.rs, docs/CODE-MAP.md (Engine table). GOAL: D10 read commands; inline yaml or path; this; offset/limit slicing (default 200, max 2000); a result cache keyed by index generation (D7). TESTS: bases_query.rs drives the registry on a TempVault of 50 notes: golden JSON for filter/sort/group/summary, inline yaml, this, invalid yaml gives invalid_params with line, a missing base gives not_found. VERIFY: standing for notescraft-engine + cargo test -p notescraft-engine --test bases_query. DO NOT: edit Session beyond one accessor if unavoidable (name it in the report).
  • 11. Write commands: base.write, base.create, base.view.*, base.row.set. OWNS: crates/engine/src/bases/write.rs, crates/engine/src/bases/commands.rs (append), crates/engine/tests/bases_write.rs, crates/engine/src/ai/assistant_gate.rs (WRITE list), CORE_IDS. FILES TO OPEN: crates/engine/src/vault/properties.rs (set), crates/engine/src/builtin/properties_commands.rs (how note.property_set is wired), crates/engine/src/review/store.rs (a write through Vault::write). GOAL: D9 and D10; base.row.set reuses properties::set; rejects file.*/formula.* (except file.name -> note.rename); lossless view edits via task 2's to_yaml. TESTS: bases_write.rs: row set keeps the body byte-identical, honours .obsidian/types.json, refuses invalid date, undo through the buffer when the note is open, view add/duplicate/remove keep unknown keys, write refused in a read-only session, base.create seeds a tag filter. VERIFY: standing. DO NOT: write outside the vault; skip craft-atomic.
  • 12. Tile fetcher and cache (Maps, network policy D13). OWNS: crates/engine/src/bases/tiles/mod.rs, crates/engine/src/bases/tiles/fetch.rs, crates/engine/src/bases/tiles/cache.rs, crates/engine/src/bases/tiles/limits.rs, crates/engine/src/prefs/maps_config.rs, crates/engine/src/prefs/catalog/ (one entry file), crates/engine/tests/maps_tiles.rs, crates/engine/tests/maps_tiles_stub/. FILES TO OPEN: crates/engine/src/share/client.rs (blocking ureq client, UA and timeouts), crates/engine/tests/publish_stub/ (a local stub server), crates/engine/src/prefs/files_config.rs + crates/engine/tests/files_config_viewers.rs (settings pattern), spec.md D13. GOAL: trait TileFetcher; HttpTileFetcher (ureq, UA NotesCraft/<ver> (+repo), https only, 5 s, 256 KiB, image/png check, no Referer); token bucket (burst 8, then 1 req/s) and 2 concurrent; disk cache .notescraft/cache/tiles/<host>/<z>/<x>/<y>.png with a 7-day floor, LRU eviction at maps.cache_mb (default 200); maps.status, maps.clear_cache commands; settings maps.enabled (default false), maps.use_tiles, maps.tile_url (validated template, https + {z}{x}{y}); everything idle when disabled. TESTS: maps_tiles.rs: disabled -> the stub counts 0 requests; UA header seen by the stub; 20 tiles requested -> max 2 in flight and the rate limit holds (fake clock); oversized body and wrong content-type dropped; cached tile served with the stub down; expired tile refetched after 7 days (fake clock); eviction at the cap; an http:// or template without {z} refused. VERIFY: standing. DO NOT: contact any real server in tests; prefetch outside the viewport; add a dependency (ureq is approved).
  • 13. Sync ignore for .notescraft/cache and the property rows in the MCP base_query tool. OWNS: crates/engine/src/sync/ (the ignore list file only), crates/mcp/src/calls/bases.rs, crates/mcp/src/lib.rs (one registration line), crates/mcp/tests/bases.rs. FILES TO OPEN: crates/mcp/src/calls/text.rs (a read tool), crates/engine/src/sync/worker.rs (where paths are filtered). GOAL: .notescraft/cache/** never syncs; MCP tool base_query {path|yaml, view, limit} returns base.query JSON. TESTS: a path under the cache is skipped by the ledger; the MCP tool lists in tools/list and answers a query. VERIFY: standing for both packages. DO NOT: add write tools to MCP.

Phase 3 - the Table view

  • 14. TabKind::Base, view shell, toolbar, view menu, status line. OWNS: crates/ui-egui/src/views/base_view/mod.rs, .../toolbar.rs, .../view_menu.rs, .../status.rs, .../state.rs, crates/ui-egui/src/widgets/tabs/kind.rs (one arm Some("base") => Self::Base + icon + label), crates/ui-egui/src/shell/app/window/views.rs (one dispatch arm), crates/ui-egui/src/text/views/base_view/mod.rs, crates/i18n/locales/en.toml (new [ui.base-view] at the END), assets/icons/ (table-2.svg, layout-grid.svg, list.svg, map.svg, list-filter.svg, arrow-up-down.svg, stretch-horizontal.svg if missing) and crates/ui-egui/src/design/icons.rs. FILES TO OPEN: crates/ui-egui/src/views/calendar_view/ (a view with a toolbar and engine calls), crates/ui-egui/src/shell/app/window/views.rs, crates/ui-egui/src/widgets/tabs/kind.rs, .claude/skills/notescraft-egui-screen/SKILL.md. GOAL: opening x.base shows the toolbar of Views.md, the view switcher with Add/Duplicate/Rename/Delete (engine base.view.*), a raw-YAML fallback card on parse errors, the status line (N results, E errors); the query runs through engine_command("base.query", ..) on a worker thread polled each frame; no table yet (a placeholder body). TESTS: crates/ui-egui/tests/base_view.rs (kittest): opens a base from a fixture vault, switches views, adds one, a broken file shows the fallback with a line number. VERIFY: standing + cargo xtask i18n. DO NOT: draw cells (task 15); hardcode strings; use raw egui Buttons where a widget exists.
  • 15. Table rendering: virtualised grid, cells, sorting, resize, summaries. OWNS: crates/ui-egui/src/views/base_view/table.rs, .../table_header.rs, .../cell.rs, .../summary_row.rs, .../group.rs. FILES TO OPEN: crates/ui-egui/src/panels/tree/ (a virtualised list with sticky parts), crates/ui-egui/src/views/properties_view/rows.rs (typed value widgets to reuse), crates/ui-egui/src/views/note_view/blocks/embed.rs (link click -> open note). GOAL: show_rows virtualisation, row heights from rowHeight, sticky header, header click sorts (writes sort via base.view.set), drag-resize writes columnSize, drag-reorder writes order, group header rows with count + summary, bottom summary row with the Summarize menu (right-click header), typed cells (D11), error chips, links clickable. TESTS: base_table.rs kittest on a 2 000-row fixture: only visible rows are built (counter), header click flips sort, a group header shows its count, summary shows the Average; frame time of 40 visible rows < 4 ms (ignored perf). VERIFY: standing + snapshot PNG read and compared to the owner's screenshot. DO NOT: allocate per row outside the window.
  • 16. Properties popover, sort/group menus, filter builder. OWNS: crates/ui-egui/src/views/base_view/properties_popover.rs, .../sort_menu.rs, .../group_menu.rs, .../filter_builder.rs, .../filter_ops.rs. FILES TO OPEN: crates/ui-egui/src/widgets/context_menu.rs, crates/ui-egui/src/panels/search_panel.rs (filter field), spec.md D11. GOAL: the popovers of Views.md: property search + checkboxes (note/file/formula) + drag reorder + "New formula" (name + expression editor with live error); sort list with drag priority; group by + manual order with show/hide; filter builder with All/Any/None groups, nested groups, operators by type, value editor, "advanced" raw expression; every change calls base.view.set / base.write. TESTS: base_filters.rs kittest: add a filter row and the YAML gets year >= 2000; nested Any group; tick pages and order gains note.pages; reorder; the operator list depends on the property type. VERIFY: standing + cargo xtask i18n. DO NOT: edit YAML by string concatenation (use the model).
  • 17. Inline cell editing, multi-select, copy/paste, undo, New. Done: typed editors (table/edit.rs), grid keys, selection and TSV (table/grid.rs, selection.rs, tsv.rs), New row (table/new_row.rs), writes queued in state/writes.rs. Cmd+Z re-writes the previous value of the last written cell through base.row.set (or note.property_remove when the key did not exist): one undo step per cell, one buffer undo step in an open note. Gaps: dates use a validated text field (the calendar popup in properties_view is tied to its draft type), New does not pre-fill equality filters. OWNS: crates/ui-egui/src/views/base_view/edit.rs, .../selection.rs, .../new_row.rs. FILES TO OPEN: crates/ui-egui/src/views/properties_view/editors.rs, crates/ui-egui/src/shell/app/window/commands.rs (engine_command), Table view.md shortcuts. GOAL: D9: Enter edits, Tab/Shift-Tab, Backspace clears, Shift-click and Cmd-A select, Cmd-C/V as TSV, Cmd-Z undoes the last property change, checkbox toggles on Enter, New creates a note via note.create with equality filters pre-filled; errors show in the cell. TESTS: base_edit.rs kittest: edit a text cell -> the note file changes, body identical; paste a 2x2 block -> 4 sets; invalid date shows the error and nothing changes; Cmd-Z restores. VERIFY: standing + cargo xtask i18n. DO NOT: write files from the UI thread (engine commands only).

Phase 4 - Cards, List, embed, Map

  • 18. Cards and List views. OWNS: crates/ui-egui/src/views/base_view/cards.rs, .../list.rs, .../cover.rs. FILES TO OPEN: crates/ui-egui/src/views/folder_view.rs (a grid), crates/ui-egui/src/views/image_view/ (image loading and cache), Cards view.md, List view.md. GOAL: cards grid (size, image property: attachment link, URL (fetched never: show a placeholder + host), #hex colour block; fit; aspect ratio), list with bullets/numbers/none, indent properties, separator; both virtualised, both use the same toolbar. TESTS: base_cards.rs: image property ![[a.png]] resolves to an attachment, a hex value paints a colour block, list numbering, indent toggle. VERIFY: standing + snapshots. DO NOT: download remote images (policy: only the Maps tiles touch the network).
  • 19. Embeds: ```base and ![[x.base]], .base in search/quick switcher/Explorer. OWNS: crates/ui-egui/src/views/note_view/blocks/base_embed.rs, crates/ui-egui/src/views/note_view/blocks/embed.rs (dispatch lines), crates/ui-egui/src/views/base_view/embedded.rs, crates/ui-egui/src/panels/tree/ (icon only if needed). FILES TO OPEN: crates/ui-egui/src/views/note_view/blocks/embed.rs, the query-embed block (obsidian-basics 19; git grep "query" crates/ui-egui/src/views/note_view/blocks), crates/ui-egui/src/views/base_view/mod.rs. GOAL: D12; this = embedding note; depth guard 3; height from the view. TESTS: kittest note with a ```base block renders a table; ![[x.base#Reading list]] selects the view; self-embed shows the cycle chip. VERIFY: standing. DO NOT: add the Kanban placeholder logic elsewhere.
  • 20. The Map view and the Maps settings card. OWNS: crates/ui-egui/src/views/base_view/map/mod.rs, .../map/tiles.rs (worker + texture cache), .../map/markers.rs, .../map/popup.rs, .../map/grid.rs, crates/ui-egui/src/views/settings_view/categories/maps.rs, the core_plugins() row in settings_view/categories.rs (one tuple), crates/ui-egui/tests/base_map.rs. FILES TO OPEN: crates/bases/src/map/* (task 8), crates/engine/src/bases/tiles/mod.rs (task 12), crates/ui-egui/src/views/settings_view/categories/calendar.rs (a core plugin card), crates/ui-egui/src/views/graph_view.rs (pan/zoom input handling), spec.md D13. GOAL: pan/zoom with ease, tiles via the fetcher (never when disabled), grid fallback + "Offline" chip, clusters with counts, marker icons (Lucide names) and colours, popup card with order properties and Open note, right-click Copy coordinates, +/-, attribution link always drawn, first-enable notice dialog, Settings card (enable, use tiles, tile URL, cache size, Clear cache). With Maps off the view shows the placeholder. TESTS: base_map.rs kittest with a stub TileFetcher: markers cluster at low zoom and split at high zoom, popup shows properties, attribution present in every frame, Maps off -> zero fetch calls, stub failing -> grid + chip, 360 markers frame < 6 ms. VERIFY: standing + cargo xtask i18n + cargo xtask ci; update ROADMAP.md, docs/ARCHITECTURE.md, docs/CODE-MAP.md (new rows for bases, base_view). DO NOT: bypass the fetcher trait; hardcode the OSM URL outside the default setting.

Phase 5 - polish (separate rounds)

Kanban view; Dataview dataview block converter; a base in the sidebar (this = active file); the lazy property index (Q4); formula editor autocompletion; map: GPX/GeoJSON overlay, search box, vector tiles.