docs: fold review findings back into the plan
Corrections made during execution as reviews surfaced defects in the plan itself: duplicate raw-path claim handling and its one-pass orphaned filter, unescaped-pipe table split, RAW_EXTENSIONS widening, dark accent tracking the installed theme, the bare node --test command, and test counts.
This commit is contained in:
@@ -269,6 +269,31 @@ test("derivePipeline treats a page with no raw path as orphaned", () => {
|
||||
assert.equal(out.unprocessed.length, 1);
|
||||
});
|
||||
|
||||
test("derivePipeline routes a duplicate raw-path claim to orphaned", () => {
|
||||
const out = derivePipeline({
|
||||
rawFiles: [{ path: "raw/sources/a.md", name: "a.md", size: 10 }],
|
||||
sourcePages: [
|
||||
{ path: "wiki/sources/first.md", name: "first.md", rawPath: "raw/sources/a.md" },
|
||||
{ path: "wiki/sources/second.md", name: "second.md", rawPath: "raw/sources/a.md" },
|
||||
],
|
||||
});
|
||||
assert.equal(out.processed.length, 1);
|
||||
assert.equal(out.processed[0].page.name, "first.md");
|
||||
assert.equal(out.unprocessed.length, 0);
|
||||
assert.deepEqual(out.orphaned.map((p) => p.name), ["second.md"]);
|
||||
});
|
||||
|
||||
test("derivePipeline lists a page once when it is both a duplicate claim and missing its raw file", () => {
|
||||
const out = derivePipeline({
|
||||
rawFiles: [{ path: "raw/sources/other.md", name: "other.md", size: 10 }],
|
||||
sourcePages: [
|
||||
{ path: "wiki/sources/first.md", name: "first.md", rawPath: "raw/sources/gone.md" },
|
||||
{ path: "wiki/sources/second.md", name: "second.md", rawPath: "raw/sources/gone.md" },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(out.orphaned.map((p) => p.name), ["first.md", "second.md"]);
|
||||
});
|
||||
|
||||
test("derivePipeline sorts unprocessed by name and processed newest first", () => {
|
||||
const out = derivePipeline({
|
||||
rawFiles: [
|
||||
@@ -307,9 +332,16 @@ function extractRawPath(text) {
|
||||
}
|
||||
|
||||
function derivePipeline({ rawFiles, sourcePages }) {
|
||||
// First claim on a raw path wins. A later page claiming the same file is a
|
||||
// duplicate claim — real catalog drift — and joins `orphaned` rather than
|
||||
// being silently dropped. `orphaned` therefore means "source page not paired
|
||||
// with a raw file", whatever the reason.
|
||||
const claimed = new Map();
|
||||
const duplicates = [];
|
||||
for (const page of sourcePages) {
|
||||
if (page.rawPath) claimed.set(page.rawPath, page);
|
||||
if (!page.rawPath) continue;
|
||||
if (claimed.has(page.rawPath)) duplicates.push(page);
|
||||
else claimed.set(page.rawPath, page);
|
||||
}
|
||||
|
||||
const processed = [];
|
||||
@@ -320,8 +352,15 @@ function derivePipeline({ rawFiles, sourcePages }) {
|
||||
else unprocessed.push(file);
|
||||
}
|
||||
|
||||
// One pass over sourcePages, so a page appears in `orphaned` at most once no
|
||||
// matter how many of the three reasons apply to it. Concatenating a separate
|
||||
// duplicates array here would double-count a losing claimant whose shared raw
|
||||
// path is also missing from disk.
|
||||
const rawPaths = new Set(rawFiles.map((f) => f.path));
|
||||
const orphaned = sourcePages.filter((p) => !p.rawPath || !rawPaths.has(p.rawPath));
|
||||
const duplicateSet = new Set(duplicates);
|
||||
const orphaned = sourcePages.filter(
|
||||
(p) => duplicateSet.has(p) || !p.rawPath || !rawPaths.has(p.rawPath)
|
||||
);
|
||||
|
||||
unprocessed.sort((a, b) => a.name.localeCompare(b.name));
|
||||
processed.sort((a, b) => b.page.name.localeCompare(a.page.name));
|
||||
@@ -344,7 +383,7 @@ module.exports.__test__ = { parseConfig, extractRawPath, derivePipeline, STATION
|
||||
node --test test/pipeline.test.js
|
||||
```
|
||||
|
||||
Expected: PASS, 7 tests.
|
||||
Expected: PASS, 9 tests.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
@@ -416,12 +455,19 @@ Values are copied verbatim from `colors_and_type.css`. Scoped to `.webinar-dash`
|
||||
--wd-border: var(--wd-ink-700);
|
||||
--wd-border-strong: #b8b8b8;
|
||||
--wd-border-subtle: #161616;
|
||||
--wd-accent: #ff4a3d;
|
||||
--wd-accent-press: #ff6d62;
|
||||
--wd-accent-on: #0a0a0a;
|
||||
/* Accent tracks the installed theme at .obsidian/themes/tesanti/theme.css,
|
||||
whose dark section reads "Black canvas, same signal red": --accent-h/-s/-l
|
||||
are declared once at :root and never redeclared under .theme-dark, and only
|
||||
the hover state lifts (#c31c14 light, #ff4d43 dark). Match that exactly. */
|
||||
--wd-accent: var(--wd-red-500);
|
||||
--wd-accent-press: #ff4d43;
|
||||
--wd-accent-on: #ffffff;
|
||||
/* Status tokens are separate from the brand accent by design-system rule, and
|
||||
here they carry small text in a dense table. #e1261c on near-black is about
|
||||
3.8:1, under AA for small text, so these lift where the accent does not. */
|
||||
--wd-ok: #2fbf6a;
|
||||
--wd-warn: #e0a516;
|
||||
--wd-danger: #ff4a3d;
|
||||
--wd-danger: #ff5c50;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
@@ -503,6 +549,10 @@ Values are copied verbatim from `colors_and_type.css`. Scoped to `.webinar-dash`
|
||||
Insert into `main.js` above the plugin class. Icons are inline stroke paths — the Lucide CDN is unavailable offline and the design system forbids emoji.
|
||||
|
||||
```js
|
||||
// Source types CLAUDE.md documents for raw/sources. Anything else in that
|
||||
// folder is not a source and stays out of the queue.
|
||||
const RAW_EXTENSIONS = new Set(["md", "txt", "pdf"]);
|
||||
|
||||
const ICONS = {
|
||||
terminal: "M4 17l6-6-6-6M12 19h8",
|
||||
check: "M20 6 9 17l-5-5",
|
||||
@@ -608,8 +658,12 @@ class WebinarDashPlugin extends PluginBase {
|
||||
const rawDir = cfg.rawDir.replace(/\/+$/, "") + "/";
|
||||
const wikiDir = cfg.wikiSourceDir.replace(/\/+$/, "") + "/";
|
||||
|
||||
// CLAUDE.md documents raw/sources as holding "markdown/text/pdf exports".
|
||||
// Allow-listing only "md" would hide the others from the queue with no
|
||||
// warning — the same silent-invisibility failure this dashboard exists to
|
||||
// remove. Wiki source pages below stay markdown-only; those really are .md.
|
||||
const rawFiles = all
|
||||
.filter((f) => f.path.startsWith(rawDir) && f.extension === "md")
|
||||
.filter((f) => f.path.startsWith(rawDir) && RAW_EXTENSIONS.has(f.extension))
|
||||
.map((f) => ({ path: f.path, name: f.name, size: f.stat.size }));
|
||||
|
||||
const pageFiles = all.filter((f) => f.path.startsWith(wikiDir) && f.extension === "md");
|
||||
@@ -637,10 +691,14 @@ Expected, against the vault's current state:
|
||||
|
||||
```bash
|
||||
cd "D:/Projects/Notes/Webinar/Webinar/.obsidian/plugins/webinar-dash"
|
||||
node --test test/
|
||||
node --test
|
||||
```
|
||||
|
||||
Expected: PASS, 6 tests.
|
||||
Bare `node --test` auto-discovers the test files. Do not pass `test/` as an
|
||||
argument — Node 24 resolves a bare directory path as a module and fails with
|
||||
`MODULE_NOT_FOUND` before running anything.
|
||||
|
||||
Expected: PASS, 9 tests.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
@@ -820,7 +878,11 @@ function parseCoverageTable(markdown) {
|
||||
if (!inTable) return;
|
||||
|
||||
const body = t.endsWith("|") ? t.slice(1, -1) : t.slice(1);
|
||||
const cells = body.split("|").map((c) => c.trim());
|
||||
// Split on unescaped pipes only. Obsidian escapes the pipe of a piped
|
||||
// wikilink inside a table cell as `\|`, and a plain split("|") tears
|
||||
// `[[harness\|alias]]` into two cells, shifting every later column left —
|
||||
// the status cell then reads "The harness]]" and the row is rejected.
|
||||
const cells = body.split(/(?<!\\)\|/).map((c) => c.trim());
|
||||
|
||||
if (cells.length < 3) {
|
||||
errors.push({ line: i + 1, text: t, reason: "expected at least 3 columns" });
|
||||
@@ -1580,10 +1642,14 @@ This writes to the vault unsupervised. Everything is committed, so `git diff HEA
|
||||
|
||||
```bash
|
||||
cd "D:/Projects/Notes/Webinar/Webinar/.obsidian/plugins/webinar-dash"
|
||||
node --test test/
|
||||
node --test
|
||||
```
|
||||
|
||||
Expected: PASS, 24 tests across three files.
|
||||
Bare `node --test` auto-discovers the test files. Do not pass `test/` as an
|
||||
argument — Node 24 resolves a bare directory path as a module and fails with
|
||||
`MODULE_NOT_FOUND` before running anything.
|
||||
|
||||
Expected: PASS, 26 tests across three files.
|
||||
|
||||
- [ ] **Step 11: Commit**
|
||||
|
||||
@@ -1616,4 +1682,4 @@ The spec's "v1 does not write the coverage file from the plugin" is honored —
|
||||
|
||||
**Type consistency.** `rawFiles` items are `{path, name, size}` in Tasks 2 and 3. `sourcePages` items are `{path, name, rawPath}` in both. Coverage rows are `{concept, status, stations, pinned, line}` in Tasks 4, 5, and 6. `onIngest(file, rowEl)` is declared in Task 3 and implemented with the same signature in Task 7. `addIcon(parent, name)` is defined in Task 3 and reused in Task 6. `STATIONS` is defined in Task 1 and consumed by `groupByStation` in Task 4.
|
||||
|
||||
**Test count.** Task 2 adds 7, Task 4 adds 11, Task 6 adds 2, Task 7 adds 4 — 24 total, matching Step 10 of Task 7.
|
||||
**Test count.** Task 2 adds 9, Task 4 adds 11, Task 6 adds 2, Task 7 adds 4 — 26 total, matching Step 10 of Task 7.
|
||||
|
||||
Reference in New Issue
Block a user