Proof D — a filtered view that keeps the tree, via dataviewjs
Section titled “Proof D — a filtered view that keeps the tree, via dataviewjs”Round 4 of
ecco-replacement-research. The last honest state: Dataview’sLISTfilters but flattens; itsTASKnests but does not really filter (View 4 matched only the top-level roots and dragged every descendant along, including threethisWeekitems, and View 5 returned nothing because TASK queries ignore fields on subtasks).This is the different axis: walk the outline in JavaScript, keep every item that matches plus its ancestor chain, and render the result as a real tree. If this works, the vault reproduces an Ecco notepad exactly — filtered and nested.
Ancestors shown for context are italicised. Only the non-italic items actually match the filter.
The outline — four levels
Section titled “The outline — four levels”- Chilton partnership [abc:: A] [status:: WIP] [focus:: today]
- Draft the follow-up email [abc:: A] [status:: WIP] [focus:: today]
- Confirm the send date with Talbot [abc:: A] [status:: waiting] [focus:: today]
- Check his Friday calendar block [abc:: B] [status:: todo] [focus:: thisWeek]
- Attach deck v2 [abc:: B] [status:: todo] [focus:: thisWeek]
- Confirm the send date with Talbot [abc:: A] [status:: waiting] [focus:: today]
- Agree the revenue split [abc:: A] [status:: todo] [focus:: thisWeek]
- Draft the follow-up email [abc:: A] [status:: WIP] [focus:: today]
- Rate scanner — daily ops check [abc:: A] [status:: recurring] [focus:: today]
- Refresh Filter+Focus (4.5 months stale) [abc:: B] [status:: todo] [focus:: thisWeek]
- Buy new label printer [abc:: C] [status:: todo] [action:: buy] [focus:: someday]
View 6 — “Today only”, filtered and nested
Section titled “View 6 — “Today only”, filtered and nested”const FIELD = "focus", VALUE = "today";
const items = dv.current().file.lists.values;const byLine = new Map(items.map(i => [i.line, i]));const kids = new Map();for (const it of items) { const p = byLine.has(it.parent) ? it.parent : null; if (!kids.has(p)) kids.set(p, []); kids.get(p).push(it);}
const matches = it => String(it[FIELD] ?? "") === VALUE;
// keep every match, plus every ancestor of a matchconst keep = new Set();for (const it of items) { if (!matches(it)) continue; keep.add(it.line); let p = it.parent; while (byLine.has(p)) { keep.add(p); p = byLine.get(p).parent; }}
const clean = t => t.replace(/\s*\[\w+::[^\]]*\]/g, "").trim();
const out = [];const walk = (it, depth) => { if (!keep.has(it.line)) return; const label = clean(it.text); out.push(" ".repeat(depth) + "- " + (matches(it) ? label : "*" + label + "*")); for (const c of (kids.get(it.line) ?? [])) walk(c, depth + 1);};for (const root of (kids.get(null) ?? [])) walk(root, 0);
dv.paragraph(out.length ? out.join("\n") : "no matches");Expected — 4 items, nested three deep, none italic (every match here happens to have matching ancestors):
- Chilton partnership - Draft the follow-up email - Confirm the send date with Talbot- Rate scanner — daily ops checkNote what is absent: Check his Friday calendar block, Attach deck v2, Agree the revenue split. Those leaked into the TASK version. Here they must not appear.
View 7 — “Waiting”, the harsh test: one deep match, ancestors for context
Section titled “View 7 — “Waiting”, the harsh test: one deep match, ancestors for context”status = waiting matches exactly one item, three levels down — the case where the TASK query returned nothing at all.
const FIELD = "status", VALUE = "waiting";
const items = dv.current().file.lists.values;const byLine = new Map(items.map(i => [i.line, i]));const kids = new Map();for (const it of items) { const p = byLine.has(it.parent) ? it.parent : null; if (!kids.has(p)) kids.set(p, []); kids.get(p).push(it);}
const matches = it => String(it[FIELD] ?? "") === VALUE;
const keep = new Set();for (const it of items) { if (!matches(it)) continue; keep.add(it.line); let p = it.parent; while (byLine.has(p)) { keep.add(p); p = byLine.get(p).parent; }}
const clean = t => t.replace(/\s*\[\w+::[^\]]*\]/g, "").trim();
const out = [];const walk = (it, depth) => { if (!keep.has(it.line)) return; const label = clean(it.text); out.push(" ".repeat(depth) + "- " + (matches(it) ? label : "*" + label + "*")); for (const c of (kids.get(it.line) ?? [])) walk(c, depth + 1);};for (const root of (kids.get(null) ?? [])) walk(root, 0);
dv.paragraph(out.length ? out.join("\n") : "no matches");Expected — the match, with two italic ancestors above it:
- *Chilton partnership* - *Draft the follow-up email* - Confirm the send date with TalbotRender check for Talbot
Section titled “Render check for Talbot”- Does View 6 show exactly those four items, nested, with the three
thisWeekitems absent? - Does View 7 show the single deep match with its two ancestors in italics?
- If either block shows a JavaScript error instead of a list, paste the error text — the algorithm is verified but the Dataview JS API surface is not.
The JS API caveats, stated up front: this relies on
dv.current().file.listsexposing.line,.parent, and inline fields as properties, and ondv.paragraphrendering a markdown list. The tree-walking logic itself was tested outside Obsidian against the same ten items and produced exactly the two expected outputs above. If it fails here, it fails at the API boundary, not in the algorithm.