Commit Graph
100 Commits
Author SHA1 Message Date
dckcandClaude Opus 5.5 193a126d1c capsite/toybox: routing carries no secrets
The ingress no longer reads labels from a Toybox-Labels header. A header
was no place for the webkey pattern: browsers can't set one on a
WebSocket, and the pattern puts the secret in-band anyway. A page keeps
it in its fragment, reaches the vat by public labels, and sends it in
the body of a call, which the vat checks like a swissnum.

A request still reaches a vat only by naming every label of its mount.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:44:29 -05:00
dckcandClaude Opus 5.5 28b1c3c9d3 capsite/toybox: no encoder object in module scope
A module-level TextEncoder is a mutable object every importer shares.
Keep only its encode method, bound and hardened; the encoder itself is
unreachable. The code hash uses it too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:44:29 -05:00
dckcandClaude Opus 5.5 7fcd251261 capsite/toybox: mount lookups in SQL, not a scan in JS
Each request parsed every mount's labels and searched them in JS. Labels
now sit one per row in mount_label, indexed on (key, value): a lookup
joins the request's labels (json_each) on the index and keeps the vat
whose every label matched. The overlap check at registration is SQL too.

mount.labels keeps the whole set as JSON; mounts stored before
mount_label get their rows filled in when the schema runs. The SQL lives
in mounts-sql.js, tested on node:sqlite, old-shape state included.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:44:29 -05:00
dckcandClaude Opus 5.5 9356f63c64 capsite/toybox: OCapN vats; createVat takes remote objects
The admin socket now also speaks OCapN over Cap'n Web: a WebSocket there
reaches the VatAdmin DO, an OCapN peer whose bootstrap (swissnum
vatAdmin) offers createVat(src, params). params may hold remote objects;
the vat's root comes back as one.

Each OCapN vat is its own peer: SES and @endo/ocapn run in its facet
(vat-runtime.js, bundled with esbuild), around the vat's own module,
which exports buildRootObject(vatPowers, params). Passing a client's
object to a vat, or a vat's root to the client, is an OCapN handoff, so
the receiver dials the exporter. Nothing here can dial out, so the
VatAdmin is also the router: a client registers its gateway under a
designator, a vat's designator is its id, and every peer reaches every
other through it. The netlayer is ertp-clerk's, made to work over Workers
RPC stubs and plain promises as well as Cap'n Web.

Not yet: sturdyrefs that survive a restart, an adminNode, and grants
(egress, mount) for OCapN vats. App vats (export class App) still work
beside them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:44:29 -05:00
dckcandClaude Opus 5.5 8d8900d84d capsite/toybox: vats have ids, not names; ingress by mount
createVat (POST / on the admin socket) makes a vat with newUniqueId and
returns its id, which addresses it only there. A vat is reachable only
where it is mounted: ?mount=<labels> on each start, as ?egress= is
granted; absent means none.

The ingress reads a request's labels from its query string, plus a
Toybox-Labels header for secret ones (a URL ends up in logs and
Referers). It routes to the vat whose mount labels the request carries,
every one: stricter than Flower's no-conflict match, since an ingress
client holds no constrained handle and a label it omits must not match.
Registration keeps Flower's rule, so a mount must conflict with every
other and no request can carry two. The app keeps the whole path and
sees its connection labels in Toybox-Connection-Labels.

A vat from before ids lives at idFromName(name). PUT /by-name/<name>
reaches it on the admin socket; it stays unreachable from outside until
mounted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:44:29 -05:00
dckcandClaude Opus 5.5 f0557d29d6 capsite/toybox: egress through Flower-style switchboard handles
The switchboard now serves a Cap'n Web Handle on each of its sockets:
public (no loopback or private addresses) and local. A socket's labels
are fixed in config, so holding the socket's fd is holding that handle;
constrain(labels) returns a narrower one, as Flower's Constrain returns
a narrower fd. Labels only accumulate: a missing key matches anything,
and a key both sides set must agree (labels.js, shared by both).

