Skip to content

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’s LIST filters but flattens; its TASK nests but does not really filter (View 4 matched only the top-level roots and dragged every descendant along, including three thisWeek items, 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.

  • 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]
    • Agree the revenue split [abc:: A] [status:: todo] [focus:: thisWeek]
  • 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 match
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 — 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 check

Note 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 Talbot
  1. Does View 6 show exactly those four items, nested, with the three thisWeek items absent?
  2. Does View 7 show the single deep match with its two ancestors in italics?
  3. 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.lists exposing .line, .parent, and inline fields as properties, and on dv.paragraph rendering 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.