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>
*/
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import * as nodeFs from "node:fs";
import { basename, join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { itemDescription, toRss } from "../src/rss.ts";
@@ -77,50 +77,80 @@ export function compare(ours: Item[], theirs: Exported[]): { complete: number; r
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
* 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).
*
* The program's edge: every power it uses arrives as a default argument
* (ocap discipline), so a test can pass others.
*
* Ends by reporting an exit code (2: usage) through `exit`.
* The program's edge (ocap discipline, transitional form): it takes the
* standard authorities as default arguments and attenuates them here —
* the database to {@link cacheItems}, the output directory to
* {@link outDir} — before anything else uses them. Ends by reporting an
* exit code (2: usage) through `exit`.
*/
export function main(
argv = process.argv.slice(2),
{
readText = (path: string) => readFileSync(path, "utf8"),
writeText = (path: string, text: string) => writeFileSync(path, text),
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),
fs = nodeFs,
connect = (path: string) => new DatabaseSync(path, { readOnly: true }),
now = () => new Date(),
log = (x: unknown) => console.log(x),
warn = (x: unknown) => console.error(x),
stdout = process.stdout,
stderr = process.stderr,
exit = (code: number) => {
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 {
const [dbPath, exportPath, outDir] = argv;
if (!dbPath || !exportPath || !outDir) {
warn("usage: node scripts/compare-export.ts <cache.sqlite> <export.xml> <out-dir>");
const [dbPath, exportPath, outPath] = argv;
if (!dbPath || !exportPath || !outPath) {
stderr.write("usage: node scripts/compare-export.ts <cache.sqlite> <export.xml> <out-dir>\n");
return exit(2);
}
const ours = readItems(dbPath);
writeText(join(outDir, "from-cache.rss"), toRss(ours, { title: "from the cache", pubDate: now().toUTCString() }));
const theirs = parseExport(readText(exportPath));
const cache = cacheItems(connect(dbPath));
const out = outDir(fs, outPath);
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);
writeText(join(outDir, "report.json"), JSON.stringify(report, null, 1));
log({
out.write("report.json", JSON.stringify(report, null, 1));
const counts = {
export: theirs.length,
cache: ours.length,
complete,
...Object.fromEntries(Object.entries(report).map(([k, v]) => [k, v.length])),
});
};
stdout.write(`${JSON.stringify(counts, null, 1)}\n`);
exit(0);
}