The switchboard enforces the labels; toybox's Egress no longer checks
URLs itself. It opens a session to its one switchboard socket, constrains
the handle to the vat's grant, and forwards through it; a WebSocket
crosses the session as a pair of message streams. A vat narrows its own
grant with env.egress.constrain().

Grants are now labels as JSON. One stored before, a bare host[:port],
reads as {host, port}.

pnpm start and the systemd units give toybox the public handle; the
switchboard unit binds the local one in its own 0700 runtime directory.
The tests give their toybox the local handle to reach an echo server,
and check that a toybox holding only the public one never reaches it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:44:29 -05:00
dckcandClaude Opus 5.5 2820c015ee capsite/toybox: a grant is an object in env, not the vat's global fetch
A vat's globalOutbound is now always null. A granted host arrives as
env.egress, an Egress the app can hand to whichever of its parts should
have it; as globalOutbound it was ambient to every module in the vat.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:44:29 -05:00
dckcandClaude Opus 5.5 7bf6eb44c7 capsite/toybox: a switchboard, the vats' only way out
A vat granted a host (PUT /<vat>?egress=<host> on the admin socket) gets
Egress as its globalOutbound, with the host in ctx.props where the vat
can't change it. Egress forwards matching requests, WebSocket upgrades
included, to the switchboard and refuses the rest with a 403; with no
grant, fetch still throws.

The switchboard is a separate workerd: a Switchboard DO that calls fetch
and returns the response, so a 101's webSocket passes through. toybox
reaches it over a unix socket in HTTP proxy style, which keeps the full
URL, scheme included. Its network service is the outer fence, with TLS
trusting the browser CAs; systemd units for it join toybox's, and only
the toybox group can open its socket.

The loaded Worker is now named by vat and incarnation, not by code hash:
the same code with a different grant must not share a Worker. The new
incarnation table sits beside the old ones; vats installed before it
read as incarnation 0 with no grant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:44:29 -05:00
dckcandClaude Opus 5.5 88739d9495 capsite/toybox: vats with code installed at runtime, on workerd
Each vat is a Durable Object that stores code by hash and runs it as a
facet through the Worker Loader: an empty env, no fetch, storage that
carries over upgrades. Two sockets with no configured address: whoever
starts workerd hands them in (systemd socket activation), so the admin
socket is the authority to install code.

Tests run a real workerd: install, upgrade, isolation between vats, no
fetch, the public port can't install, and code and state survive a crash.
They spawn the package binary rather than the .bin shim, which forks
workerd, so killing the shim orphaned it and the test run never ended.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:44:29 -05:00
dckc 1d8a220bbc capsite: checklist: 2-iv, the weekly draft works in dev
Generated-by: Claude Opus 5.5
2026-10-09 02:12:41 -05:00
dckc 68132ff4b3 diigo-week: a dashboard card drafts last week in one click
The card shows last week's issue with a link to it, or a button to
draft it. Drafting a week that already has an issue (draft or
published) is refused, with a link to that issue, rather than making
a duplicate.

Generated-by: Claude Opus 5.5
2026-10-09 02:12:41 -05:00
dckc 9c09e8c5a4 capsite: checklist: 4b, consider scruffier tags
Generated-by: Claude Opus 5.5
2026-10-09 02:12:41 -05:00
dckc 94a059af25 diigo-week: draft a potluck issue from a week of diigo bookmarks
An EmDash plugin (native, content:write only) with one admin page:
a diigo username and an ISO week. It reads diigo-cache's public page
(?in=<week>&by=created) as microformats, follows rel=next, and
creates a draft in issues shaped like seed/issues.json.

Transport is a DIIGO_CACHE service binding rather than ctx.http.fetch:
EmDash's SSRF guard refuses localhost, and the binding names one
worker. Logic lives in src/week.ts with the fetcher passed in; tests
read fixtures in diigo-cache's markup. The checklist's goal 2 now
names potluck; DiigoBridge waits.

