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>withNode { 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 ofBases syntax.mdand the Layouts pages (copied intocrates/bases/tests/fixtures/*.yaml) parses andemit(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
.basemodel: 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.mdD3. 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 inextraat 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: kanbanand an unknown view key survive; limits (100 views, 200 formulas, 4 KiB expression) refused;limit: -1anddirection: SIDEWAYSare errors; missingviewsyields 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.mdD5,Bases syntax.mdoperators 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 ofnote.x,file.x,formula.x,thisreferences, 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.mdD5 (types, ordering, equality),crates/templates/src/date.rs(civil-date algorithms to copy, NOT to depend on). GOAL:Valueenum of D5;Datewithms,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 (takenow_msas 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.mdD1, D4, D6. GOAL:trait NoteSource,RowRef(D1),Env { now_ms, seed, this },eval(&Expr, &Env, &RowRef, &Formulas) -> Value; operators; lazyif; formula references with cycle detection (FormulaCycleonce); 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 areValue::Error(String), never panic. TESTS:tests/eval_basic.rswith aVec<TestRow>source: one table-driven test per function (normal, null, wrong type), theppuandformatted_priceexamples, cycle, division by zero,iflaziness (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: listcontains containsAll containsAny filter flat join map reduce reverse slice sort unique length isEmpty(lambda variablesvalue index acc); dateyear..millisecond format time relative date isEmpty(format tokensYYYY MM DD HH mm ssplus the Moment subset ofnotescraft-templates::formatsemantics, re-implemented); linkasFile linksTo; fileasLink hasLink hasProperty hasTag inFolderand thefile.*fields throughRowRef; regexmatches, stringreplacewith a regex and$1. TESTS:tests/eval_lists.rs,eval_dates.rs,eval_files.rs: each function per the docs;hasTagmatches nested tags;inFoldermatches descendants;relativegives "3 days ago";list(type)[0].asFile().properties.iconwith a test source; a pathological regex ((a+)+$) is stopped by the regex crate's step limit. VERIFY: standing. DO NOT: toucheval/mod.rsbeyond 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.mdD7. GOAL:run(&Base, view, &dyn NoteSource, &Env) -> QueryResultwithRowIds, columns (orderor defaultsfile.name), multi-key stable sort evaluating only sort keys,groupBy+groupOrder(null key),limit, default summaries of D6 and custom summaries overvalues;QueryResult::cell(i, col)lazy;errorscounted; 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;groupOrderhides groups;nullingroupOrder; custom summaryvalues.mean().round(3); 15 000 synthetic rows, 1 filter + 1 sort in < 60 ms (#[ignore]perf test inperf/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; cloneValues 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.mdD13,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)-> enumColor::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,fitof two points,parse_coordinatesfor"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.mdD2. 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.
NoteSourceover the index andbase.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 incrates/engine/src/lib.rs, oneregister_bases(reg)call incrates/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; inlineyamlorpath;this;offset/limitslicing (default 200, max 2000); a result cache keyed by index generation (D7). TESTS:bases_query.rsdrives the registry on aTempVaultof 50 notes: golden JSON for filter/sort/group/summary, inline yaml,this, invalid yaml givesinvalid_paramswith line, a missing base givesnot_found. VERIFY: standing fornotescraft-engine+cargo test -p notescraft-engine --test bases_query. DO NOT: editSessionbeyond 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(hownote.property_setis wired),crates/engine/src/review/store.rs(a write throughVault::write). GOAL: D9 and D10;base.row.setreusesproperties::set; rejectsfile.*/formula.*(exceptfile.name->note.rename); lossless view edits via task 2'sto_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.createseeds a tag filter. VERIFY: standing. DO NOT: write outside the vault; skipcraft-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(blockingureqclient, 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.mdD13. GOAL:trait TileFetcher;HttpTileFetcher(ureq, UANotesCraft/<ver> (+repo), https only, 5 s, 256 KiB,image/pngcheck, no Referer); token bucket (burst 8, then 1 req/s) and 2 concurrent; disk cache.notescraft/cache/tiles/<host>/<z>/<x>/<y>.pngwith a 7-day floor, LRU eviction atmaps.cache_mb(default 200);maps.status,maps.clear_cachecommands; settingsmaps.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; anhttp://or template without{z}refused. VERIFY: standing. DO NOT: contact any real server in tests; prefetch outside the viewport; add a dependency (ureqis approved). - 13. Sync ignore for
.notescraft/cacheand the property rows in the MCPbase_querytool. 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 toolbase_query {path|yaml, view, limit}returnsbase.queryJSON. TESTS: a path under the cache is skipped by the ledger; the MCP tool lists intools/listand 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 armSome("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.svgif missing) andcrates/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: openingx.baseshows the toolbar ofViews.md, the view switcher with Add/Duplicate/Rename/Delete (enginebase.view.*), a raw-YAML fallback card on parse errors, the status line (N results,E errors); the query runs throughengine_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 eguiButtons 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_rowsvirtualisation, row heights fromrowHeight, sticky header, header click sorts (writessortviabase.view.set), drag-resize writescolumnSize, drag-reorder writesorder, 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.rskittest 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.mdD11. GOAL: the popovers ofViews.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 callsbase.view.set/base.write. TESTS:base_filters.rskittest: add a filter row and the YAML getsyear >= 2000; nested Any group; tickpagesandordergainsnote.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 instate/writes.rs. Cmd+Z re-writes the previous value of the last written cell throughbase.row.set(ornote.property_removewhen 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 inproperties_viewis 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.mdshortcuts. 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,Newcreates a note vianote.createwith equality filters pre-filled; errors show in the cell. TESTS:base_edit.rskittest: 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),#hexcolour 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:
```baseand![[x.base]],.basein 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```baseblock 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, thecore_plugins()row insettings_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.mdD13. 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 withorderproperties 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.rskittest with a stubTileFetcher: 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; updateROADMAP.md,docs/ARCHITECTURE.md,docs/CODE-MAP.md(new rows forbases,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.