fixup! diigo-cache: RSS in diigo's export shape, from cached items

compare-export.ts per the ocap style guide (dckc/awesome-ocap,
style-guide/): main takes standard authorities under standard names as
default arguments — argv, fs, connect, now, stdout, stderr, exit (the
transitional form of _script_io) — and attenuates them itself:
connect(dbPath) to a read-only cacheItems, the output directory to an
outDir that writes only plain file names inside it. Counts go to
stdout (program output), usage to stderr.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-07 08:36:57 -05:00
1 parent d145841a2e
commit dfd9a3d1ef
1 file changed
+54 -24
+54 -24
View File
@@ -5,8 +5,8 @@
* *
* node scripts/compare-export.ts <cache.sqlite> <export.xml> <out-dir> * node scripts/compare-export.ts <cache.sqlite> <export.xml> <out-dir>
*/ */
import { readFileSync, writeFileSync } from "node:fs"; import * as nodeFs from "node:fs";
import { join } from "node:path"; import { basename, join } from "node:path";
import { DatabaseSync } from "node:sqlite"; import { DatabaseSync } from "node:sqlite";
import { itemDescription, toRss } from "../src/rss.ts"; import { itemDescription, toRss } from "../src/rss.ts";
@@ -77,50 +77,80 @@ export function compare(ours: Item[], theirs: Exported[]): { complete: number; r
return { complete, report }; return { complete, report };
} }
/** read-only access to one cache's items: attenuates a database */
export function cacheItems(db: { prepare(sql: string): { all(): unknown[] } }) {
return {
/** every item, newest update first */
all: (): Item[] =>
(db.prepare("select json from items order by updated desc").all() as { json: string }[]).map(
(r) => JSON.parse(r.json) as Item,
),
};
}
/** write access to files directly inside one directory: attenuates fs */
export function outDir(fs: Pick<typeof nodeFs, "writeFileSync">, dir: string) {
return {
write(name: string, text: string): void {
if (basename(name) !== name) throw new Error(`not a plain file name: ${name}`);
fs.writeFileSync(join(dir, name), text);
},
};
}
/** /**
* Generates RSS from a cache's items (src/rss.ts) and compares it with a * Generates RSS from a cache's items (src/rss.ts) and compares it with a
* diigo RSS export: title, tags, description, highlights + sticky notes. * diigo RSS export: title, tags, description, highlights + sticky notes.
* Prints counts only; writes <out-dir>/from-cache.rss and * Prints counts to stdout; writes <out-dir>/from-cache.rss and
* <out-dir>/report.json (mismatched links, by category). * <out-dir>/report.json (mismatched links, by category).
* *
* The program's edge: every power it uses arrives as a default argument * The program's edge (ocap discipline, transitional form): it takes the
* (ocap discipline), so a test can pass others. * standard authorities as default arguments and attenuates them here —
* * the database to {@link cacheItems}, the output directory to
* Ends by reporting an exit code (2: usage) through `exit`. * {@link outDir} — before anything else uses them. Ends by reporting an
* exit code (2: usage) through `exit`.
*/ */
export function main( export function main(
argv = process.argv.slice(2), argv = process.argv.slice(2),
{ {
readText = (path: string) => readFileSync(path, "utf8"), fs = nodeFs,
writeText = (path: string, text: string) => writeFileSync(path, text), connect = (path: string) => new DatabaseSync(path, { readOnly: true }),
readItems = (dbPath: string): Item[] =>
(new DatabaseSync(dbPath, { readOnly: true }).prepare("select json from items order by updated desc").all() as {
json: string;
}[]).map((r) => JSON.parse(r.json) as Item),
now = () => new Date(), now = () => new Date(),
log = (x: unknown) => console.log(x), stdout = process.stdout,
warn = (x: unknown) => console.error(x), stderr = process.stderr,
exit = (code: number) => { exit = (code: number) => {
process.exitCode = code; process.exitCode = code;
}, },
}: {
fs?: Pick<typeof nodeFs, "readFileSync" | "writeFileSync">;
connect?: (path: string) => { prepare(sql: string): { all(): unknown[] } };
now?: () => Date;
stdout?: { write(text: string): unknown };
stderr?: { write(text: string): unknown };
exit?: (code: number) => void;
} = {}, } = {},
): void { ): void {
const [dbPath, exportPath, outDir] = argv; const [dbPath, exportPath, outPath] = argv;
if (!dbPath || !exportPath || !outDir) { if (!dbPath || !exportPath || !outPath) {
warn("usage: node scripts/compare-export.ts <cache.sqlite> <export.xml> <out-dir>"); stderr.write("usage: node scripts/compare-export.ts <cache.sqlite> <export.xml> <out-dir>\n");
return exit(2); return exit(2);
} }
const ours = readItems(dbPath); const cache = cacheItems(connect(dbPath));
writeText(join(outDir, "from-cache.rss"), toRss(ours, { title: "from the cache", pubDate: now().toUTCString() })); const out = outDir(fs, outPath);
const theirs = parseExport(readText(exportPath)); const exportXml = fs.readFileSync(exportPath, "utf8");
const ours = cache.all();
out.write("from-cache.rss", toRss(ours, { title: "from the cache", pubDate: now().toUTCString() }));
const theirs = parseExport(exportXml);
const { complete, report } = compare(ours, theirs); const { complete, report } = compare(ours, theirs);
writeText(join(outDir, "report.json"), JSON.stringify(report, null, 1)); out.write("report.json", JSON.stringify(report, null, 1));
log({ const counts = {
export: theirs.length, export: theirs.length,
cache: ours.length, cache: ours.length,
complete, complete,
...Object.fromEntries(Object.entries(report).map(([k, v]) => [k, v.length])), ...Object.fromEntries(Object.entries(report).map(([k, v]) => [k, v.length])),
}); };
stdout.write(`${JSON.stringify(counts, null, 1)}\n`);
exit(0); exit(0);
} }