Generated-by: Claude Opus 5.5
2026-10-09 02:12:41 -05:00
dckc be796d9d0d diigo-cache: the public page is an h-feed of h-entry bookmarks
Microformats are the public page's API for other sites (potluck
first): each bookmark is an h-entry with u-bookmark-of, p-name,
p-summary, a p-category per tag (linked to a tag search),
dt-published (bookmarked) and dt-updated (last edited). Prev/next
links carry rel=prev/next, so a reader can follow a filter's pages.
Tests parse the page with microformats-parser to pin the contract.

Generated-by: Claude Opus 5.5
2026-10-09 01:49:40 -05:00
dckc c66aaea366 potluck, diigo-cache: docs one line per paragraph; potluck deploy caveat
Obsidian and GitHub render each newline as a line break, so the
hard-wrapped READMEs and CONTRIBUTINGs rendered ragged. Whitespace
only, except a note in potluck's CONTRIBUTING that other workerd
hosts are untested: they'd have to supply the D1 and R2 bindings.

Generated-by: Claude Opus 5.5
2026-10-09 01:30:56 -05:00
dckc a25bd699e1 potluck: README: workerd, not a vendor
Generated-by: Claude Opus 5.5
2026-10-09 01:29:31 -05:00
dckc ea3e256bb8 diigo-cache: README: workerd, not a vendor
Generated-by: Claude Opus 5.5
2026-10-09 01:28:50 -05:00
dckc 0af7a3ea6c diigo-cache: README sells and gets to TADA; CONTRIBUTING updated
New README: two sentences, the dev run through connect, fill, and a
filtered public page, then features. CONTRIBUTING gains the 1d
filters and its own run and deploy sections instead of pointing at
the checklist; links to the old README now point at CONTRIBUTING.

Generated-by: Claude Opus 5.5
2026-10-09 01:27:45 -05:00
dckc 9719fd0aa8 diigo-cache: README becomes CONTRIBUTING; AGENTS.md links to it
Generated-by: Claude Opus 5.5
2026-10-09 01:27:12 -05:00
dckc bce2424cc0 potluck: README: a heading that says something
Generated-by: Claude Opus 5.5
2026-10-09 01:25:47 -05:00
dckc 7f1e19fe85 potluck: README: two sentences, then getting started, then features
Generated-by: Claude Opus 5.5
2026-10-09 01:25:13 -05:00
dckc 8a812d76e3 potluck: README sells and gets to TADA; CONTRIBUTING has the rest
The README was EmDash's template blurb. Now it says what potluck is
and its first block runs from a fresh clone to /issues, using a new
`pnpm seed:issues` script that applies seed/issues.json to the local
D1 database. CONTRIBUTING covers how it's put together, seeds, checks,
the EmDash rules, and deploying; the template's stale notes (Inter,
three-column articles) are gone. AGENTS.md is now a symlink to it.

