compare-export.ts: the usage exit status is a named constant,
BAD_USAGE = 2, whose JSDoc cites the convention (Bash Reference Manual
§3.7.5); the manual just says "Exits 2 on bad usage".
Generated-by: Claude Opus 5.5
compare-export.ts: exit 2 on bad usage is an external convention, so its
@file cites the source (Bash Reference Manual §3.7.5, Exit Status).
Generated-by: Claude Opus 5.5
compare-export.ts: as a program script, its @file is its manual
(purpose, usage, outputs, exit code), per docs-files-api's exception;
main's JSDoc keeps only the edge and attenuation.
Generated-by: Claude Opus 5.5
Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.
Generated-by: Claude Opus 5.5
Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.
Generated-by: Claude Opus 5.5
Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.
Generated-by: Claude Opus 5.5
Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.
Generated-by: Claude Opus 5.5
Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.
Generated-by: Claude Opus 5.5
Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.
Generated-by: Claude Opus 5.5
Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.
Generated-by: Claude Opus 5.5
Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.
Generated-by: Claude Opus 5.5
Docs per the docs-files-api skill: a 1–2 line @file; what a declaration is or does in JSDoc on it, not in // comments or the file header.
Generated-by: Claude Opus 5.5
/public/<username> reads through the cache's viewer() facet
(PageViewer: stored pages, knownTotal, fill progress — the reader
alone), so it can't fetch from diigo or write storage.
Behavior change, agreed beforehand: an unfetched page no longer fills
itself on a miss; it says "not fetched yet" (JSON: stored: false), and
the owner fills via the webkey page. The HIT/MISS line becomes "stored
<when>". Strangers can no longer make the cache spend the owner's diigo
session; the README's deployment concern shrinks to /api.
Tests: viewing an unfetched page makes no webapp call; the page appears
once stored; fill progress shows after one fill step.
Generated-by: Claude Opus 5.5
No SQL text in the cache logic any more. Statements live in two files,
parsed by sqlStatements (`-- name:` sections, as in finquick's
brcal/gcbook.js) and imported as text (wrangler Text rule for *.sql):
- cache-read.sql: selects only, checked when the module loads
- cache-write.sql: schema, the v2 renumbering, etch, drops
store.ts turns them into two frozen facets with domain methods:
makeReader(sql) runs only the read statements (page, knownTotal,
deepestPageSince, pagesThrough); makeWriter(storage) only the write
ones (migrate, etch, dropPagesThrough), each change one transaction.
The cache migrates in its constructor under blockConcurrencyWhile, as
Cloudflare recommends, so readers never meet missing tables; the
migration test plants v1 storage and evicts to get a fresh activation.
A viewer() facet (PageViewer: stored pages and fill progress, from the
reader alone) is added for the public page.
Pure refactor: stored state and behavior unchanged.
Generated-by: Claude Opus 5.5
capsite-notes gains a map of what a person can do in diigo-cache, where,
by which transport (HTTP route or Cap'n Web RPC), and with what
authority — a flowchart of the pages plus a table — marking the
misalignments: publish and dump are cache-key actions on HTTP forms, not
the RPC facet; the public page can write (a miss fills from diigo).
Input to aligning the admin UX with the RPC interface.
Generated-by: Claude Opus 5.5
1c-vi: the cache proven complete against diigo's RSS export (and the
0-based page_num found on the way); 1e: the password-manager login;
1f: "invalidate the last N days" as today's stopgap; how-to-run: the
webkey page and compare-export.
Generated-by: Claude Opus 5.5
From the "Capsite checklist revisited" session: the root goal becomes
madmode publishing a weekly roundup of diigo bookmarks; goal 2 reads the
public cache (no credential on madmode's side); new 1f (stale head
pages must refresh); goals 3–7 restated around the weekly page,
deployment, cookie expiry, and bib sync.
Generated-by: Claude Code
compare-export.ts does its file I/O asynchronously (node:fs/promises;
main is async), so no sync-I/O precedent. The one exception says why:
node:sqlite has only DatabaseSync.
Generated-by: Claude Opus 5.5
compare-export.ts: Object.fromEntries(Object.entries(…).map(…)) as
objectMap(original, mapFn), with @endo/common's signature.
Generated-by: Claude Opus 5.5
compare-export.ts runs main only under import.meta.main (Node's
`if __name__ == "__main__"`), so importing it — say, from a test —
runs nothing.
Generated-by: Claude Opus 5.5
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
compare-export.ts under ocap discipline: parseExport and compare are
pure; main is the program's edge and takes every power it uses (argv,
file reads and writes, the sqlite read, the clock, console, the exit
code) as default arguments. Nothing ambient at module level; the last
line just calls main().
Generated-by: Claude Opus 5.5
scripts/tsconfig.json gives scripts/ node's types (@types/node, a new
devDependency) and allows the .ts import specifiers node's type
stripping needs; the worker's tsconfig still keeps node's types out.
pnpm lint runs it.
Generated-by: Claude Opus 5.5
A diigo note item's body (note_content) renders first, as a Highlight:
line, as the export does: the 6 "export has notes, cache none" items
from the comparison were notes whose bodies the cache had all along.
Generated-by: Claude Opus 5.5
invalidateRecent(days) drops the cached pages holding anything diigo
updated within the last N days (on the cache's injected clock), and
every page before them, so the next read or fill refetches them; item
rows stay. Exposed on the fill capability (private and public) and on
the webkey page as "invalidate the last N days" per cache.
A user-requested refetch, not a change in what stored state means.
Generated-by: Claude Opus 5.5
Comparing a cache with diigo's RSS export: the size-100 walk had missed
exactly the 100 newest bookmarks (everything updated since 2026-09-16).
diigo's page_num is 0-based, and we sent our 1-based page number
unchanged, so "page 1" was really page 2 and the newest page was never
fetched. diigo's total (16744) was right; the "total-overstated"
reading was a misdiagnosis.
- page() asks for page_num = n - 1
- BREAKING for stored state, agreed beforehand, migrated in place:
schema v2 renumbers every stored page +1 (pages and page_members, via
negation so no number collides), once, recorded as kv "schema" = 2;
nothing is refetched, page 1 just becomes a miss
- fill jobs record their numbering; a job saved without it fails ("page
numbering changed …; start the fill again") instead of resuming
- the fake pages from 0 like diigo (7 tests failed until the fix);
tests feed v1-shaped storage and a v1-numbered job to the new code
- the next-page peek guard stays; its comments no longer claim diigo
overstates its total
- the fixed test clock moves to 2099: pinned to "today" its alarms came
due in real time and fired on their own
Generated-by: Claude Opus 5.5
src/rss.ts renders items as RSS 2.0 in the shape of diigo's RSS export:
title, link, and a description packing Tags:…;Description:…;
Annotations and Sticky notes:… (Highlight:/Sticky notes: lines). Unlike
the export it is well-formed (characters XML forbids are dropped; the
export carries some raw) and keeps whole titles (the export cuts at
255). Pure: dates come from the caller.
scripts/compare-export.ts (a program script: node, node:sqlite) loads a
cache's items, writes the RSS, and compares it with an export, keyed on
link, reporting counts by category.
Generated-by: Claude Opus 5.5
Seen live: "filled: 168 of 1 pages". diigo's total overstated by 100
(16744 vs 16644), so the walk went on to page 168, which diigo answered
empty with a total ≤ 100; the fill recomputed its page count from that,
ceil(total/100) = 1.
Now a page whose own total says the data ended before it ends the walk,
with the count set to the last page with items ("167 of 167"). Judged
by total, not by items, since the public cache may keep none of a page
that isn't the end. The known-pages estimate ignores zero totals. The
fake answers past-the-end pages as diigo does (no items, total 0).
Generated-by: Claude Opus 5.5
page() ran the 500 ms pacing gate before the cache check, so every hit
waited too: a fill rerun skipping 166 cached pages took ~80 s and
looked like a start from scratch. The gate now sits after the hit
check (re-checking storage once through it, in case a request ahead
had just etched the page).
Regression test: the fixed-clock test cache records its sleeps; a miss
sleeps once, three hits add none (failed before: 4 sleeps).
Generated-by: Claude Opus 5.5
The real fill failed at "diigo returned 44 items for page 167 of 168":
diigo's `total` overstates what load_user_items returns, so the true
last page looked short and the guard took it for a capped page size.
A short page that isn't the last by `total` now fetches the next page
(paced like any diigo call): empty means the data really ends there —
accepted, with a "total-overstated" notice logging total vs actual;
anything else means items would be skipped — refused, the message now
giving both counts. The fake gains an account whose total claims 60
more than it has; a fill on it runs to done.
Generated-by: Claude Opus 5.5
Asked for 128, diigo returned 100 (the short-page guard caught it on the
first real fill), so PAGE_SIZE = 100: ~168 pages for 16.7k bookmarks.
The same run showed a fill job outliving a page-size change: progress
counted in 24s ("page 25 of 698") resumed as if counted in 128s. Stored
state reinterpreted, so: FillStatus records pageSize (additive; jobs
saved without it counted in 24), and a job whose size differs from
PAGE_SIZE fails with "page size changed …; start the fill again"
instead of resuming; starting again replaces it. Tested with the old
shape in storage.
Generated-by: Claude Opus 5.5
PAGE_SIZE = 128 (was 24), one constant for the fill job, public pages,
dumps, and DiigoBridge, so a cache never mixes sizes: ~131 pages instead
of ~698 for 16.7k bookmarks.
Cache invalidation, agreed beforehand: pages are cached by (page number,
size), so pages cached at 24 become misses and refetch at 128. Items are
keyed by link_id and just re-upsert; nothing is lost.
Unknown whether diigo honors count=128, so a short page that isn't the
last is refused loudly (logged, thrown; a fill fails with the reason)
rather than silently skipping items. The fake gains a capped account to
test it; the plain account grows to 300 (3 pages), the deep one to 6000.
Generated-by: Claude Opus 5.5
/cache/ without a fragment is now a login form: petname
(autocomplete=username) and cache id (type=password,
autocomplete=current-password), the shape password managers fill from
the entries the save button makes. Its inputs have no name, so a native
submit (script failed) sends nothing; the key stays in memory, out of
the address bar and history. Home links to it.
Generated-by: Claude Opus 5.5
Past the transitional idiom: the cache logic (DiigoCacheLogic,
PublicCacheLogic in cache.ts) requires its Clock, with no default, so
cache.ts reaches no ambient time. The worker's entry module is the
edge: it makes the live clock and exports the runtime-constructed
DiigoCache/PublicCache as (ctx, env) subclasses that supply it (the
Cloudflare constraint: the runtime constructs DOs with ctx and env
only). Binding class names are unchanged, so stored state is too. The
tests' FixedClockCache extends the logic class.
Generated-by: Claude Opus 5.5
No module-level liveClock: the live clock is made in the default
argument expression, at the edge where the runtime constructs the DO,
so module scope holds no powerful object.
Generated-by: Claude Opus 5.5
Ocap discipline: no ambient time in logic. PagedCache reads time and
waits only through a Clock { now, sleep }, a default constructor
argument (the live clock; the runtime constructs DOs with ctx and env
only). The fill job's timestamps and alarms, the etch time, and the
diigo-call pacing all go through it.
Tests interpose through the constructor: tests/test-worker.ts (now the
tests' main module) adds FixedClockCache, bound as CLOCKED_DO, and a
test pins startedAt, updatedAt, and the alarm time to the fixed clock
(an ambient new Date() fails it).
Generated-by: Claude Opus 5.5
The cache page now follows the paper's form (fragment + skeleton page),
with the XHR replaced by a Cap'n Web call taking the key in-band.
Generated-by: Claude Opus 5.5
Starting a fill no longer POSTs the cache id to /_fill. Instead:
- the created page links the cache's webkey, /cache/#<cache id>: a
skeleton page, the same for every key, that reads the key from the
fragment (never sent to the server or in a Referer)
- its script trades the key in-band for a capability over Cap'n Web at
/api: CacheApi.cache(key) returns a CacheCap facet (fill, status,
fingerprint, the public companion's fill) and never the DO's wiring
methods; malformed and unknown keys get one refusal that never echoes
the key, and each refusal is logged without it
- then calls startFill(): trade and call pipeline into one round trip
- the worker serves capnweb's browser build itself at /_capnweb.js (a
wrangler Text rule), so no third-party script sits beside the key
The /_fill route and its forms are gone. Tests run the real capnweb
client against the worker (fetch routed to worker.fetch).
Generated-by: Claude Opus 5.5
POST /_fill (cache id in the body; target private|public; action
start|stop) starts or stops a background walk of every diigo page into
the cache. DO alarms advance it pagesPerStep pages per step (default
20), a second apart; progress (FillStatus) is stored in the DO after
each page, so it survives eviction. Cached pages are skipped; starting
while running rejoins the job; a stop mid-step wins; a failure is
recorded with its reason and logged once. The created page gets fill
buttons; the public page shows "filling: n of m pages".
Fill state is new, additive DO storage (fill, fillStep): no existing
state changes meaning.
The 13 fill specs are unskipped and green, plus one: a fill makes one
diigo call per page fetched.
Generated-by: Claude Opus 5.5
Two bugs in page()'s miss path, found while building the fill job:
- the webapp stubs (app, pathjoin, query) were never disposed, which
kept the DO from being evicted (evictDurableObject hung after a single
miss); they are now disposed once the page is in hand
- a miss on page n > 1 also fetched page 1 and threw it away, doubling
diigo calls; every page carries `total`, so only page n is fetched
Regression test: a miss, then eviction, then a cache hit. tsconfig adds
the ESNext.Disposable lib for Symbol.dispose.
Generated-by: Claude Opus 5.5