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);
|
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", () => {
|
test("derivePipeline sorts unprocessed by name and processed newest first", () => {
|
||||||
const out = derivePipeline({
|
const out = derivePipeline({
|
||||||
rawFiles: [
|
rawFiles: [
|
||||||
@@ -307,9 +332,16 @@ function extractRawPath(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function derivePipeline({ rawFiles, sourcePages }) {
|
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 claimed = new Map();
|
||||||
|
const duplicates = [];
|
||||||
for (const page of sourcePages) {
|
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 = [];
|
const processed = [];
|
||||||
@@ -320,8 +352,15 @@ function derivePipeline({ rawFiles, sourcePages }) {
|
|||||||
else unprocessed.push(file);
|
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 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));
|
unprocessed.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
processed.sort((a, b) => b.page.name.localeCompare(a.page.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
|
node --test test/pipeline.test.js
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: PASS, 7 tests.
|
Expected: PASS, 9 tests.
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
- [ ] **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: var(--wd-ink-700);
|
||||||
--wd-border-strong: #b8b8b8;
|
--wd-border-strong: #b8b8b8;
|
||||||
--wd-border-subtle: #161616;
|
--wd-border-subtle: #161616;
|
||||||
--wd-accent: #ff4a3d;
|
/* Accent tracks the installed theme at .obsidian/themes/tesanti/theme.css,
|
||||||
--wd-accent-press: #ff6d62;
|
whose dark section reads "Black canvas, same signal red": --accent-h/-s/-l
|
||||||
--wd-accent-on: #0a0a0a;
|
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-ok: #2fbf6a;
|
||||||
--wd-warn: #e0a516;
|
--wd-warn: #e0a516;
|
||||||
--wd-danger: #ff4a3d;
|
--wd-danger: #ff5c50;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 820px) {
|
@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.
|
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
|
```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 = {
|
const ICONS = {
|
||||||
terminal: "M4 17l6-6-6-6M12 19h8",
|
terminal: "M4 17l6-6-6-6M12 19h8",
|
||||||
check: "M20 6 9 17l-5-5",
|
check: "M20 6 9 17l-5-5",
|
||||||
@@ -608,8 +658,12 @@ class WebinarDashPlugin extends PluginBase {
|
|||||||
const rawDir = cfg.rawDir.replace(/\/+$/, "") + "/";
|
const rawDir = cfg.rawDir.replace(/\/+$/, "") + "/";
|
||||||
const wikiDir = cfg.wikiSourceDir.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
|
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 }));
|
.map((f) => ({ path: f.path, name: f.name, size: f.stat.size }));
|
||||||
|
|
||||||
const pageFiles = all.filter((f) => f.path.startsWith(wikiDir) && f.extension === "md");
|
const pageFiles = all.filter((f) => f.path.startsWith(wikiDir) && f.extension === "md");
|
||||||
@@ -637,10 +691,14 @@ Expected, against the vault's current state:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd "D:/Projects/Notes/Webinar/Webinar/.obsidian/plugins/webinar-dash"
|
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**
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
@@ -820,7 +878,11 @@ function parseCoverageTable(markdown) {
|
|||||||
if (!inTable) return;
|
if (!inTable) return;
|
||||||
|
|
||||||
const body = t.endsWith("|") ? t.slice(1, -1) : t.slice(1);
|
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) {
|
if (cells.length < 3) {
|
||||||
errors.push({ line: i + 1, text: t, reason: "expected at least 3 columns" });
|
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
|
```bash
|
||||||
cd "D:/Projects/Notes/Webinar/Webinar/.obsidian/plugins/webinar-dash"
|
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**
|
- [ ] **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.
|
**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