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:
1 parent
d145841a2e
commit
dfd9a3d1ef
1 file changed
+54
-24
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user