Generated-by: Claude Opus 5.5
2026-10-09 01:24:10 -05:00
dckc 002f08b456 diigo-cache: date windows and search on the public page (1d)
/public/<username> takes days, after, before, in (year, month,
quarter, ISO week), by=updated|created, and q (words, "phrases",
#tags, -negation, gmail/Slack/GitHub date operators). JSON answers
echo the parsed filter; a malformed one is a 400.

Filters query the stored items in sqlite through the read-only
viewer instead of walking diigo pages: the public page keeps no
fetch authority, and a filled cache already holds every item.
Results say `complete` when every diigo page is stored; freshness
is 1f's head refresh. So the fetch budget and the O(log pages)
walk test go, and the fake deep account shrinks to 300.

Generated-by: Claude Opus 5.5
2026-10-09 01:17:03 -05:00
dckcandClaude Opus 5.5 efb54ad4cc docs: Good Fences Make Good Agents (design story + IBIS)
The story behind good-fences: lend a governed mower over the fence to a
neighbor whose tenants are model-written code, with the design IBIS
worked out so far. Previously an untracked note at the workspace root.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 01:00:15 -05:00
dckcandClaude Opus 5.5 8c110098ff feat(good-fences): a Think agent mows with a codemode mower connector
Neighbor is a Project Think agent on gpt-oss-120b. Its execute tool
runs model-written code in a dynamic worker; MowerConnector puts the
mower there as the `mower.*` namespace (start/push/turn/stop, each
taking an args object). makeMower is the Logo-turtle mower on a
wrapping yard; for now it lives inside the agent's DO.

Getting to green, one symptom at a time:
- chatted instead of acting: say it acts by running code
- reply cut mid-sentence: maxOutputTokens 4096 via beforeTurn
- execute failed creating codemode's runtime facet ("Incorrect type
  for the 'class' field"): declare CodemodeRuntime as a DO class
- looked for the mower in the workspace filesystem: activeTools
  ["execute"], and name the namespace in the prompt
- found the mower, then asked for lawn dimensions: "don't ask me
  questions: put it to use"

Green: 36 mower calls, a 30-cell rectangle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 00:35:56 -05:00
dckcandClaude Opus 5.5 d61cb8c9a1 test(good-fences): lent a mower, a Think agent mows (red)
The first end-to-end test: a Project Think agent, on the real model,
writes code that runs in a dynamic worker and drives a mower; the test
counts the cells cut, and saves the turn to itest-output/.

Installs @cloudflare/think, @cloudflare/codemode, agents, ai,
workers-ai-provider, zod. The model-test worker now has a main entry,
the Neighbor DO, and a LOADER binding.

Red: src/worker.ts doesn't exist yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 00:31:25 -05:00
dckcandClaude Opus 5.5 a2ca59a27e feat(good-fences): code-mode prompt; the model writes code to mow
codeModeMessages turns what I say plus `about` into a prompt: a
neighbor persona, rules for the reply (one async arrow function over
`env`), and the lent things' types. Things are remote and calls on one
are delivered in order, so a lamp example shows pipelining: await only
for a value, or on a batch's last call. With only the rule, the model
awaited every call; with the example, it pipelines.

aboutMower gives the Mower type with Promise returns. The model test
checks the code calls only Mower methods, pushes, and awaits less often
than it calls; it saves the code to itest-output/ via a reporter, since
the test runs in workerd, which can't write files. Model tests get 60s.

Green: both model tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 00:21:44 -05:00
dckcandClaude Opus 5.5 4a85143a66 fix(good-fences): give the model room to answer
gpt-oss-120b reasons before it answers, and Workers AI's default
max_tokens (256) ran out mid-thought: finish_reason "length", content
null. Ask for 4096, and when there's no reply text, say which choice
and message keys came back instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 00:21:44 -05:00
dckcandClaude Opus 5.5 37faa92649 test(good-fences): lent a mower, the model writes code to mow (red)
Code mode (2c): the harness turns what I say plus `about` into a prompt;
the model replies with `(env) => ...` code. Checked textually for now,
since workerd won't eval it: the code pushes `env.mower` and calls only
methods in `about`. It also prints the code, to see what the model writes.

Red: src/code-mode.ts doesn't exist yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 00:01:39 -05:00
dckcandClaude Opus 5.5 ec24f3e8e4 feat(good-fences): complete(messages) on a Workers AI model
makeWorkersAiModel narrows the AI binding to one model's chat
completion: the harness gets `complete`, not `run` on any model at
any cost. gpt-oss-120b replies in the OpenAI shape (checked with a
one-off probe), so the text is choices[0].message.content; anything
else throws, naming the keys it got.

Green: the model counts 3 rs in strawberry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 23:41:18 -05:00
dckcandClaude Opus 5.5 47a2337afe test(good-fences): the model counts the rs in strawberry (red)
A first integration test of the model call: Workers AI through the AI
binding, behind the same `complete(messages)` interface as the sim's
fake model. Opt-in (`pnpm test:model`) since it needs `wrangler login`
and costs money.

Red: src/model.ts doesn't exist yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 23:36:46 -05:00
dckcandClaude Opus 5.5 9b12943be4 test(good-fences): show which mower the neighbor gets
The chat and load arrows drew `things: { mower }` whether I lent the
raw mower or the governed one. Name objects by the reply arrow that
introduced them, so the diagram reads `things: { mower: governed }`
and lending the raw mower would fail the trace comparison.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 23:27:13 -05:00
dckcandClaude Opus 5.5 9a3e7348c6 test(good-fences): lend-the-mower journey (red)
The spec is a sequence diagram of the a2030.md story: I lend a governed
mower; the neighbor's harness shows the model only `about`, runs its
code as a tenant, and the governor stops the tipsy tenant at a 2-cell
fence. The test runs the actors through a causal tracer and compares
the trace with the diagram, then checks the yard and what the model saw.

The tracer comes from agoric-sdk PR #12895, extended to name
participants made mid-sequence (governor, tenant) and to draw thrown
errors as replies.

Red: src/ doesn't exist yet (ERR_MODULE_NOT_FOUND for src/fence.ts).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 22:17:48 -05:00
dckc b4ab820486 capsite/docs: references to DOs in Discord messages
How a Discord message could carry references to our DOs, after
@endo/chat: names in the text, references beside it in a mailbox DO,
an Adopt button answered ephemerally, and per-user petstores. Webkeys
in markdown work but disclose the capability to every channel reader
and to Discord itself; ephemeral replies still pass through Discord,
so references should never cross it at all.

Generated-by: Claude Opus 5.5
2026-10-08 16:19:45 -05:00
dckc 93d92d0207 capsite: checklist: run potluck, not madmode
The EmDash site is now potluck/, created fresh with create-emdash.

Generated-by: Claude Opus 5.5
2026-10-07 20:33:08 -05:00
dckc 1aeb3a21db capsite: toybox user stories and goals (T1-T3)
toybox-stories.md: the weekly roundup (#23), album-to-album sync
(endojs/playground#16), and drag-to-archive.org (#118), with the needs
they share. The checklist gains the toybox tree: a changed signal (T1),
Blockly composition by petname (T2), and confined code in Dynamic
Workers (T3).

Generated-by: Claude Opus 5.5
2026-10-07 20:33:08 -05:00
dckc a086a8c495 capsite: checklist: 1g, notes on public items are public too
The public cache keeps public items whole, private annotations and
comments included (2,525 of 14,645). Dan finds that acceptable; the
goal is to finish thinking it through, not to fix a leak.

Generated-by: Claude Opus 5.5
2026-10-07 20:33:08 -05:00
dckc 45223eb0b7 capsite: checklist: reconcile 1d with 1f; decide DiigoBridge's fate (2-iii)
1d's query.ts plans to filter while walking diigo's pages, but a filled
cache holds every item: choose before implementing. madmode's public
reads need no cache id, so DiigoBridge's cache-id TODO is moot unless a
private-reading caller wants it.

Generated-by: Claude Opus 5.5
2026-10-07 20:33:08 -05:00
dckc fd7fd5728d capsite/potluck: a weekly zine of issues, not a blog
- issues collection (ISO-week slug, week, dek, rich-text body); home,
  /issues, and /issues/<week> pages; seed/issues.json holds three
  sketched issues (W38-W40) from public diigo bookmarks, editorial
  lines marked (draft); issues-sketch.md is the dry run behind them
- microformats: h-feed of h-entry lists; an issue is an h-entry
  (p-name, dt-published, p-summary, e-content); off-site links render
  as h-cite, /tag/<tag> links as rel-tag p-category (CiteLink.astro)
- diigo tags: shown on each bookmark and attached to issues via the
  tag taxonomy (now posts + issues); /tag/<tag> lists issues
- demo blog content gone: posts dropped from the seed, home page lists
  issues, site title/tagline, footer About widget, and About page
  rewritten; nav is Home / About / Issues; search and RSS cover issues
- .column container gives potluck's pages the header's margins

Generated-by: Claude Opus 5.5
2026-10-07 20:31:54 -05:00
dckc b9f3156d6d capsite/potluck: plain style after kmjn.org's "Plain HTML with a few bureaucratic gestures"
https://www.kmjn.org/notes/modern_plain_html.html and its stylesheet:
system fonts (Verdana; no Google webfonts), one 800px column,
line-height 1.35, links in the text color with a dotted underline, no
rounding, shadows, or tracking; the header is plain, not sticky glass.
Token overrides live in theme.css; per-page scoped styles unchanged.

Generated-by: Claude Opus 5.5
2026-10-07 20:00:29 -05:00
dckc 4b4ef8fea6 capsite: potluck, a fresh EmDash site (cloudflare:blog out of the box)
pnpm create emdash@latest potluck --pm pnpm --install --yes, per
https://docs.emdashcms.com/ ("Create your first EmDash site"); the
--yes defaults are cloudflare + blog. Replaces the old madmode/
starter (never committed).

Generated-by: create-emdash 1.2.0, run by Claude Opus 5.5
2026-10-07 19:57:43 -05:00
dckcandClaude Opus 5.5 af8dd75fd1 capsite: public-origin candidate: sync (as in bib sync #195)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:03:27 -05:00
dckcandClaude Opus 5.5 45a2524542 capsite: more public-origin candidates: syndicate, peers
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:01:42 -05:00
dckcandClaude Opus 5.5 9dbf34efa7 capsite: pesos.madmode.com as the candidate public origin
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:00:40 -05:00
dckcandClaude Opus 5.5 9c3e6fa396 capsite: fedi-cache keeps the token; ActivityPub sources come later
The existing export (mastodon-archive) reads favourites, bookmarks,
and mentions, which ActivityPub doesn't carry. So the client API with
a narrow read-only token (held in webapp) is the primary source;
items are still keyed by ActivityPub uri, and outbox/follower-actor
sources move to a later portability goal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 12:58:55 -05:00
dckcandClaude Opus 5.5 1e6436e089 capsite: social.coop tree at the ActivityPub level (fedi-cache)
Mastodon is one implementation; the cache speaks the protocol: outbox
for backfill, and fedi-cache as a follower actor so Update/Delete are
delivered rather than polled for. Its own keypair replaces an access
token, so webapp drops out of this one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 11:16:05 -05:00
dckcandClaude Opus 5.5 99ce5b6964 capsite: goal trees for a zotero cache and a social.coop cache
Both follow the diigo-cache pattern (webapp holds the credential,
read-only and minted narrow at the source; the cache serves a public
page). Zotero's versioned sync answers diigo's stale-head problem;
Mastodon's doesn't announce edits/deletes, so its tree carries that
as a design question.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 10:21:24 -05:00
dckc 279ddb4caa fixup! diigo-cache: the public page only reads
Comments written for the reader: no restating what the shebang says, no traces of the design conversation; the publish comment no longer mentions a projection that no longer exists.

Generated-by: Claude Opus 5.5
2026-10-07 09:46:21 -05:00
dckc 7c0d1d62c6 fixup! diigo-cache: RSS in diigo's export shape, from cached items
Comments written for the reader: no restating what the shebang says, no traces of the design conversation; the publish comment no longer mentions a projection that no longer exists.

Generated-by: Claude Opus 5.5
2026-10-07 09:46:21 -05:00
dckc 697fd95514 fixup! diigo-cache: RSS in diigo's export shape, from cached items
compare-export.ts: the manual is a HELP string at the top of the module
(a docstring, as a value) and main prints it on bad usage. No reading
its own source, no regex, one copy; @file points at it.

Generated-by: Claude Opus 5.5
2026-10-07 09:44:26 -05:00
dckc 2055e8c1c9 fixup! diigo-cache: RSS in diigo's export shape, from cached items
compare-export.ts: the usage message comes from the @file manual's
synopsis line (read from the script's own source, on the bad-usage path
only), docopt-style, so the synopsis lives in one place; if the header
changes shape, it says "see the @file comment".

Generated-by: Claude Opus 5.5
2026-10-07 09:42:31 -05:00
dckc 8400078656 fixup! diigo-cache: RSS in diigo's export shape, from cached items
compare-export.ts: the manual drops "exits 2 on bad usage"; it's the
convention, and BAD_USAGE documents it.

Generated-by: Claude Opus 5.5
2026-10-07 09:40:47 -05:00
dckc a40d2e924a fixup! diigo-cache: RSS in diigo's export shape, from cached items
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
2026-10-07 09:40:10 -05:00
dckc 549ff5bf4b fixup! diigo-cache: RSS in diigo's export shape, from cached items
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
2026-10-07 09:39:00 -05:00
dckc 9c99f4f969 fixup! diigo-cache: RSS in diigo's export shape, from cached items
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
2026-10-07 09:37:31 -05:00
dckc 2bb18a9e96 fixup! diigo-cache: diigo counts pages from 0; migrate stored pages (schema v2)
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
2026-10-07 09:34:06 -05:00
dckc 2a7d0ae0cb fixup! diigo-cache: SQL in .sql files, behind a read-only facet
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
2026-10-07 09:34:06 -05:00
dckc ab195b51c4 fixup! diigo-cache: date windows and search, designed as skipped specs (goal 1d)
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
2026-10-07 09:34:06 -05:00
dckc 11c4583a17 fixup! diigo-cache: bookmark cache in Durable Objects (goal 1c)
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
2026-10-07 09:34:06 -05:00
dckc 2fc5cfb9ae fixup! refactor: rename diigo-dump to diigo-cache
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
2026-10-07 09:34:06 -05:00
dckc 0a5de621ec fixup! diigo-cache: fill from a webkey page over Cap'n Web
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
2026-10-07 09:34:06 -05:00
dckc 164d3cb8cb fixup! diigo-cache: the public page only reads
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
2026-10-07 09:34:06 -05:00
dckc 2a1942fc2a fixup! diigo-cache: invalidate the last N days of cached pages
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
2026-10-07 09:34:06 -05:00
dckc 76b9ed1494 fixup! diigo-cache: RSS in diigo's export shape, from cached items
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
2026-10-07 09:34:06 -05:00
dckc 38d9ed4122 fixup! docs: goal tree aims at the weekly roundup (#23)
5-ii: rate-limiting is chiefly about /api now that public pages only read.

Generated-by: Claude Opus 5.5
2026-10-07 09:26:09 -05:00
dckc d857018e4b fixup! docs: diigo-cache UX map
The public page's write access is resolved: it reads through viewer().

Generated-by: Claude Opus 5.5
2026-10-07 09:26:09 -05:00
dckc d7190abaf8 diigo-cache: the public page only reads
/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
2026-10-07 09:26:09 -05:00
dckc a17d287e1d diigo-cache: SQL in .sql files, behind a read-only facet
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
2026-10-07 09:24:33 -05:00
dckc 6dd7a37532 docs: diigo-cache UX map
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
2026-10-07 09:21:30 -05:00
dckc 0bffff8175 fixup! docs: goal tree aims at the weekly roundup (#23)
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
2026-10-07 09:06:55 -05:00
dckc 1a3bc14416 docs: goal tree aims at the weekly roundup (#23)
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
2026-10-07 09:06:55 -05:00
dckc f3241e4985 fixup! diigo-cache: RSS in diigo's export shape, from cached items
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
2026-10-07 08:44:22 -05:00
dckc dc4f23ad04 fixup! diigo-cache: RSS in diigo's export shape, from cached items
compare-export.ts: Object.fromEntries(Object.entries(…).map(…)) as
objectMap(original, mapFn), with @endo/common's signature.

Generated-by: Claude Opus 5.5
2026-10-07 08:41:30 -05:00
dckc 21505a5b8f fixup! diigo-cache: RSS in diigo's export shape, from cached items
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
2026-10-07 08:40:00 -05:00
dckc dfd9a3d1ef 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
2026-10-07 08:36:57 -05:00
dckc d145841a2e fixup! diigo-cache: RSS in diigo's export shape, from cached items
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
2026-10-07 08:34:02 -05:00
dckc 7be87d37dc chore: Update pnpm-lock.yaml
Generated-by: Claude Opus 5.5
2026-10-07 08:34:02 -05:00
dckc 0006c7c3a3 diigo-cache: lint typechecks the program scripts
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
2026-10-07 08:34:02 -05:00
dckc cbde0dfbd3 fixup! diigo-cache: RSS in diigo's export shape, from cached items
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
2026-10-07 08:31:07 -05:00
dckc 81f91a6747 fixup! diigo-cache: invalidate the last N days of cached pages
README: the invalidate control.

Generated-by: Claude Opus 5.5
2026-10-07 08:27:19 -05:00
dckc 904f620f06 diigo-cache: invalidate the last N days of cached pages
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
2026-10-07 08:27:13 -05:00
dckc cffb1fa9b2 diigo-cache: diigo counts pages from 0; migrate stored pages (schema v2)
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
2026-10-07 08:26:13 -05:00
dckc e6deb9fda0 diigo-cache: RSS in diigo's export shape, from cached items
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
2026-10-07 05:22:12 -05:00
dckc 6af05190a6 chore: ignore export/ (diigo exports are personal data)
Generated-by: Claude Opus 5.5
2026-10-07 05:22:12 -05:00
dckc 4034fba76b diigo-cache: a fill ends at the first page past the data
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
2026-10-07 05:08:12 -05:00
dckc fd16c7db53 diigo-cache: pace diigo calls only; cache hits don't wait
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
2026-10-07 05:06:17 -05:00
dckc f5d57f73d3 diigo-cache: a short page before the stated end peeks at the next page
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
2026-10-07 05:04:04 -05:00
dckc f4b68f1b59 fixup! diigo-cache: bookmark cache in Durable Objects (goal 1c)
README: PAGE_SIZE is 100, diigo's cap.

Generated-by: Claude Opus 5.5
2026-10-07 04:59:39 -05:00
dckc 563cb83995 diigo-cache: page size 100, diigo's cap; fill jobs record their page size
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
2026-10-07 04:59:39 -05:00
dckc 3baffcab4c fixup! diigo-cache: bookmark cache in Durable Objects (goal 1c)
README: page size is PAGE_SIZE (128), not 24.

Generated-by: Claude Opus 5.5
2026-10-07 04:57:01 -05:00
dckc 826153e35f diigo-cache: fetch 128 items per diigo page; refuse short pages
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
2026-10-07 04:56:56 -05:00
dckc efaf74023d diigo-cache: webkey page logs in with the password manager
/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
2026-10-07 04:48:15 -05:00
dckc 961515d8db fixup! diigo-cache: the caches take their clock as a constructor argument
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
2026-10-07 04:39:29 -05:00
dckc cf901ded5f fixup! diigo-cache: the caches take their clock as a constructor argument
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
2026-10-07 04:35:39 -05:00
dckc b6823867ff diigo-cache: the caches take their clock as a constructor argument
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
2026-10-07 04:34:48 -05:00
dckc 7d2100bf77 fixup! docs: goal tree and rules track the 1c cache
1e: the fill is driven from the webkey page over Cap'n Web.

Generated-by: Claude Opus 5.5
2026-10-07 04:32:44 -05:00
dckc 3706e6de2c fixup! docs: webkeys — ids as bearer capabilities, kept out of URLs
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
2026-10-07 04:32:44 -05:00
dckc 3d0e3aea69 chore: Update pnpm-lock.yaml
Generated-by: Claude Opus 5.5
2026-10-07 04:32:44 -05:00
dckc 8c221abe6f diigo-cache: fill from a webkey page over Cap'n Web
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
2026-10-07 04:32:44 -05:00
dckc fbe2538345 fixup! docs: goal tree and rules track the 1c cache
1e done: compressed to a pointer at the API and tests.

Generated-by: Claude Opus 5.5
2026-10-07 04:25:38 -05:00