Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
50 most recent check-ins
|
2026-11-14
| ||
| 07:42 | 0.155.1 (patch): a boot that fails exits 1, so systemd restarts it (ticket d72c34c2ab) Production, 2026-11-14 03:47 UTC: the disk was full, the sqlite native library could not be extracted, the pool failed to open at boot. The error reached ABCL's debugger, which read EOF from the service's stdin and exited 0: systemd logged "Deactivated successfully", Restart=on-failure never fired (NRestarts=0), and the site answered 502 until someone looked. - boot.lisp binds *debugger-hook* around the boot, in the booting thread only: an unhandled error prints "spicemine: boot failed: <reason>" and the condition's type, and exits 1. That thread then sleeps; the threads the server starts see the global value, so an error answering a request can never end the process. - deploy/README.md (what the unit gives you), FEATURES.md. - deploy.sh needed nothing: a server that never answers as the new version already fails the deploy after 120 s, with the journal. Tests: - boot-exit.sh (new, 9, in the gate): real boots of the installed server - a home it cannot open (the database fails, as production's did) and a port another process holds each exit 1 with "boot failed" and never the debugger; control: a good boot says it is up and is still running. - Mutation control: the hook unbound - exit 0 and the debugger, as in production: 5 failures. Gate: 14172 checks, ALL GREEN (+9: boot-exit.sh). The first run failed once on browser/think (sqlite), a typing-cadence check under load, unrelated; the rerun is green, threshold unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Leaf check-in: 60b7791dec user: dkords tags: trunk | |
| 03:20 | 0.155.0 (minor): what a crawler is told on a walled site says what the wall does (ticket fc7623a488) Left open by 0.150.0 (GUARDIAN.md s8, "not done"). Worse than impolite: on a members-only site robots.txt still listed the source paths of public ask/no projects, and /.well-known/ai-policy every public project, naming to a stranger what the wall hides. - Members only: robots.txt disallows everything and names no project; the AI policy document lists no project (its vocabulary stays). - Showcases shown: everything disallowed but the home page, the spaces and each face - showcase, journal, feed, pictures - of whatever a visitor sees (public and not closed, or members-only). RFC 9309 rules: the longest wins, $ ends one. A private or closed project never named. - While the wall is up, any project's source counts as disallowed for the robots-ignored signal: we said no in writing. - Open: as before. - GUARDIAN.md s1.4 and s8, FEATURES.md. Tests: - aiok-test (+40, now 73): both words' documents; twenty paths read through an RFC 9309 matcher against what a visitor is served; what is never named; the signal's project rule under each word and open again. - wall.e2e (+7, now 91): both documents under members only and showcases shown, and robots.txt as before when open. - Mutation control: robots.txt and the project list ignoring the wall - 14 unit and 4 e2e failures. Gate: 14163 checks, ALL GREEN (+94: aiok-test and wall.e2e run on sqlite and postgres). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 4a06a1b954 user: dkords tags: trunk | |
| 00:33 | 0.154.0 (minor): Invites step 2 - a request is a short form; the interview its own switch (ticket 1a4d412d8c, INVITES.md s5) - A request for an invite is, by default, a short form the mailed link opens: what you are making (required, up to 1000 characters), a link to something you made (http or https), the name you would like (a name's shape). spice/ask:ask-send-form; what is to be fixed is said, and what was typed comes back. - The conversation with the Doorkeeper is the request switch's own switch, "With an interview" (option.invites.interview, spice/ask:interview-on-p). Each way round, the other kind of send goes back to the page the link opens now. The row's line says which. - Migration 0063: an instance that took requests keeps the interview. - The admins' mail and queue: "the whole request"; a form request's link is text, never a link; a conversation is folded only when there is one. - The door's lead says what the link opens. - INVITES.md (step 2 built; open question 6 answered by the switch), FEATURES.md. Tests: - ask-test (+32): the row and its switches; the form refusing nothing said, too much, a link that is not one, a name of the wrong shape, none of it reaching an admin; sent once, a break kept, the name lowered, no transcript, an admin told, the queue with the link never a link, answered as any other; migration 0063 three ways. - ask.e2e (+15): the door's words, the link opening the form, a phone, a bad link refused with what was typed kept, sent and spent, the admin told, the queue, a quiet decline; then the interview switched on and the Doorkeeper walk as before. - Mutation controls: the link always opening the Doorkeeper - 2 e2e failures; links accepted unchecked - 14 unit, 2 e2e. Gate: 14069 checks, ALL GREEN (+94). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: c5a4b7a5c3 user: dkords tags: trunk | |
| 00:11 | 0.153.0 (minor): a failed backup or offsite push is mailed to the admins (ticket f3634732d5) Production's offsite pushes failed silently for two days. 0.114.0 put the push's failure on /api/v0/health, which nobody was looking at; the backup itself had no marker at all. - The NixOS module: spicemine-backup gains the push's pattern - OnFailure= to spicemine-backup-failed, which leaves backup-failed in the data directory (when, and where the reason is); a success removes it. The server is told its path (-Dspice.backup.marker), where backups are on. - Health: a backup check beside offsite (spice/health:+markers+, marker-lines), degraded with the marker's first line. - Mail: on each tick of the mail worker, spice/notify:watch-alarms mails every admin with a confirmed address, kind alarm, once for each failure (a marker with a new first line: a job failing each night mails each night) and once when it works again. What was last told is kept in site_settings (alarm.backup, alarm.offsite; spice/domain:site-setting), so a restart does not tell it twice. No unsubscribe: it is the operator's. English, as the request mail to admins is. The journal says "alarm: backup failed, N admins told", never an address. - MAIL.md, deploy/README.md, FEATURES.md. Tests: - health-test (+8): the backup check absent without its property, ok, failing with the first line, degraded, ok again; marker-lines. - notify-test (+25): nothing without markers; an admin mailed with when, where the reason is and health, no unsubscribe; not a non-admin nor an admin whose address is unconfirmed; not again for the same marker, nor after a restart; again for a new failure; the push on its own; once when each works again. - alarm.e2e (17, new, in the first browser stream): on a running server's own clock - health and mail as the marker comes, changes and goes. The gate's servers name a backup marker and export BACKUP_MARKER. - eval-test: the failure unit and the property where backups are on. - Mutation control: the alarms taken off the clock - 8 failures. Gate: 13975 checks, ALL GREEN (+100). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 04e5c29e3f user: dkords tags: trunk | |
|
2026-11-13
| ||
| 21:22 | 0.152.0 (minor): signing in goes back to the page that sent you there (ticket a31a97263d) Left open by 0.150.0 (GUARDIAN.md s8, "not done"): a visitor sent to sign in by the wall, or by a page that needs an account, landed on their own space afterwards, not on the page they asked for. - The wall and every page that sends someone to sign in (to-sign-in, 25 places in the pages and the zines) carry the page as /login?next=... when it was asked for by GET; a POST goes to /login as before. - The sign-in form keeps it in a hidden field; signing in goes there; a wrong password or a stale form keeps it. Someone already signed in, sent to /login?next=..., goes straight there. - Believed only when it is a page of this site (spice/domain: return-path-ok-p): never //host, /\host or a scheme, nothing outside printable ASCII, at most 400 characters, and never the ways in, the API or a repository's wire (it asks wall-path-class). Anything else, and signing in goes to their space as before. - GUARDIAN.md s8, FEATURES.md. Tests: - wall-test (+27): 21 paths to go back to or not, too long, an extension's page, what /login carries for a GET, a query, a POST and another host. - wall.e2e (+12): members only - sent with the page and back to it; a wrong password keeping it, then the right one going there; six pages neither carried by the form nor gone to (two other hosts' shapes, a scheme, the API, sign-out, a repository's wire), read as the server's redirect so a failure is a verdict and not a timeout; someone signed in sent on. Open: a page that needs an account goes back there too. - Mutation controls: sign-in ignoring the page - 3 failures; the page believed unvetted - 6. - zine.e2e held a stranger's sign-in address to end in /login; it now carries the page, so the path is compared. Gate: 13875 checks, ALL GREEN (+78). One red run first: the zine check above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 33c62fc95e user: dkords tags: trunk | |
| 11:54 | 0.151.0 (minor): nothing offered to a visitor that a visitor cannot have (ticket d39cf20d27) dkords, 2026-11-13: "Unauthenticated-site inconsistencies." An audit of every link on every page a signed-out visitor reaches, under each of the site's three words (0.150.0's wall), found these, and nothing else. Open, as every instance is today: - A space's owner was a link to /ui/<name> even when that account has no space (the first admin, a robot): a 404. A link only when the space exists; else the name. - A members-only project's journal offered "full history" to someone who may not read it. Offered to who may read the project. Walled (members only, or with the showcases shown), signed out: - No search box and no ? in the top bar; the footer is the build alone (its API, about, engines, book and skin links led to the sign-in page); no "More about this engine" and no book panel on the home page. - An (i) keeps its sentence, shown on hover and focus as before, and is not a link (spice/i18n:*gloss-link-p*, bound by def-page). - The honeypot is on the wall's open list, so following it is still judged by the guardian rather than sent to sign in. - GUARDIAN.md s8, FEATURES.md. Tests: - visitor.e2e (28, new, last in its stream; it puts the site back open): under each word every link on every page a visitor has is followed and none answers with the sign-in page, a 401 or a 404 - 106 links on 20 pages open, 19 on 9 with showcases shown, 7 on 3 members only; three exceptions, each named in the suite. What is there open and gone walled; the (i) focused shows its sentence; the owner without a link; the journal's history link by who may read. - Mutation control: the pages not knowing a walled visitor - 4 failures. Gate: 13797 checks, ALL GREEN (+56). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> check-in: 650da8d183 user: dkords tags: trunk | |
| 11:32 | 0.150.0 (minor): the site's wall - an instance for its members, with the showcases still shown when the admin says so (ticket b74f6cfcc7, GUARDIAN.md s8) dkords: "a members only site should still show a showcase if this permission is turned on"; asked which, chose a new site-wide wall. - Migration 0062: site_settings (key, value, who, when). - Settings, admins only: "Who can see this site" - anyone (each project decides; the default, nothing changes), members only with the showcases shown, or members only. POST /settings/site. English and Japanese. - The read rules: with the wall up nobody anonymous reads a project, by a page, the API or a clone (which still gets its 401 and challenge, so a token goes on); a visitor sees a face only where showcases are shown, of what was public or members-only, never private or closed. without-wall, for "is this public among the people here" (a me page's links, carrying a post). - The door: the request gate asks wall-path-class before any route. Open: signing in, up, asking for an invite, mail's links, style and script, robots.txt, /.well-known/, API health and ping. Showcase: /, /ui, a space, a project's showcase, journal, its feed and pictures. A repository's own address is let through to the read rules. The API answers 401; every other page, one nobody listed too, goes to /login. - The showcase tells a visitor the rest is for people signed in; the sign-in page of a walled site says who the site is for. - GUARDIAN.md s8, FEATURES.md. Tests: - wall-test (89, new, sqlite and postgres): the word, refused and kept; reading and seeing for a visitor, someone signed in and the owner under each word, over five kinds of project; without-wall; 61 paths. - wall.e2e (72, new, last in its stream; it puts the site back open): open as before; the setting for an admin only; members only - 22 pages to sign-in, its style, signup and asking open, the API 401 but health, a token through and a bad one not, git refused and then let in with a token, someone signed in as before; showcases shown - home, space, showcase with its note, journal, ten pages still walled, a private project 404, API and git still shut; open again. - Mutation controls: the read rule ignoring the wall - 3 failures, and 3 in wall.e2e; the door taken out - 32 in wall.e2e. Two tests of other releases, found flaking here and fixed, nothing loosened: - notify.e2e waited for a count of all mail to an address, which the digest (every 15 s on the gate's servers since 0.147.0) could meet before the reply it wanted. It waits by kind now. - think.e2e held ONE uneven run to 0.6-1.6 of the steady one. Measured over 25 runs a draw is 2274-3979 ms about 3490, against a steady 3400: under the bound about once in twenty (three gates: ticket caf9598b0c). The average of three runs is held to the same bound. Gate: 13741 checks, ALL GREEN (+322). Two red runs first, each on one of the two flakes above. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> check-in: 9d8c4299d3 user: dkords tags: trunk | |
| 10:12 | 0.149.1 (patch): spicemine's own licence - Apache-2.0, copyright IoTone, Inc. (ticket 8a7915ad73) dkords: "what is the license supposed to be for Spicemine. It seems to be missing ... no top level LICENSE.txt ... no mention in the README ... our code seems to be missing copyright headers"; then "Let's go with Apache 2. The copyright holder is IoTone, Inc."; the year 2026. - LICENSE: the Apache License 2.0 (the official text, by its sha256). NOTICE: the holder, the licence, where the third parties are listed. - README.md: a License section. SBOM.md: how the licence sits with what ships (nothing asks more of it). - Two lines on 282 source files - Lisp, JavaScript modules, Java, shell, the NixOS module, the build files, our own static scripts, bin/: "Copyright 2026 IoTone, Inc." and "SPDX-License-Identifier: Apache-2.0". Lisp after the mode line; shell at the end of the leading comment block, so a script that prints its own first lines prints what it did. - Not headed: the migrations (schema_migrations keeps each one's checksum; a released one never changes), documents, test data, the bundled licence texts, vendored libraries, poc1/, and the two examples the book prints whole (examples.sh holds them to the page). - The distribution carries LICENSE and NOTICE. Tests: - license-headers.sh, new, in the gate (6 checks): LICENSE by its sha256, NOTICE and README name the holder and the licence, every tracked source file of ours carries both lines (282 looked at), and a control - a file with no header is reported. Gate: 13419 checks, ALL GREEN (+6). Two red runs first: the book quoting two examples that had gained headers (fixed by leaving them unheaded), then think.e2e's timing check alone (ticket filed; green on an unchanged rerun). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> check-in: e4ad7737ef user: dkords tags: trunk | |
| 09:28 | 0.149.0 (minor): one Invites extension; "asking in" becomes its request switch (ticket 1a4d412d8c, INVITES.md step 1) dkords: "Asking in is such a strange expression ... 'Invite Plugin' should be something that is available, with all the variants available as flags"; 2026-11-13: "Invites redesign". - The extension ask-in is now invites, "Invites", on by default. What it did is its request switch (option request), off by default. - Migration 0061: an instance whose admin had turned ask-in on has the request switch on, with who turned it on kept. - Its row on Settings > Extensions says what is on in one line and keeps the switch folded under "Change how people get in"; saved by its own form (/settings/extensions/invites, admins only). Requests on without mail: the row says the door stays shut. - /ask and the admins' answer are 404 unless the switch is on. An invite already made from a request still fills its name in at signup after the switch goes off. - spice/ext: extension-option and set-extension-option, an extension's own switches (option.<name>.<key> in extension_settings). EXTENSIONS.md s10. - The words: "Asking in" is gone from the pages - the queue is "Requests for an invite", an invite's note "requested: name (#n)". The extension's word keys are invites-*. - Not in this step, against INVITES.md s5.1: expiry as a setting. - INVITES.md status, ASKING.md, EXTENSIONS.md, FEATURES.md. Tests: - ask-test (118, +22, sqlite and postgres): on by default with requests off; the row's line both ways; options refused (no extension, a bad key, a long value) and kept; no mail said on the row; Invites off beats its switch; the switch off; 0061 never spoken of, off, on, and not turned back on after an admin's no. - ask.e2e (60, +3): the row, nowhere "asking in", the options folded, ticked and saved, unticked again. - Mutation control: the switch ignored - 10 failures, 9 in ask.e2e. Gate: 13413 checks, ALL GREEN (+50). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> check-in: d82a63d1b4 user: dkords tags: trunk | |
| 05:58 | 0.148.0 (minor): mail for follows and appreciations, off unless turned on (ticket 0a7a2c8f01, MAIL.md s7, the last piece) dkords: "let's get going on all of this." - what MAIL.md s7 still had unbuilt. - Migration 0060: notify_prefs.social (0 by default) and notify_social (who was told of what, and when). - spice/social:*after-social*: functions of (what actor org repo post), called on a new follow or a new appreciation, never one taken back. - A new follower is told to the space's admins: the space and how many follow it now, never who. The space's page shows a count and no names, and the mail says no more than the page. - Thanks are told to the post's writer: who, the project, the post's first words, its address. - Once for a person and a thing, so one taken back and given again is not told twice; never one's own; only a person with a confirmed address who may read the project. Past five in a day the rest wait for the digest, as all of it does with one email a day chosen. A digest drops them when the switch was turned off since. - Mail kind notify-social; unsubscribe kind k=social; the digest's k=all turns this switch off too. - Settings > Notifications: "When someone follows a space I run, or thanks me for a post". English and Japanese. - PREFS answers a fourth value; SET-PREFS takes :social. - DELETE-USER removes what was noted for or from the account. - MAIL.md s7, FEATURES.md row. Tests: - notify-test (156, +42, sqlite and postgres): off by default and nothing noted; turned on and kept; a follower counted and not named, its link and kind; the follower not mailed; not twice; one's own; thanks named and quoted with the post's address; not twice; the day's cap and the rest in the digest; daily; switched off since; k=social and k=all; both sides deleted with an account. - notify.e2e (67, +13): the switch off, a follow mailing nobody; on, a follow from the space's page mailed with its count and no name; not twice; a post thanked from the project page, named and quoted; the link's page. - Mutation controls: told again each time - 7 failures; the switch ignored - 10; the hook not registered - 4 in notify.e2e. Gate: 13363 checks, ALL GREEN (+110). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> check-in: 3f7c75daca user: dkords tags: trunk | |
|
2026-11-12
| ||
| 09:57 | 0.147.0 (minor): the daily digest - held answers wait instead of being dropped, and one email a day for everything, by choice (ticket 4f13ebb302, MAIL.md step 5, last part) dkords: "Let's get started." - the last of MAIL.md s9 step 5. - Migration 0059: notify_pending (what waits for an account's digest, as the words it would have said), notify_prefs.delivery (each | daily, each by default) and last_digest_at. - A reply held because the issue was not opened since the last message now waits for the digest; before, it was not sent at all. With "one email a day" chosen, mentions and access given wait too. One item a person a write, as before; at most 500 waiting an account. - Opening an issue clears what waits about it. - Once a day, at spice.notify.digest.hour (8 unless set, the server's clock), each account with something from before then gets one message (kind notify-digest): grouped by issue, an item a line, clipped to 200 characters, forty at most and the rest counted. Each item is checked again when the digest is made: its switch on, the project readable, the issue not opened since. The issues in it count as mailed about. - The digest's one-click link, k=all, turns all three switches off. - spice-mail *on-tick*: functions of the time, on each pass of the worker (the core's counterpart of the :mail-tick slot). The digest is on it. - spice.notify.digest.seconds (a day unless set) shortens the period: the gate's browser servers use 15 s. - Settings > Notifications: "Send them" - each as it happens, or one email a day at about H:00 (server time). The words for held answers now say they wait for the digest. English and Japanese. - Fix: DELETE-USER deletes the account's notification rows, its address confirmations and password resets, and clears it from ask-in decisions. Since 0.145.0 an account with notification settings could not be deleted (foreign keys). - MAIL.md s7 status, FEATURES.md row. Tests: - notify-test (114, +40, sqlite and postgres): the digest time; the first answer mailed, the next two waiting; no digest before its time, one after, its subject, the issue, each answer on its line in order, not the one already mailed, its link; gone once taken, once for that time; opening clears; daily, a mention and a grant waiting and in the digest; 45 waiting, forty listed and five counted; a project no longer readable and a switch turned off dropped; k=all stops all three; an account with prefs, waiting items and a thread deleted. - notify.e2e (54, +10): the delivery choice and its hour in Settings, one email a day saved; a mention not mailed at once, in the digest with its issue, link and k=all link; a held answer, as it happens, in the digest; the digest's link asks to stop all. - Mutation controls: the digest not checking items again - 2 failures; opening not clearing - 4; the tick not registered - 3 in notify.e2e. Gate: 13253 checks, ALL GREEN (+100). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: d8afbe8e85 user: dkords tags: trunk | |
| 07:29 | 0.146.0 (minor): mail when someone gives you access - a grant on a project, a role in a space (ticket e43698742c, MAIL.md step 5, second part) dkords: "Let's move to the next items." - MAIL.md s9 step 5: after replies and mentions (0.145.0), grants. - New hook spice/domain:*after-grant*: SET-GRANT and SET-MEMBERSHIP call it when the account gets access it did not have, or more of it (read to write; member to writer to admin), and only when told who gave it (new :by on SET-MEMBERSHIP; SET-GRANT had one). Each hook is its own failure, never the grant's. Never for access taken away or lowered. - spice-notify ON-GRANT: one message to the account given it - who, what, where and a link to the project or space - if it has a confirmed address, is not a robot, did not do it itself, and has the switch on. Never held. English, as the other notifications. - The API's members POST passes who did it. A grant from an access request (GUARDIAN.md) is mailed as any grant. Space roles have no page yet, so only the API's are mailed. - Settings > Notifications: a third switch, "When someone lets me into a project or a space", on. Saving the form saves all three; SET-PREFS without :grants keeps it. The unsubscribe link takes k=grants. - Migration 0058: notify_prefs.grants, default on. Mail kind notify-grant. - The section's hint now says issues and access. Words in en and ja. - MAIL.md s7 status, FEATURES.md row. Tests: - notify-test (74, +24, sqlite and postgres): a grant mailed with its subject, words, link and unsubscribe; the same again, less, no giver, one's own (in any case), unconfirmed, a robot - nothing; more - told; a role in the space, a higher one told, a lower nothing; the switch off, kept when the other two are saved without it; its link stops them; a failing hook leaves the grant standing. The older checks of PREFS now see three values. - notify.e2e (44, +12): a third person; the switch on; a grant through the API mailed, its link opens the project for them; the same grant nothing; a space role mailed; its unsubscribe page names it and its button stops them; off, an admin role mails nothing; on again from Settings. Gate: 13153 checks, ALL GREEN (+72). The first run was red on one timing check in think.e2e (postgres, "takes about as long", 3478 ms against 1916 ms), untouched here; the rerun was green. - Mutation controls: access mailed when not raised - 5 failures in notify-test; the hook not registered - 3 in notify.e2e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 916ca6959d user: dkords tags: trunk | |
| 06:35 | INVITES.md: design for one Invites extension, the ways in as switches (ticket 1a4d412d8c) dkords: "Asking in is such a strange expression... 'Invite Plugin' should be something that is available, with all the variants available as flags ... Let's write this up as a feature, but come back to it later." Proposed, not built, deferred. Invite links (as today), a shared link, a request form (today's ask-in, with the interview as a sub-switch), instance capacity, member quotas, expiry; later a waiting list, earned invites, a domain door. Six open questions. ASKING.md's status points to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: f4b4a8c9ab user: dkords tags: trunk | |
| 01:06 | 0.145.0 (minor): mail about issues - a reply, a mention (ticket f8c7001d10, MAIL.md step 5, first part) dkords: "start on the next thing too" - MAIL.md s9 step 5: issue replies and mentions first. - New spice-notify.lisp, on spice/issues:*after-write* (new: called after an issue is opened or answered here, each hook its own failure). - A mention to each @name in the new words, outside code and not part of an address; a reply to whoever opened, answered or follows the issue. One message a person a write: named and involved, the mention. - Only an account that may read the project (its rights, not the writer's token scope), with a confirmed address, not a robot, never the writer. Words only: fields alone are not mailed; nor are comments that arrive by push. - The second reply about an issue not opened since the last message is held; opening the issue page lets the next through. Until the digest exists, a held reply is not sent. A mention is never held. - English, as the admins' notice: accounts keep no language. - Settings > Notifications: a switch for answers and one for mentions, both on (MAIL.md s7 as proposed; s10.2 unanswered). Shown while mail is on; asks for a confirmed address first. - /unsubscribe?t=&k=: the page asks, its button stops that kind; a mail program's one-click POST (List-Unsubscribe=One-Click, RFC 8058) stops it with no page and no form token, the key being the authority. - Mail: List-Unsubscribe and List-Unsubscribe-Post on a message that has a link; enqueue takes :unsubscribe; kinds notify-reply, notify-mention. - Migrations 0055 notify_prefs (an account's switches and its key), 0056 notify_threads (mailed, opened), 0057 mail_outbox.unsubscribe. - CSS: a label.check in a form.stack sits beside its box, as label.check always meant; the stack's column rule overrode it (the token form's account-wide box included). - ask-test and mailtheme-test load spice-notify with spice-ui (the first gate run failed on it: spice-ui calls it). - MAIL.md s7 status and particulars, FEATURES.md row. Tests: - notify-test (new, 50, sqlite and postgres): the names an @ means; a mention with its subject, words, link and header; the writer and a robot not told; a reply; held, then released by opening; one message a person a write; a mention never held; unconfirmed not told; fields alone nothing; long words clipped; the switches; the unsubscribe key, a key that is no one's; a private project's non-reader not told, a writer told; mail off, nothing. - notify.e2e (new, 32, in BROWSER_1): two people confirm; the switches, on; a mention with List-Unsubscribe; a reply; the second held; opened, the next; answers switched off; the unsubscribe page changes nothing and its button stops them; one click; a bad key 404 and its page; a phone. - Mutation controls: no hold - 2 failures; no read check - 1; the issue page not marking it opened - 2 in notify.e2e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 1e7178cab6 user: dkords tags: trunk | |
| 00:16 | 0.144.0 (minor): asking in says when no admin can be told of a request (ticket eebb7d21d1) dkords: "No email received in the previous round. I am an admin" - the log showed "ask: request #1 sent" and no ask-admin queued: no admin had a confirmed address, and nothing said so. - The log line counts them: "ask: request #N sent, K admins told". - With none, the queue on /settings/invites and the extension's row on /settings/extensions say so: "No admin has a confirmed address, so nobody is mailed when someone asks." (English and Japanese). - New slot :extension-note (the extension's name): what an extension says about itself on its row while on. - EXTENSIONS.md slot row, ASKING.md s1, FEATURES.md. Tests: - ask-test (96, +5): the log's count; no warning while an admin can be mailed; the row and the queue say so when none can; not on another extension's row. - ask.e2e (57, +1): no warning on the row with the admin confirmed. - Mutation control: the warning never shown - 2 failures in ask-test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 18ee3c6d36 user: dkords tags: trunk | |
|
2026-11-11
| ||
| 23:21 | 0.143.0 (minor): the home page's "Request an invite" leads to asking in, while it is on (ticket eebb7d21d1) dkords: "Let's put the link on the home page if the feature is activated too." - New slot :invite-url: where invite-url points. ask-in fills it with /ask while mail can be sent; the first non-empty filler wins over spice.invite.url (the admin turned it on, later than the deployer set the property), then /help/invite as before. Both of the classic home page's "Request an invite" buttons follow it. - /help/invite shows the door too (the :login-door slot), for a reader who arrives there by an old link. - Off: the buttons go back to /help/invite. - EXTENSIONS.md slot row, ASKING.md s1, FEATURES.md. Tests: - ask-test (91, +3): invite-url without mail, with it, and off. - ask.e2e (56, +4): no door on the home page while off; both buttons to /ask and the door on /help/invite while on; back to /help/invite off again. - Mutation control: the slot answering "" - 1 failure in ask-test, 1 in ask.e2e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 6a0e257c64 user: dkords tags: trunk | |
| 21:31 | 0.142.0 (minor): asking in - an invitation asked for through the Doorkeeper and answered by an admin (ticket eebb7d21d1, ASKING.md step 3) dkords: "Let's get going." - New extension ask-in (ext-ask.lisp), off by default, reserving "ask". - The door: "No account? Ask for an invitation." on the sign-in page, while mail can be sent. /ask takes an address and mails it a link (a day, only its hash kept, a newer one ends the earlier). It answers one sentence whatever stopped it - an account's confirmed address, a request already waiting, an address answered in the last thirty days, a client's fourth request in a day - and says only a bad address, mail off or the queue full. Refusals keep no address. The outcome goes to the log as a word. - The conversation: /ask?t= opens the Doorkeeper, carried in the form as the think tab's is, a turn without a page load. Opening the link changes nothing, so a mail scanner spends nothing. At its summary, "Send my request" (refused before it); the link is then spent. - The admins: every admin with a confirmed address is mailed what the asker is making and a link to the queue, never the asker's address. The queue is on /settings/invites, above the invites: the first answer, the proved address, the notes by question, the name asked for and whether it is free, the conversation folded; invite, decline (with "say nothing") and later. Two answers at once: one wins. - Invite makes an invite noted "asked in: NAME (#n)", mailed to the proved address in the asker's language; the signup fills in the address and the name, and starts confirmed. Decline mails a short message in the asker's language, unless "say nothing". - Thirty days unanswered and a request lapses, from the mail worker's tick, and the asker is told it says nothing about them. - Fifty waiting close the door until an admin answers some. - Migration 0054: ask_requests. - Core: four slots - :login-door, :signup-name (the invite's id), :invites-page, :mail-tick (each mail worker pass); four mail kinds (ask, ask-admin, ask-decline, ask-lapse); signup-page takes a name. - Words in English and Japanese. The Doorkeeper itself speaks English only and says so to a Japanese reader; the admins' message is English, because accounts keep no language yet (ASKING.md says so). - The notes end each answer with a full stop; the name asked for is taken without it (the first run of ask-test caught "tidekeeper."). - ASKING.md status, EXTENSIONS.md slot table, MAIL.md kinds, FEATURES.md. Tests: - ask-test (new, 88, on sqlite and postgres): the door off without mail and the extension off by default; a bad address said; one link, its hash only, expired and superseded links; half a conversation refused, sent once, the link spent; the admin with an address told, not the one without, never the asker's address; waiting, member and limited refused with no address kept, another client and an unknown client not limited; the queue's HTML; later; invite (the note, the address, the language, the inviter, the invites list's message, the name at signup and not for another invite); a second answer refused, and the claim itself; decline quietly and aloud, in Japanese; the lapse at thirty days and not twenty-nine, from run-slot and from the worker's own pass, once, then asking again; the queue full; off. - ask.e2e (new, 52, in BROWSER_2): the whole way in a browser - off by default (404, no door), turned on from the extensions page, the door, the one answer, the link mailed and themed, a scanner's GET, the conversation turn by turn (skip pushed, "everyone" chased, quick replies), the summary, sent, the link spent; the admin's message and queue link, the queue card, later, invite, the invite's message, the signup with address and name filled in, confirmed; a second asker with no name, declined, the message; phones for the door, the conversation and the queue; off again. - Mutation controls: sending before the summary - 24 failures; an account's confirmed address mailed - 2; the lapse at 28 days - 1; the claim without its decided_at guard - 1 (it got through until the claim was checked directly; the sequential pre-check hid it); the name not filled in at signup - 3 in ask.e2e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 74909e088a user: dkords tags: trunk | |
| 07:34 | 0.141.0 (minor): the Doorkeeper, a persona for asking for an invitation (ticket eebb7d21d1, ASKING.md step 2) dkords: "Let's keep going." - New persona doorkeeper.script (group interview, order 33), on the think tab with the others. It opens "Hello. What are you making?" and asks what there is of it already (insisted on once, kept as written), who it is for, why here (insisted on once), whether anyone comes along (just me | one or two | a team), and the username wished for. Vague answers ("stuff", "things", "everything", "everyone", "secret") are chased once. It does not judge; its notes are for the admin who will. English only: its Japanese comes with step 3, :ask-in. - ASKING.md into the repo: the design, now with the status of steps 1 and 2, the questions as built, and the s7 answers as proposed defaults. - The Pop-11 reference (structure.p) joined two answers to a verbatim question with a space; spice-board joins them with U+2028, one line each, as a log's lines. No golden had reached it before the Doorkeeper's (a skip, then the link after insist). The reference now writes U+2028 as its UTF-8 bytes; every other golden regenerates byte for byte. INTAKE.md says so. - THINK.md table row 33, FEATURES.md (33 personas, 39 goldens). Tests: - persona-test (+11): the linter; doorkeeper.golden (8 turns: a skip insisted on, a link, the summary) and doorkeeper-vague.golden (9 turns: "stuff" and "everyone" chased, a skipped why-here insisted on, the name not given), both made by the reference; the agenda checks for "ok" and "I don't know". - think.e2e: 32 chips, the Doorkeeper among them. - Mutation control: dropping the why-here insist from the installed script fails doorkeeper-vague. The space-joined reference failed doorkeeper.golden at turn 7 (U+2028 vs space at character 98). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 63daa47129 user: dkords tags: trunk | |
| 05:57 | 0.140.0 (minor): the invites list - each invite's status and what became of its message, and delete for an unused one (ticket eebb7d21d1, MAIL.md s6) dkords: "What about the invitation status and the "delete" button if we need to delete." - The list on /settings/invites is now the newest fifty invites, used or not. It used to show only the unused ones, expired included, with no way to tell which was which. - Status: waiting until its date, expired and when, or joined, with the account made linked to its space, and the date. - Message: queued, sent and the date, retrying with the relay's words, or not sent and why; "-" when the link was copied by hand. - An unused invite has a delete button (POST /settings/invites/delete). Deleting stops its link working, and a message still waiting in the outbox is deleted with it. It is withdrawn, not failed, so the health check does not count it as given up (the first gate run caught exactly that: "degraded" for two days after a delete). A used invite has no button and is refused: it is the record of who came in through whom. - Migration 0053: invites.mail_id, the outbox row that carries the link, set when the invite is mailed. Invites mailed before 0.140.0 show "-". - CREATE-INVITE returns the invite's id as a second value; new INVITES-RECENT, SET-INVITE-MAIL and DELETE-INVITE. - The heading "Outstanding" is now "Recent invites"; the "expires" column folds into status. Strings in English and Japanese. MAIL.md s6. Tests: - verify-test (+12, 108): the id from minting; the list knows its message and that it is unused; deleted, link dead, gone from the list, its waiting message gone; the health check not degraded by it; deleting again does nothing; a used invite listed with who joined, refused, still listed. - invite.e2e (+9, 43): a used row says joined, links the account, its message sent and dated, no delete button; a hand-made one waiting with its date and no message; a fresh mailed one deleted through its button, the page says so, the row gone, its link no longer valid. Controls: - used invites deletable: 2 failures - the waiting message given up instead of deleted: 2 - the status never "joined": 1 (e2e) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 2c0f8dfb04 user: dkords tags: trunk | |
| 04:57 | 0.139.0 (minor): the mail theme - every message also as a page in the reader's skin, the inviter's name linked to their page (ticket 8f3ef73d43, MAIL.md s3) dkords: the invite arrived "completely unthemed", looking "as if it came from a royal prince overseas"; and "the user name should get linked to the user profile as well". - ext-mail, a new extension, mail-theme, on by default (Settings > Extensions turns it off). It fills a new slot, :mail-html (an outbox row -> HTML). With it, a message is multipart/alternative: the text, unchanged and first, and a page. The page has the host's name over the strip's colours, the words as written, and a link standing alone made into a button worded for the kind ("Make your account", "Confirm the address", "Set a new password"), with the address written out under it. A word followed by " <address>" becomes that word linked. A footer says it is automatic. All styles are inline; no images, no fonts fetched, no script. An account's mail uses its own skin; a newcomer's uses one of the ten picked by the row's id, so a retry keeps it. The button and the footer are in the language the message was queued in. - spice-mail: the outbox keeps that language (migration 0052, mail_outbox.lang). RENDER-MESSAGE asks the slot; when nothing fills it, or the filler fails, the message is the text alone, as before. - The invite's text names the inviter as "ana <address>". The address is their page, /@ana, once they have made one; until then that page is a 404 to everybody else, so it is their space, /ui/ana (SPICE/VERIFY:PAGE-OF). - e2e: one reader for a message, mail-read.mjs, used by verify, reset and invite. - MAIL.md s3 and its status, EXTENSIONS.md's slot table. Tests: - mailtheme-test (new, 40): two forms, the text exactly as written; the newcomer's skin by id and kept on a retry; the account's own skin; the button and its words; the address under it; a link inside a sentence; the name linked and its bracketed address not shown; the words escaped; the inbox preview without the address; no image, stylesheet, script or src; Japanese chrome and lang="ja"; no link, no button; off, the text alone and decoding back; a theme that throws, the text alone. - verify-test (+2, 96): the invite text carries the inviter's space with no page made, and /@name with one. - invite.e2e (+10, 34), on the page rendered in Chromium at phone width: the button and its href, the name linked to the space, which opens signed out, nothing fetched, no sideways scroll, a screenshot. Controls: - the account's skin ignored: 2 failures - the words not escaped: 2 - the language ignored: 3 - the name not linked: 1 - PAGE-OF never choosing /@: 1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: bb42985b87 user: dkords tags: trunk | |
| 02:52 | 0.138.0 (minor): mail, step 4's first half - invites by email: an admin gives the invitee's address, the link is mailed as well as shown, and a signup with that address starts out confirmed (ticket eebb7d21d1, MAIL.md s6) - invites.email (0051): the address the link was mailed to, null for one copied by hand. CREATE-INVITE takes :email; INVITE-USABLE-P and INVITES-OUTSTANDING read it. - spice-verify: MAIL-INVITE queues the message - who invited, the instance, the link on the public URL, the days it is good for; never the admin's note; dropped unsent once the invite has expired. CONFIRM-AS-INVITED: a signup through an invite mailed to an address, giving that address (any case), starts out confirmed and takes the confirmed mark from any other account, as a followed link does. - Settings > Invites: "their email (optional)" while links can be sent; a bad address refused and no invite made; the link still shown once; the page says it was mailed, without the address (a query string is the most-logged part of a request); a "mailed to" column. - Signup: the invited address filled in; with it, no confirmation link and no reminder; with another, that one gets its link as usual. - Not built: spicemine-admin invite --email. The tool runs without the server's settings and does not know the public URL a mailed link needs; MAIL.md s6 says so. - Every new string in the catalog, English and Japanese. MAIL.md status and s6 as built. The rest of step 4, asking in (an invitation requested through a Think interview, approved by an admin), is designed and not built. Tests: verify-test (+15, 94): an invite keeps its address, one by hand none; the message (kind, instance, who invited, the link, seven days, not the note, dropped at the invite's expiry); the invited address in another case confirmed; another address not; none invited, nothing; a second account confirmed by the same address takes it. invite.e2e (new, 24): the field; a bad address refused, no invite; the link shown once and mailed, the page saying so without the address; the list's column; the message the server wrote (subject, the same invite's link on this instance, who invited, not the note); opened signed out - the address filled in, signup confirmed, no reminder, no confirmation link; the link once; another address - the reminder and its own link; an invite by hand as before; a phone. migrate-test: 0051. Controls: any address confirmed by the invite - 2 failures (verify-test), 2 (e2e); the invite never confirming - 3, 3; the message never queued - 6 (e2e). Gate: 12454 checks, ALL GREEN (+78: verify-test 15 and invite.e2e 24, each on sqlite and on postgres). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: fac60128f5 user: dkords tags: trunk | |
| 00:49 | 0.137.0 (minor): an address is required while the instance sends mail - signup asks for one, Settings will not empty it, and every page reminds an account without a confirmed one (ticket f1c2f8f6bc, MAIL.md s4) dkords: operating with accounts that have no email address is a defect anywhere but a local install. It is also what leaves password reset (0.136.0) silent: /forgot gives one answer to everybody, and an account whose address was never confirmed gets no link and no way to know why. - spice-verify: ADDRESS-REQUIRED-P (links can be sent: mail on and a public URL) and NEEDS-ADDRESS-P (NIL, :none, :unconfirmed; robots NIL). SET-EMAIL refuses to empty the address while one is required (:required); with mail off, or no public URL, it is optional as before. - Signup: the field is required (the browser's rule and the server's: an empty or malformed address is refused and said); its hint says it is the way back from a forgotten password. - Every page, for a signed-in person whose account has no address or no confirmed one: a line above the page saying a forgotten password cannot be reset by mail until it has one, linking to Settings. Not on Settings itself, nor on the page a confirmation link opens. Nobody is locked out. - Settings, email section: says where a reset link would go, or that there is none until the address is confirmed; the field is required and loses "leave empty to remove it"; emptying it is refused and said. - Every new string in the catalog, English and Japanese. MAIL.md status and s4. Tests: verify-test (+8, 79): emptying refused while required, the address and its mark kept; with no public URL, removal and its notice as before; :none, :unconfirmed, then nothing once confirmed; a robot never asked; no links, no requirement. verify.e2e (+12, 45): the signup field required; past the browser's rule, the server refuses and says so; the reminder on her space page while not confirmed, pointing at Settings; an account with no address told that; Settings has no reminder and says a reset cannot come by mail yet; confirmed - Settings says where a reset link goes, and no reminder; the Settings field required, emptying refused, the confirmed address kept. auth.e2e: the new person gives an address at signup (43 unchanged). Controls: emptying allowed again - 1 failure (verify-test); robots asked - 1; the signup check dropped - caught (verify.e2e); the reminder never shown - 3. Gate: 12376 checks, ALL GREEN (+40: verify-test 8 and verify.e2e 12, each on sqlite and on postgres). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: d75ca7f24d user: dkords tags: trunk | |
|
2026-11-10
| ||
| 22:25 | 0.136.0 (minor): mail, step 3 - password reset: "forgot your password?" mails a link to the account's confirmed address (ticket 36c64fbc9a, MAIL.md s5) - spice-reset (new), password_resets (0050): one row per request to /forgot, matched or not, so the limits count requests. REQUEST-RESET finds the account by username or by its confirmed address (any case), never an unconfirmed address and never a robot, and mails a link: 32 random bytes, only the hash kept, one use, an hour; a newer request ends the earlier links. RESET-PASSWORD claims the link with a conditional update, sets the password (12 characters at least, as signup), signs out every session on the account, ends every other open link, and mails "your password was changed" to the confirmed address, pointing at Settings for the tokens, which it leaves alone. Limits: three requests an account and ten a client an hour; the client is the guardian's key for the request, stored hashed, and an instance that cannot tell has the account limit only. - /forgot: one answer for every request - "If that matches an account with a confirmed address, a link is on its way" - whether it matched, was unconfirmed, a robot, nobody, or over a limit; the outcome goes to the log as a word, never with what was typed. The page says an account without a confirmed address comes back through the instance's admin; with mail off, it says only that. - /reset?t=: names the account, two password fields, autocomplete new-password (the username for the password manager); the GET changes nothing. Mismatch and too short are said and keep the link; the reset signs this browser out too and lands on the sign-in page saying so. A link that does not work is one answer for unknown, used, expired and replaced. - The sign-in page offers "forgot your password?" when mail is on. - Every string in the catalog, English and Japanese. - MAIL.md status, AUTH.md (email and reset are no longer "out of this pass"), deploy/README.md s5. Tests: reset-test (new, 49, sqlite and postgres): off says so; a name nobody has, an address nobody has, an unconfirmed account by name and by address, a robot with a confirmed address, empty - none, and nothing sent; by username and by address in capitals - sent, its message (kind, instance, account, the link on the public URL, 60 minutes, the outbox row dying with the link), the hash only, the client never stored as given; a newer request ends the older link; the hour; too short refused with the link kept; the reset - the new password signs in, the old does not, both sessions signed out, the token left, used once, the notice (signed out, Settings, no reset link); a reset ends every other link; the account limit and the hour after; the client limit across ten guesses then a real account, another client unaffected, no client known. reset.e2e (new, 27): two accounts through invites, one confirming its address; the sign-in page's link; /forgot gives the same sentence for nobody, the unconfirmed account and the confirmed address in capitals, and only the confirmed account gets mail; the link opened signed out - mismatch and too short refused, then set, on the sign-in page; both devices signed in to the account signed out; old password refused, new accepted; the notice; the link once; a phone reads both pages. migrate-test: 0050. Controls: an unconfirmed address linked - 3 failures; a robot linked - 1; sessions surviving - 1; an earlier link surviving a newer request - 1; no client limit - 1; a link that never expires - 1; /forgot saying "No such account" (e2e) - 2. Gate: 12336 checks, ALL GREEN (+152: reset-test 49 on sqlite and on postgres, reset.e2e 27 on each). The first run had one failure, not this release's: issues.e2e on postgres, a page.fill timing out at 30 s under the full load; the same suites in the same order alone passed (issues 63 ok), and the whole gate rerun is the green one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 717181e1d0 user: dkords tags: trunk | |
| 21:46 | FEATURES.md: Hail Mergey as a designed row (MERGE.md, ticket 29809eadfc) On the list of features to build; not next - mail step 3 is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: e68a612d4a user: dkords tags: trunk | |
| 21:40 | MERGE.md, SPEC.md s14, TRACKERS.md: Hail Mergey - proposals, the what-changed card, ways in for small teams and non-code work, a rules engine, the landing queue, and an opt-in local Laya advisor Design, nothing built; ticket 29809eadfc. Takes up TRACKERS.md s2 to s4 (designed 2026-10-18, never built) and adds: the card (one sentence counted from facts, pictures by kind of file, then the diff); pass the pen, drafts on the table, patchwork and the evening merge; Hail Mergey - a rulebook in the project (.spicemine/mergey), a closed grammar over facts, first match wins, presets, "what would Mergey do?", every decision written into the proposal's file, and what it never does (rebase, land its own rulebook or terms, land on an unknown mergeability except by overwrite, act beyond the role of whoever pressed); check statuses for the queue now that webhooks exist; Laya-MLX on the owner's Mac as a robot account, answering typed questions whose probabilities only a rule can act on. Four decisions for dkords in s10. "Pull request" stays out of the interface (CREATORS.md s6). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 2934cfbacd user: dkords tags: trunk | |
| 20:50 | 0.135.0 (minor): mail, step 2 - verified addresses: a link to confirm the account's address, from Settings and from signup (ticket 36c64fbc9a, MAIL.md s4) - spice-verify (new): users.email_verified_at (0049) and email_verifications - one row per link sent: the address it went to, the token's hash only (32 random bytes), 24 hours, one use. SET-EMAIL saves the address, clears the confirmed mark and sends a link to the new address; when the old one was confirmed, a notice to it with no link, naming the new address masked (a***@example.org); emptied, the address is removed (and the old one told). SEND-LINK sends a fresh link and makes the earlier ones useless. A link is refused once the account's address is not the one it was sent to, however that came about. CONFIRM-LINK claims the row with a conditional update (one of two at once wins), marks the account confirmed, and takes the confirmed mark from any other account with the same address. Limits: five links an account and three an address a day; over them the address is still saved and the page says no link went. - Links are built on -Dspice.public.url, never on a request's Host header (which the asker chooses). The module passes it from publicUrl (https://<hostName> by default); without it, no link is sent and Settings says the address cannot be confirmed. - Settings: a first section, the address with a "confirmed"/"not confirmed" chip, "send the link again" while not confirmed, and the field (empty removes). Signup: the hint says a link will be sent, and it is, after the account is made. - /verify-email?t=: the page shows the address masked and the account, and a button; the GET changes nothing (a mail scanner opens every link). The button's POST, with the page's form token, confirms and redirects to the result; it signs nobody in. - Mail text and every page string in the catalog, English and Japanese. - MAIL.md status (and what was added to s4), deploy/README.md s3d. Tests: verify-test (new, 71, sqlite and postgres): off saves and says so, no row; mail on without a public URL still no links; five refused addresses, header injection among them, none saved; the link sent, its message (kind, subject, account, the link on the public URL, 24 hours, the outbox row expiring with the link), only the hash stored; the page's question changes nothing; the button once; unknown, expired, superseded, address-changed (by set-email and by hand) refused; the notice to an old confirmed address, masked, no link; none for an unconfirmed one; removal and its notice; the address taken by a second account; both limits and the day after; the Japanese subject. verify.e2e (new, 33): signup with an address - the message the server wrote to its mail directory, its link opened in a second, signed-out browser; opening changes nothing, the button confirms, signs nobody in, a reload sends nothing again, the link works once; a bad address refused and said; a changed address - link to the new, notice to the old, masked, no link; send again, the earlier link dead; a phone reads both pages. eval-test (+3): publicUrl as spice.public.url, by default from hostName, set, and absent. migrate-test: 0049. The gate's browser servers now write their mail to a directory (MAIL_DIR) with spice.public.url set. Controls: the address check dropped - 1 failure (by hand); the other account keeping its mark - 1; no limits - 3; earlier links kept good - 1; the notice naming the new address in full - 2; opening the link confirming it - 2 (e2e). Found on the way, by the e2e: the button's POST answered with a page, and the form stream then showed /verify-email without its token - "this link does not work"; it now redirects to the result, as every POST here does. Gate: 12184 checks, ALL GREEN (+211: verify-test 71 on sqlite and on postgres, verify.e2e 33 on each, eval-test 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 55c3e2438d user: dkords tags: trunk | |
| 09:21 | 0.134.1 (patch): mail-setup.sh reads the host's settings in one evaluation, and says so first (ticket 11ea3238f7) On production, step [1] of deploy/mail-setup.sh sat silent for minutes: it read seven settings with host-settings.sh get, and each get is a config_name eval plus a whole NixOS system evaluation. - host-settings.sh get KEY KEY... answers several keys as one JSON object {"KEY": value} from ONE evaluation of services.spicemine (--apply selecting the keys, each checked first); one key still answers its value. snapshot (adopt's before/after, over every managed key) and list use it too - one evaluation where there were thirty-odd. - mail-setup.sh step [1]: prints "reading this host's settings - one evaluation ..., up to a minute or two..." and makes one get for all seven; host-settings.sh's own words if it fails. Gate: 11973 checks, ALL GREEN (+81: mail-test 76 now on postgres too, and the 5 below). Tests: host-settings.t.sh (+3, 56): several keys, one object, ONE evaluation (the stand-in nix logs each), an unmanaged key among several refused. host-settings-nix.t.sh (+1, 24) against real nix: several keys from the real configuration (hostName, projectUrl, mail.enable false, mail.port 587, the password file's default) - and its adopt checks, before and after equal, now through the one-evaluation snapshot. mail-setup.t.sh (+1, 17): one get, and the line said first. Controls: the wizard back to a get per key - 1 failure; host-settings get back to an eval per key - 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 133679270a user: dkords tags: trunk | |
|
2026-11-09
| ||
| 10:40 | 0.134.0 (minor): mail, step 1 - the outbox and its sender, a health check, the module's mail options, and deploy/mail-setup.sh and mail-check.sh (ticket 36c64fbc9a, MAIL.md s9 step 1) - spice-mail: ENQUEUE writes a row to mail_outbox (0048) and returns; a worker sends what is due every 30 s, through the host's sendmail-compatible program (-Dspice.mail.command, run as `-t -i', the message on stdin: Proc.runInput) or into a directory as <id>.eml (-Dspice.mail.dir). Neither set: mail is off, ENQUEUE says so (:off) and no worker runs. A failure backs off (1, 5, 30 min, 2 h, then 6 h) and is given up when the row is two days old, kept with its error; a row past its expires_at is dropped, never sent late; sent and given-up rows go after 30 days. Every header value is checked for CR, LF and control characters before the row is written, and To is one plain address. The message: plain text, UTF-8, base64; the subject RFC 2047 encoded and folded when not ASCII; Auto-Submitted. No address in a log line - the row's id and kind. - -Dspice.mail.from is a bare address and gets the name spicemine (JAVA_OPTS is split on spaces); Name <address> is used as it is. - Health: a "mail" check while mail is on - degraded when a row has waited over an hour or one was given up in the last two days, naming how many and the last error, never an address. - spicemine-admin mail-test ADDRESS queues one message (whatever this tool's own settings: the server sends it); mail-status ID reads it back, exit 0 sent, 1 given up, 3 waiting. - NixOS: services.spicemine.mail.{enable,from,host,port,user,passwordFile}. msmtp is the sendmail program; its config is generated into the store and holds no secret - passwordeval reads passwordFile (default <home>/mail-password, 600, the service user's) at send time. STARTTLS on 587, TLS on 465, the instance's name as the EHLO domain. Assertions: on needs from, host and user; the password file never in the store. A changed relay config restarts the server. - host-settings.sh manages mail.enable, .from, .host, .port, .user, .passwordFile: an address is one address, a port 1 to 65535, the password file never under /etc/nixos. - deploy/mail-setup.sh: asks (each question offering what is in effect), the password with echo off into its file - written beside it and moved, 600, owned by the service user, on no command line and never printed; the settings through host-settings.sh with the switch on the last change only; then mail-check.sh; then looks up the domain's SPF. deploy/mail-check.sh ADDRESS: queues a test as the service user, waits for the server's sender, prints sent or the relay's own words. - MAIL.md (status, and two departures: the password file is in the data directory, since the service is sandboxed from /root; From is a bare address), deploy/README.md s3d. Tests: mail-test (new, 76, sqlite and postgres): off refuses; header injection in an address and a subject refused, forced or not; the message's headers, encoded subject (every word 75 characters or fewer, decoding back) and body; no address logged; backoff, retry times, giving up at two days, never again; expiry dropped and never handed over; prune at 30 days; health ok, degraded when late or given up, ok a week on; the directory transport (no half-written file); a command transport of our own (run as -t -i, the message on stdin, no address in its argv), one that fails (its exit and words kept), one that never reads stdin (no hang). migrate-test: 0048. eval-test (+6): off has no mail flags; on, msmtp, the bare From, the config's host/port/domain/from/user and passwordeval; 465 TLS from the start; no password in the unit; refused without a relay, and with the password file in the store. host-settings.t.sh (+9): the mail values refused and accepted, no password in the file. mail-setup.t.sh (new, 16): the password in its file, 600, and in no setting, command line or output; the settings in order with the switch on the last only; a second run changes nothing; a changed port is the one setting switched; a new password on request; a failed test message fails the run; /etc/nixos refused; no password refused. The gate's rehearsal instance now has mail through a sendmail of ours, and mail-rehearsal.sh (new, 6) runs deploy/mail-check.sh against it: queued by spicemine-admin, sent by the server, -t -i and the message on stdin; a relay that refuses - mail-check fails with its words. Controls: header check removed - 4 failures; expiry ignored - 3; 465 left on STARTTLS - 1; the wizard printing the password - 1. msmtp 1.8.32 read the generated config from a 0444 file with --pretend and took the password from the file. Also: SBOM.md s2 lists msmtp (GPL-3.0-or-later, run, not linked) - the first gate's SBOM check caught the module installing it unlisted, as it should; and run-tests-pg.sh --lisp runs mail-test too (the first green gate showed mail-test only on sqlite). Gate: 11892 checks, ALL GREEN in 8:48 (+113: mail-test 76, eval-test 6, host-settings 9, mail-setup 16, mail rehearsal 6). Then mail added to the postgres Lisp lane and that lane run alone, as the gate runs it: green, mail-test 76 on postgres. surface.lock: admin mail-test, admin mail-status, migration 0048. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 3560c3a7f0 user: dkords tags: trunk | |
| 06:31 | 0.133.0 (minor): the rest of zines out of core - its 59 words and its stylesheet rules are the zine extension's own; DEFEXTENSION takes :style (ticket 33ca91a394, EXTENSIONS.md s9) - Words: the 59 catalog entries only zines use leave core's catalog for ext-zine.lisp's +ZINE-WORDS+, read through :words (0.132.0). Their keys now begin zine- as an extension's must: 47 renamed (zinepub-* to zine-pub-*, zines-* to zine-all-*, the rest zine-<old key>); every use in ext-zine.lisp renamed with them; none used anywhere else. Seven words zines shares with core pages (the drop's refusals, the quota messages, space settings) stay core's. - Style: DEFEXTENSION takes :style, CSS put in every page while the extension is on (SPICE/EXT:EXTENSION-STYLES, after core's). The 28 zine rules - the reader's frame, cards and faces, catalogs and shelves, the drop and its sheet, print - move there from core's stylesheet; a page with zines off carries none of them. The extension's code, as trusted as its Lisp. - l10n/strings.json (gradlew l10nExtract) stays core's catalog only; EXTENSIONS.md says so. Tests: extensions.e2e (+3, 88): zines off - no zine rule in a page's stylesheet; on again - there; a zine word in Japanese from the extension's catalog (the reader's tab on a PDF's page). zine 67, zinedrop 51, opds 66, help 34, i18n 92, i18n-test, zine-test, opds-test unchanged - no [[key]] anywhere. Control: styles of an extension that is off kept - 1 failure. Gate: 11779 checks, ALL GREEN in 9:05. surface.lock: no change but the version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 34fadfcff6 user: dkords tags: trunk | |
| 06:09 | 0.132.0 (minor): an extension's own words and static files, and a third party's viewer - the hello example shows a .hello file as a card (ticket 33ca91a394, EXTENSIONS.md s9) - Words: DEFEXTENSION takes :words, core's catalog shape - (:key "English" "日本語" "note"). Each key begins with the extension's name, so it cannot take one of core's; the Japanese may be "" (the reader gets the English); both languages the same ~a placeholders. One that breaks a rule is an error at registration: a third party's extension is not loaded, and the admin's page says why. spice/i18n:tr and trf read them after core's catalog (found by name: l10n-extract loads spice-i18n alone). - Files: an extension loaded from a directory serves its static/ at GET /lib/ext/{name}/* while it is on, 404 off - stylesheets, scripts, JSON, PNG, JPEG, WebP, fonts, text, typed and nosniff, an hour's cache. Never HTML or SVG (a document of this origin), nothing outside static/, no "..". The extension knows its directory (EXT-DIR), set while LOAD-EXTENSION-DIRS loads it. - For an extension's code: spice/ui:ext-static-url and spice/ui:file-text (a viewer's way to the file's text at the view on screen). - A viewer that is off falls back to core: a text file is its text again; only core's five viewer kinds say "the viewer is off here". - examples/extensions/hello: a viewer for .hello files (a card, its own stylesheet, a line in its own words), its page's heading a word of its own, static/hello.css. Tests: extensions.e2e (+9, 85): with hello on, a .hello file's page is the card - its text, its stylesheet applied (dashed border), its note in English and, with ?hl=ja, Japanese; /hello's heading in Japanese; hello.css 200, text/css, nosniff; its extension.lisp, a %2E%2E path, a path through static/, zine's and an unknown extension's all 404; off - the stylesheet 404, the .hello file core's text again with no card and no "viewer off". ext-test (+14, 49): words found on or off; refused - a key without the name, no note, placeholders that differ, malformed - and none installed; files - its directory known, a stylesheet and a script in a folder with their types, no SVG or HTML, nothing outside static/, nothing missing, none for core's extensions, none off. files 66, pdf 43, model 39, office 24 unchanged. Controls: static files served while off - 1 failure; tr not asking the extensions - 4. Also: webhooks-test's retry clock, the flake of the last two gates (ticket 2a6f47c8b2). The test read *T* before the 503's event was made; under a loaded gate the delivery was due a second after *T*, and process-due :now *T* skipped it - 6 failures. *T* is read again after the event. Proved: a 1.1 s pause there gives the same 6 failures before, none after. Gate: 11773 checks, ALL GREEN in 8:32 (the run before it: the webhooks flake, which led to the fix above). surface.lock: GET /lib/ext/{name}/*. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 1f945e0b1d user: dkords tags: trunk | |
| 05:40 | 0.131.0 (minor): five of the file viewers are extensions the instance admin may turn off - PDF, 3D, archive contents, office words, font specimen - folded under one line on the admin's page (ticket 33ca91a394, EXTENSIONS.md s8) - spice-ext: DEFEXTENSION takes :viewers, ((key . function) ...), the key core's file kind or a name's suffix (a string; the narrower, so it wins), the function (org repo path size) -> HTML; and :group, a heading on the admin's page. VIEWER-FOR is the file page's question; VIEWER-OFF-P tells "turned off here" from "none installed". - ext-viewers.lisp: viewer-pdf and viewer-model (their frame routes /pdf/ and /model/ move here, 404 while off), viewer-archive, viewer-office, viewer-font - on by default, group "viewers". The drawing code stays where it was. - The file page: an extension's viewer for the file first, under the :file-actions slot (zines' reader, offered whether the PDF viewer is on or off); an installed viewer that is off - the file's name, size and download, and a line saying the viewer is off on this site. Pictures, sound, video, text, Markdown, tables, notebooks, diagrams and shaders stay core's. - A commit's page shows an office file's words as its change only while that viewer is on. - /settings/extensions: a group folds under one line, "File viewers 5 of 5 on", open when one is off. GET /api/v0/admin/extensions: each extension's group and viewers. - Words: 3, English and Japanese, each with its context. Tests: extensions.e2e (+16, 76): the five in the API, grouped, on, saying what they show; the admin's page folds them closed with "5 of 5 on", unfolded to switch the PDF viewer off - open, "4 of 5 on"; the PDF's page then says so, draws no frame, keeps its download and the zine reader, the frame 404, the zine frame still 200; on again; a zip's contents listed, gone with the archive viewer off; an STL drawn, gone with the 3D viewer off and its frame 404. ext-test (+7, 35): a suffix core has no kind for, a suffix over a kind, an off extension's skipped, a dot in a directory no suffix, off told from never installed. pdf 43, model 39, office 24, files 66, history 365, zine 67, opds 66 unchanged. Control: VIEWER-FOR ignoring the switch - 3 failures. Not run in the off state: a commit page's office words (the office suite runs with the viewer on). Gate: 11727 checks, ALL GREEN in 8:41, with production's ImageMagick (a run before it without MAGICK exported skipped the HEIC legs and said green - caught by the per-suite counts; one between them lost 6 webhooks-test checks on postgres to its retry clock under load, 59/59 three times alone after: ticket 2a6f47c8b2). surface.lock: no route added or gone - the two frame routes the same paths, now an extension's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 0cbcd27e89 user: dkords tags: trunk | |
|
2026-11-08
| ||
| 02:02 | 0.130.1 (patch): the zine code moves out of spice-ui into ext-zine.lisp, behaviour unchanged (ticket d9935635dc, EXTENSIONS.md step 4) - ext-zine.lisp: the reader and its frame, the shelves and catalogs, the drop and its order check, the phone's form and its post, the OPDS pages' helpers, the slots zines fills and DEFEXTENSION :zine - 590 lines out of spice-ui.lisp, whole. Its routes are ADD-ZINE-ROUTES, the extension's :routes, made at boot by ADD-EXTENSION-ROUTES like a third party's; add-ui-routes no longer names one. - Still in SPICE/UI's package (it uses the page frame and the repository helpers as before); loaded after spice-ui, before api-v0 (boot.lisp, compile-all.lisp). - What stays in core and is zines': the stylesheet's zine rules, the page-feed macro (guarded by the extension since 0.128.0), the frame tickets and the vendored pdf.js and pdf-lib versions, which the PDF viewer shares. Tests unchanged: zine 67, zinedrop 51, opds 66, extensions 60, help 34 - before the gate, against the moved code; surface.lock the same routes. Gate: 11681 checks, ALL GREEN in 9:21. The creep alarm named the lisp suites, guardscore, editor and branches, on both databases - every lane about 40 s slower alike, none of them touched by this change; at the time a jq (the session hook) and Spotlight were each taking a whole core. Noted on ticket 2a6f47c8b2, thresholds as they are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: aa34a2d3b1 user: dkords tags: trunk | |
| 01:46 | 0.130.0 (minor): a third party's extensions, loaded from a directory - with a worked example, and the deployer's two host settings (ticket d9935635dc, EXTENSIONS.md step 3) - -Dspice.extensions.dir names one or more directories, colon-separated. At boot, after core and before the routes are made, each subdirectory's extension.lisp is loaded, in name order. One that fails to load, or speaks another extension API, is logged and skipped - the instance starts without it, never not at all - and the instance admin's /settings/extensions lists it under "Not loaded" with why; GET /api/v0/admin/extensions says the same in "failed". - DEFEXTENSION takes :routes, a function of (app) called once at boot; its routes go through EXT-PAGE / EXT-ROUTE / EXT-POST, so they 404 while the extension is off. - spice/ui exports what an extension's code needs, and no more: ext-page ext-route ext-post page not-found current-user r csrf-field redirect repo-base. - An extension's reserved names are the site's roots to the guardian too: /hello/... is the site's page, never read as a wire mount. - examples/extensions/hello: the worked example - a page at /hello, a line on every showcase (the :showcase-body slot), a home page of its own (the :home role); installed off. - The NixOS module: extensionsDir (a string, not a nix path: the directory stays on the host, out of the world-readable store) and extensionsOff (names, comma-separated: off for good, locked on the admin's page). Both through deploy/host-settings.sh like every host setting - absolute directories, none inside /etc/nixos; names in the form an extension's is. - EXTENSIONS.md s6 rewritten: what core offers an extension, how it loads, and the deployer's two commands. - The gate's servers load examples/extensions and core/src/test/extensions (a broken one, and one for API 2). Tests: extensions.e2e (+13, 60): hello installed from the directory, off as it says, providing home, reserving hello; the broken one and the one for API 2 not installed and the instance up; both reported with why, in the API and on the admin's page; off - /hello the site's 404, no greeting on a showcase; "hello" refused as an account; turned on from the admin's page - its page in core's frame, its line on the showcase, a home the admin may pick; picked - a visitor's / is its home page; off again - / back to the classic home, /hello 404. ext-test (+1, 28): the guardian reads a reserved name's paths as the site's own. host-settings.t (+3, 44): a relative extensionsDir, one inside /etc/nixos, and a malformed extensionsOff refused. eval-test (+2): both settings in spicemine-host.json reach the unit; with neither, no extension flag. Control: the extensions' routes never made - 1 failure. Gate: 11681 checks, ALL GREEN in 9:27 (the nix suites ran: deploy/ changed). surface.lock: no change but the version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: ecc33b335b user: dkords tags: trunk | |
| 01:26 | 0.129.0 (minor): the instance admin's extensions page - each installed extension on or off with one button, and the home page picked (ticket d9935635dc, EXTENSIONS.md step 2) - /settings/extensions, the instance admin's alone (anyone else: the site's 404; /settings links it for the admin only): each installed extension as a card - its title, name, on / off / off for good, its summary, the names it reserves, one button to switch it, and who last set it and when. One the deployer turned off (spice.extensions.off) says so and has no button. The home page: the enabled home extensions to choose from, the chosen one marked; with none on, a line saying / shows a visitor the spaces. - Under /settings, not /admin as EXTENSIONS.md first said: /admin is not one of the site's roots the guardian knows, so a two-segment /admin/... would be read as a wire mount. The doc says so. - spice-ext: EXTENSION-CHANGED, the admin's last word on one - by whom, when. - The shelf's face frame (0.127.1) is transparent to its edges: the frame's own dark ground showed as bars beside a page narrower than the card. - Words: 19, English and Japanese, each with its context for the translator. Tests: extensions.e2e (+12, 47): an instance admin signed in on a phone; settings offers "extensions" to them and not to the owner, whose /settings/extensions is a 404, nor to a visitor; the page - zines on with its reserved names and "turn off", home-classic marked the home page, the picker on classic, nothing sideways at 390 px; turned off from the page - "Saved.", the row off, set by this admin, "turn on", /zines 404; on again from the page. Control: the page open to any signed-in person - 1 failure. Gate: 11648 checks, ALL GREEN in 9:00. surface.lock: GET /settings/extensions, POST /settings/extensions, POST /settings/extensions/home. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 1016b7238d user: dkords tags: trunk | |
| 01:08 | 0.128.0 (minor): extensions - zines and the home page become features an instance admin turns off, and a deployer can add their own (ticket d9935635dc, EXTENSIONS.md step 1) dkords: "let a 3rd party deployer / integrator create their own features, and even disable features. Zine fits well as an add on feature, maybe available by default but with a way for an admin to deactivate. Let's also make the home splash page as a plugin module that can be changed." - spice-ext: DEFEXTENSION (name, title, summary, the extension API it speaks, its default, the names it reserves, the slots it fills, the roles it provides, its OpenAPI paths); one speaking another API is refused at install. Three switches, in order: the deployer's -Dspice.extensions.off=a,b (off for good: shown as locked, the admin cannot turn it on), the instance admin's word (extension_settings, migration 0047, with who and when; kept across restarts), the extension's own default. - Off is absent and nothing is lost: an extension's routes (EXT-PAGE / EXT-ROUTE / EXT-POST) answer the site's 404; its slots - named places in core's pages - render nothing; a form saved while it is off does not touch its settings; its API paths 404 and leave the OpenAPI document; the names it reserves stay reserved (SLUG-AVAILABLE-P asks every installed extension). - Zines are the first extension, on by default: the reader, the frame, /zines, every /opds catalog, the order check, the drop and the phone form behind the guard; the showcase opening and body, the space's shelves, a file page's "read as a zine", the showcase and space toggles and their saves, the setup view's block, the page feeds - each a slot zines fills. - The home page is a role (EXTENSIONS.md s5): / for a visitor is the chosen home extension's. home-classic is the landing page as it was; with no home on, / is the space list - a working instance, never a blank page. - GET /api/v0/admin/extensions, PUT /api/v0/admin/extensions/{name} {enabled}, PUT /api/v0/admin/home {extension}: the instance admin's (403 to anyone else), in the OpenAPI document. - surface.sh reads the extension route macros, so an extension's routes are on the surface as before. - Found on the way: history.e2e took "today" once, at the start, in UTC; a gate run across midnight UTC saw tomorrow's heading. Either day now. - spicemine-admin loads spice-ext before spice-domain, as it loads spice-migrate: the name check is read when spice-domain is. Not yet (EXTENSIONS.md s7): the admin's page (0.129.0); third parties loaded from a directory, with an example and the NixOS option (0.130.0); the zine code moving out of spice-ui. Tests: ext-test (new, 27, sqlite and postgres): install order; defaults; a slot's pieces in install order, an off one's gone, run-slot skipping it; the admin's word read back after the state is forgotten, with who; off for good over the admin's on, refused; reserved while off and the domain's check saying so; another API refused; a second install replacing in place; the home - unchosen, chosen, the chosen one off, an off one or a non-home refused, none at all. extensions.e2e (new, 35): the admin's list - zine on, default, unlocked, its reserved names; home-classic the home; 403 to an owner, 404 a name not installed, 400 a non-boolean; on - nine zine routes 200, the showcase and space catalogs; off - the nine 404, no shelf, no feed link or header, the README there, the PDF still a file without "read as a zine", the toggles and the setup block gone, the OpenAPI document without the zine paths, the name "zines" still refused; on again - the catalog back, kept through a showcase save made while off; the home - the landing page, zine refused as a home, classic off and / the space list, an off home refused, on and chosen, the landing page back. Controls: slots ignoring the switch - 6 failures; the routes ignoring it - 1; a save calling an off extension - 1. Gate: 11624 checks, ALL GREEN in 8:10. surface.lock: migration 0047, GET /api/v0/admin/extensions, PUT /api/v0/admin/extensions/{name}, PUT /api/v0/admin/home. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 00ba4ee0b8 user: dkords tags: trunk | |
| 00:20 | 0.127.1 (patch): a zine with no cover picture shows its own first page on the shelf, and the drop makes that page its cover (ticket 9c306cf5bb) Reported on a space with two zines: "neither render the PDF contents". The readers drew (checked in chromium and webkit, ink on the canvas, phone and desk widths); the shelf never did - a card showed the picture uploaded with the zine, else a grey book. A zine published from the phone form or dropped without a picture was a grey book everywhere it was listed. - A card with no cover picture holds the reader's frame in face mode (/zineframe/<path>?face=1): the zine's first page as its layout reads it, no controls, no pages after it, nothing the reader kept; loaded lazily, and it takes no pointer, so the card stays one link to the reader. Fixes the zines already published, with nothing re-published. - The drop sheet, at publish with no cover picked, asks its frame for that first page as a PNG and sends it as the cover - so the shelf, the API's cover_url and the OPDS catalogs show the zine itself. With the reading copy off too. Tests: zine.e2e (+3, 67): the coverless mini zine's card has the face frame and the covered one does not; the face draws MPAGE1 inked, one panel, no bar; a click on the face goes to the reader. zinedrop.e2e (+3, 51): no cover picked - the first page became cover.png, a portrait PNG the shape of one mini-zine panel; with the copy off, still a cover. Controls: the face drawing nothing - 1 failure; the drop never making a cover - 3. Gate: 11500 checks, ALL GREEN in 8:10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: dee55ec9f4 user: dkords tags: trunk | |
|
2026-11-07
| ||
| 12:47 | 0.127.0 (minor): a reading app that signs in reads its account's private shelves - Authentication for OPDS, Basic with a token (ticket 38510a63f9: OPDS.md step 5) - Every catalog takes a credential: HTTP Basic with the account's name and a token - the pair the API and the wire mount take - or a Bearer token. With a valid one, the feeds hold what that account may read, as the token's scope allows: the public shelf, and the zines of every project in the account's spaces or granted to it that it may read (at most 300 projects). A token scoped to one project sees that project's shelf alone - CAN-READ-REPO-P asks the scope. - A credential that is not valid: 401 with the sign-in document, so the app asks again. No credential: a stranger, as before - and a private project stays a 404, never a 401: a catalog does not say a private project exists. - GET /opds/me: the account's shelves, a group to each project with zines, public and private; 401 to a stranger. The root, signed in, offers it as "Your shelves". - GET /opds/auth: the Authentication for OPDS document (application/opds-authentication+json) - http://opds-spec.org/auth/basic, labelled "account" and "token" (never the password: a token can be scoped and revoked), a help link to /settings#tokens. It needs no credential, as the draft requires; the root links it (rel http://opds-spec.org/auth/document); every 401 carries it with WWW-Authenticate: Basic. - A private zine's files are linked through the API's raw route, which takes the same credential; a public one's through the page's, as before. - Signed-in answers are Cache-Control: private, no-store; every catalog answer says Vary: Authorization. - `auth` reserved beside opds and zines (/opds/auth). Tests: opds-test (+1, 143): auth is reserved. opds.e2e (+13, 66): /opds/me without a credential - 401, Basic asked, the document with its basic type and "token" label; the root names the document; /opds/auth open; a credential that is not valid, 401; signed in, the press and the private drawer; private, no-store, Vary; the drawer's file through the API, fetched with the credential as a PDF and not without; the drawer's feed there signed in and 404 to a stranger; a token scoped to the press sees no drawer; "Your shelves" on the signed-in root and not on a stranger's. Controls: the token's scope dropped, signed-in answers publicly cached, a bad credential taken for a stranger - one failure each (3). OPDS.md steps 1-5 are built; item 6 (EPUB, CBZ, covers from page 1, position sync) waits for a reader app to ask. Gate: 11488 checks, ALL GREEN in 8:25. surface.lock: GET /opds/auth, GET /opds/me. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 13dde2edf8 user: dkords tags: trunk | |
| 12:30 | 0.126.0 (minor): a person's public list is an OPDS catalog (ticket 38510a63f9: OPDS.md step 4) "Users publish catalogs" without a catalog editor: the shelf is the project, the curation is the list (0.95.0). - GET /opds/@{name}/lists/{slug}: each project on the list that has public zines is a group of them, in the list's order, titled by the project, the curator's note as its subtitle, with the project's own feed as the group's self link. An address on the list that is another catalog - /opds a whole segment of its path, or a name ending .opds+json - is a navigation link to it, so a list spans instances; decided from the address, never by fetching another host. A private list, and a private or closed project on a public one, are absent - by LIST-ITEMS' rule and the public shelf's, and nothing is said of them. A list with nothing to show answers with the way back to /opds. - The list's page names its catalog in the head and a Link header when the list is public. Found on the way: the first test of "is this address a catalog" took /blog/opds-is-nice for one; the path is now read by segments. Tests: opds-test (+14, 142): addresses that are catalogs and ones that are not (a page with opds in its name, ftp); a list's title, self and page; a group to each public project in order with the note; the private project, the closed one and the private project's note absent; another catalog linked, a blog not; a private list no catalog. opds.e2e (+9, 53): a list made over the API, found from its page's head and Link header, followed: the press as a group with the note, the private project absent, the other site's catalog linked and the blog not; made private, 404. Controls: private lists served and every address taken for a catalog - 2 failures. Gate: 11460 checks, ALL GREEN in 8:50. surface.lock: GET /opds/@{name}/lists/{slug}. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 99edee868d user: dkords tags: trunk | |
| 12:14 | 0.125.0 (minor): a series reads as a run on a zine catalog (ticket 05e8d9041d, the brief's look and feel) On a project's catalog showcase and a space's catalog, each series' issues now sit together under the series' name, numbered ("No. 4" before the title), the newest number first; the zines of no series follow, as they come. The order is the issue number the creator gave (0.124.0); an issue with none goes last in its run. Outside catalog mode the shelf is as it was. Tests: zine.e2e (+2, 64): two issues of one series on the catalog under its name, No. 2 before No. 1; the zines of no series after the runs. Control: issues sorted oldest first - the run check fails. Gate: 11414 checks, ALL GREEN in 8:14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: ac095137a9 user: dkords tags: trunk | |
| 12:01 | 0.124.0 (minor): a zine's series, language, date and pages - in the publishing forms, its JSON, the API and the catalogs, with a language facet (tickets 38510a63f9, 05e8d9041d: OPDS.md step 3) - Both publishing forms (the phone's zinepub and the catalog's drop sheet) fold "more: series, language, date" under the title and the line: the series, the issue's number, the language (a tag: en, ja, pt-BR), the date it came out. Each optional. - The drop sheet's order check already counts the zine's pages as its layout reads them; it writes that count into the post (a hidden field), so the JSON says how many pages without the creator typing it. - The JSON beside a zine: "series": {"name", "position"} (or a bare name, no number), "language", "published", "pages" - each written and read only in its shape: a language that is not a BCP 47 tag, a date that does not exist (2026-02-30), pages that are not 1-10000 are not written, and not read when hand-written. - OPDS publications: belongsTo.series [{name, position}] beside the project's collection, language, published, numberOfPages. /opds/zines has a language facet (each language said, counted; a zine that says none is in none), and ?language= filters, with ?layout= or alone. - The API's zine objects: series, series_position, language, published, pages (spice-openapi). Not yet: a series shown as a run on the catalog shelf (the brief's "series read as runs"). Tests: zine-test (+15): tags, dates, the JSON with all four and with none in shape, read back from a repository, a bare series name, the rest unsaid when not their shapes. opds-test (+15, 128): a publication's series and number beside its project, its language, date and pages; none for a zine that said none; the language facet, followed, both filters together. zinedrop.e2e (+3, 48): the order check fills the pages (8); the folded fields written to the JSON and the API; the catalog entry says them. Controls: any language accepted - 6 failures; the series left out of the document - 1. Gate: 11410 checks, ALL GREEN in 8:10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: b04e52eb44 user: dkords tags: trunk | |
| 11:46 | 0.123.0 (minor): OPDS search and the layout facet (tickets 38510a63f9, 05e8d9041d: OPDS.md step 2) - Every feed links search as a template, {"rel": "search", "href": ".../opds/search{?query}", "templated": true}, the way OPDS 2 asks; GET /opds/search?query=Q answers with the public zines whose title, blurb, project or space hold every word of Q (letters and digits, any case), counted, at most 100. No words, or nothing found: a feed still, with the way back to /opds. Nothing private or closed is found - the search reads the same public shelf the feeds do. - /opds/zines has a layout facet when the shelf has more than one layout: a link to each (booklet, mini8, spreads, pages, grid, "as it comes"), each with numberOfItems; /opds/zines?layout=L lists that layout's, its facet link marked self, its own self link and its paging keeping the layout. A layout nobody has filters nothing. The language facet waits for the language key (OPDS.md step 3). Tests: opds-test (+32, 113): the template on the root, a space and a project; search by title, by every word of the blurb, by project, by space; a missing word leaves none; never a private or closed zine; no words, the way back and no count; the facet's layouts and counts; following it, only booklets, counted, marked in force, self keeping the layout; an unknown layout no filter; the next page keeping the layout. opds.e2e (+6, 44): the root's search template filled in as an app fills it - found by the blurb's words, a feed when nothing is found, a private zine not found by its space's name; the facet followed, as many as it said. Controls: a search that ignores its words and a facet that does not filter - 7 failures. Gate: 11344 checks, ALL GREEN in 8:08. surface.lock: GET /opds/search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: d38f43d68c user: dkords tags: trunk | |
| 11:29 | 0.122.0 (minor): OPDS 2.0 catalogs of the public zines - /opds, /opds/zines, a space's, a project's, one publication - found from the pages that show them (tickets 38510a63f9, 05e8d9041d: OPDS.md step 1, the brief's step 4) A reading app, an e-reader or another site can now subscribe to what is published here. Read-only and anonymous: the zines a stranger may read (public projects at exposure open), from the public shelf /zines already keeps - a feed asks no repository anything. - spice-opds.lisp (new): the documents as data, written by spice/json; every href absolute (the request's scheme and host). O drops a blank value, so nothing in a document is null or "" (the spec: no blank values). - /opds: navigation to every zine and to each space with zines (by its name), and a group of the first dozen as publications, so an app shows covers at once. - /opds/zines?page=N: fifty to a page; numberOfItems, itemsPerPage, currentPage; first, last, previous, next. - /opds/{space}: its zines; a group to a project, with the project's own self link, when more than one. A space with none answers with the way back to /opds (a feed must have a collection). - /opds/{space}/{project}: one project's shelf. Not on the public shelf - private, closed, at exposure showcase, or not there - is the same 404. - /opds/pub/{space}/{project}/{path}: one publication (application/opds-publication+json). Title; the blurb as description; Periodical under zines/, else Book; the reader page as identifier; the space as publisher, the project as belongsTo collection; the reading copy (0.121.0) then the print copy as open-access acquisitions, both application/pdf; the reader page as alternate; the cover as the picture itself, with its own type. Not the thumbnail: it answers JPEG only when it made one, and a feed must not state a type it may not be (opds.e2e found a small PNG cover served as image/png). A reading copy is not a publication of its own. - Discovery: /zines, a space's page and a project's showcase - when they have public zines - name their catalog in the head (<link rel="alternate" type="application/opds+json">) and a Link header (WITH-PAGE-FEED, *page-feeds*). - `opds` and `zines` reserved for people and spaces (/opds/zines is every zine) and as the guardian's roots: a catalog is metadata, never judged as source; its downloads are the raw route's and judged there. Project names are not affected - a space's `zines` project still is one. - The public shelf is remembered per project now, and which projects are public is asked of the database every time (cheap), so a project closed or made private leaves /zines and every catalog at once. An event that changes what a repository holds or who may see it (visibility, exposure, delete, push, upload, edit, zine, import, adopt) forgets that project's zines alone, through *event-hooks*. The first build of this forgot the whole shelf on every event - found by opds.e2e, where the space page's catalog check had filled it just before the suite closed a project - and the gate's new creep alarm named the cost at once: after any push, every page that asked "has this a catalog?" rescanned every public repository, and aiok, lists and access went from 6-8 s to 21-32 s. One project at a time, they came back (gate line below). Also: mirrors.e2e waits for its sentence after the add form, not for a load that a Datastar form never makes (9 red gates in the log). zine.e2e counts its own space's `press`, not any space's - the fresher shelf showed the other suite's. Tests: zine-test (+5): each public project's zines kept, a private one's never; a push forgets that project and only it; an unrelated event forgets nothing; an event naming no project forgets all. opds-test (new, 81, on postgres too): every feed - the root, every zine, two pages of two, the space with one project and with two, a project, an empty space - has a title, a self link, a collection, titled navigation, nothing blank; every publication a title, an acquisition, images of the allowed types, nothing blank; Winter's metadata, its two acquisitions in order, its cover, its alternate; a zine with no JSON; a name outside ASCII percent-encoded; no publication for the private, the closed, or a reading copy; the names reserved. opds.e2e (new, 38, BROWSER_1): from /opds by following only the links each document gives, to the space, the publication, both PDFs and the cover, each fetched by a stranger as the type it said; paging to the end and past it; the private, closed and showcase projects and their publications 404; the head and the Link header on /zines, a space and a showcase, none on a page with no public zines; nobody may be called opds; a reading app walking a feed 25 times answered every time. Controls: no print acquisition and no blank filter - 8 failures; the shelf's event hook removed - the space feed showed the showcase project's zine; every event forgetting the whole shelf - 2 failures. Not yet (OPDS.md): search and facets (step 2), series, language and dates (3), lists as catalogs (4), private shelves with a token (5); X-AI-Policy on feed responses. Gate: 11268 checks, ALL GREEN in 8:11 (0.121.1: 8:27). aiok, lists and access: 5-8 s again (21-32 with the whole-shelf forgetting). surface.lock: the five /opds routes. Still named by the alarm: guardscore-test on postgres, 57 s against a usual 15 (ticket 2a6f47c8b2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 7e22ce257f user: dkords tags: trunk | |
| 10:50 | 0.121.1 (patch): the gate's time - a creep alarm, the nix suites kept while deploy/ is unchanged, a third of the processes a page started, and the hg sidecar flake found (ticket 2a6f47c8b2) The gate had gone from 7.7 minutes (0.96.0) to 10.8 (0.121.0). Measured, not guessed: the CPU-heavy browser suites ran about twice as long in the gate as alone (history 60 s alone and 110 in the gate, chronicle 24 and 57) with no check added since 0.99.0; Flight Recorder on a server running three suites found it mostly idle - 222 CPU samples in about 100 s - and starting 2353 processes, 1273 of them `git rev-parse` asking whether HEAD names a commit. Process starts, many lanes of them at once, are where the time went. - A. The creep alarm (gate-clock.mjs): every step and suite against the median of its own last ten green runs; 25% slower and SPICE_CREEP_MIN seconds (10) is named under "-- creeping", after allowing for checks it gained (each suite's ok lines are now logged, level suite-checks); the whole gate against SPICE_GATE_BUDGET (660 s). Named, not failed: a gate's times are the machine's as much as the code's. Its first run named the Lisp suites 50% slow; the cause was outside the code - a hook of the operator's running jq over a long transcript beside the gate - found by looking at what was running once the alarm pointed. - B. suite_cached (gate-lib.sh): the nix evaluations (nix eval, host settings against real nix: 4 minutes of CPU beside the browser lanes) keyed on a hash of every file under deploy/, nix's version, nixpkgs' store path and gate-lib.sh itself; a pass is kept 14 days and printed again, saying when it ran and its key; a failure or a skip is never kept; SPICE_GATE_FRESH=1 runs everything. - C. Fewer processes per page (spice-scm): whether HEAD names a commit, the default branch's name, and a tip's hash are read from the repository's own files - HEAD, the loose ref, packed-refs - and from git only when those are not plain (a reftable store, a HEAD it does not read, any file surprise); tree listings (ls-tree -r, names and sizes) are kept by commit hash, which can never go stale, a copy to each caller. *proc-count* counts %proc's starts for tests. The same three suites: 2353 processes -> 816; alone, chronicle 25 s -> 15, zine 22 -> 19. - The hg sidecar flake (0.120.0's and today's red gates): health-test started the sidecar on a fixed port, 42991, and the gate runs the Lisp suites on sqlite and postgres side by side - two sidecars, one port, one could not bind. Now a port the system says is free. Shown: two old copies at once, 1 failure ("hg serve" could not start); two new, none. Not done, on purpose: pinning cores. macOS has no affinity, and the evidence is process starts, not JVM threads; -XX:ActiveProcessorCount would have answered a question nobody asked. Tests: gate-clock.t.sh (+6): per-suite checks logged; as fast as its record, nothing said; twice the time, the same checks, named with how much; twice the time with twice the checks is not creep; over budget said; under SPICE_CREEP_MIN is noise. gate-lib.t.sh (+8): runs the first time; the same inputs not run again, said so, lines printed; one byte changed, run again; another tool version, another key; a failure fails and is never kept; SPICE_GATE_FRESH; a skip not kept. branch-test (+21): HEAD from the files against git's rev-parse in every shape (empty, loose ref, packed, a missing branch, detached, unreadable -> git), no process for the common case; the default branch's name from HEAD; the tip hash git's own; a tree listed again without a process; a caller's sort does not reach the kept list; a new commit lists the new tree; sizes too; packed refs still read. Controls: the cache keeping failures - 3 failures; packed-refs ignored - 1; the memo without a copy and without the hash in its key - 8 (the git and jj branch checks among them). Gate: 11020 checks, ALL GREEN in 8:27 (0.121.0: 10:48), on the third run; the first two lost the mirrors form race (a known flake, 9 times before) and the hg sidecar port, fixed here. The nix suites came from the cache. Still named by the alarm: guardscore-test on postgres, 54 s against a usual 13 - next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 253e560481 user: dkords tags: trunk | |
| 09:27 | 0.121.0 (minor): a zine's reading copy, made in the creator's browser with pdf-lib, and "download to read" (ticket 05e8d9041d, the brief's step 3) A zine laid out for the photocopier downloads as sheets: a booklet reads 8, 1, 2, 7 on a screen. Now, when a zine is published by dropping it on a catalog, the creator's browser also makes the pages in reading order, one to a PDF page, and the same post commits it beside the print copy: - pdf-lib 1.17.1 (MIT, approved 2026-11-06) vendored at static/pdf/pdf-lib.min.js: the ES module build, unmodified, checksummed (pdf-lib.sha256), integrity checked against the npm registry's sha512; static/README.md and SBOM.md. It runs only inside the sandboxed check frame /-/zinecheck. - zine-frame.js, check mode: keeps the dropped bytes; asked at publish, copies each zine page out of the imposed PDF - clipped to its panel, a flipped panel turned 180 degrees - onto a page of its own, vector to vector, in the layout the creator settled on. A page carrying its own /Rotate is refused, and the zine is published with the print copy alone; so is a plain-pages zine, whose print copy already reads. - zines/<slug>-read.pdf, named in the JSON's new "reading" key. It is not a zine of its own: the shelf leaves out a -read.pdf beside its zine (reading-path-p). - The reader: "download to read", the quiet button beside "download to print". The API's zine objects: reading, reading_url (spice-openapi). - Showcase form: "Make a reading copy", on by default (migration 0046_reading_copy.sql, showcase.reading_copy: NULL on, 0 off); the drop sheet knows each project's choice. - ctx-files carries each file's form field (the print copy and the reading copy are both PDFs). The two small things from 0.120.0: the check frame is sized to its thumbnails, so no row is cut off; cancel is the quiet button. Two bugs found on the way, both fixed: - The sheet's form was sent twice. Every POST form is Datastar's (+stream-script+), which posted the moment publish was pressed - before the reading copy was in it - and the sheet's own post followed; and after a Datastar morph the sheet's script ran twice. The form is now data-plain, and the script arms once per sheet. Production's 0.120.0 has no submit handler on the sheet, so it sends one post. - Two file writes into one repository at once could fork fossil ("would fork"; and "database is locked"). scm-add-files is now one at a time per repository (a lock per repo-dir in its :around method); other repositories wait for nothing. Tests: zine-test (+12): the toggle on by default, off, on; a reading copy beside its zine and a lone -read.pdf; the shelf leaving the copy out; the JSON's reading key, kept only when the file is in the project; four publishes into one fossil project at once, none refused, all four there. zinedrop.e2e (+13, 45): the frame as tall as its thumbnails; cancel quiet; exactly one post; the copy beside the booklet, not on the shelf, a PDF of its own; "download to read" on the reader; read back in the site's reader, ZPAGE1 ... ZPAGE8 one to a page; the mini zine's copy MPAGE1 ... MPAGE8, and by the site's pdf.js each page's text exactly its own word, upright; the showcase toggle on, then off, and a booklet published with no reading key and no file. Controls: no 180-degree turn - the upright check fails; the toggle not saving - 2 failures; no data-plain - two posts, and the copy lost (2 failures); no lock - three of the four concurrent commits refused. Gate: 10985 checks, ALL GREEN. surface.lock: migration 0046. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 627144fadf user: dkords tags: trunk | |
| 07:34 | 0.120.0 (minor): drop a PDF on a zine catalog, check its reading order, publish (ticket 05e8d9041d, the brief's step 2) For a writer, a page drawn as a zine catalog - a project's showcase or a space's page with the 0.119.0 toggle on - is a drop target: - Dragging files over the page outlines it: "drop to publish into <name>". On a phone the strong button "publish a zine" opens the file picker. A visitor gets no drop, no sheet, no script. - The sheet: the order check. The first eight pages drawn as the reader will show them, numbered, by the reader's own code (zine-frame.js in a new check mode) in a sandboxed frame, /-/zinecheck, from the bytes of the file still on the creator's machine, posted in by message - no upload until publish. The layout is guessed from the page shape and set in the select; changing it reorders the thumbnails in place (a booklet read as spreads shows 8, 1, 2, 7). Title from the file's name, a line, the cover (an image dropped with the PDF), the project. - Publish is one form post, POST /ui/{org}/-/zinedrop, into the 0.108.0 publishing path. On a space, the project select offers the projects this person may write and that take files, `zines` first; a space with none offers "zines (a new public project)", made on that first publish (fossil, as /new). On a project's showcase the project is fixed. - A writer's empty space catalog says "Drop a PDF here to publish the first zine" - the catalog shows before the space has any project. The two fixes from 0.119.0's review: "download to print" and its note sit above the reader's frame, so a phone shows them on the first screen; a space's shelf is headed by the project's showcase title. New: static/zine-drop.js (ours, SBOM.md). zine-frame.js: check mode; one PDF load wins (pages gathered apart, swapped in whole - two loads had interleaved pages); thumbnails marked pending while redrawn. spice-ui: zine-drop-ui, zine-drop-targets, zine-check-document, zine-drop-post. The check frame's policy is the PDF frame's with connect-src only /lib/pdf/. Tests: zinedrop.e2e (new, 32, BROWSER_2): the empty catalog for a writer and nothing for a visitor; a real drop (a DataTransfer with a booklet and a cover) lights the page, opens the sheet, eight numbered thumbnails in reading order (ZPAGE1, ZPAGE2 ... ZPAGE8), spreads reorders them to ZPAGE8 first, back again; nothing exists before publish; publish makes the public zines project and lands on the reader with the JSON's title, blurb, layout and cover; on a 390px phone the button's picker and a guessed mini zine, the sheet fitting, the print button on the first screen; a project's catalog showcase drops into itself; its showcase title heads the space's shelf; portrait pages read as pages; cancel publishes nothing; another person's post commits nothing and makes no project; the check frame's policy. Controls: the post not checking the writer - "nothing was committed" fails; the frame ignoring a layout change - the spreads check fails (2 failures). Gate: 10935 checks, ALL GREEN on the second run; the first lost two checks of health-test (postgres) waiting for the hg sidecar to start under load, which passed 3 of 3 alone and is not touched here. surface.lock: GET /-/zinecheck, POST /ui/{org}/-/zinedrop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 09e6ba2433 user: dkords tags: trunk | |
| 04:54 | 0.119.0 (minor): the zine catalog, step 1 - a showcase toggle and a space toggle that open the page on its zines, and print first in the reader (ticket 05e8d9041d) The Zine Catalog Design Brief's first step. Whether a page is a catalog is a showcase decision: - Showcase form: "Show as a zine catalog". On, the showcase opens on the covers of the project's zines right under its title - two to a row on a phone, larger on a desk, no count, no file names - with everything else and the README below. A writer gets "publish a zine" under the shelf. Off, or no zines: the showcase as it was. - Space settings: the same toggle. On, the space's page opens on a shelf per project (in the list's order, under the project's name) above the project table; each reader sees the shelves of the projects they may read. - The reader: "download to print" is now the button, with one line under it on how that layout prints and folds (booklet, mini zine, spreads, grid, pages). A zine whose layout its maker did not say gets "print it as it is laid out": the reader's guess happens in the browser and the server will not promise a fold it may not have. Migration 0045_zine_catalog.sql: showcase.zine_catalog, orgs.zine_catalog (1 or NULL). spice-zine: catalog-p, set-catalog, org-catalog-p, set-org-catalog, print-note-key. Open questions left as the brief's defaults: showcase-exposure zines stay off catalogs. OPDS.md (design, added): OPDS 2 feeds for zines, with s8 carrying the 2026-11-06 decisions. Tests: zine-test (+11): the toggles on, per project, off; the note per layout, unsaid gets no fold. zine.e2e (+17): the form's toggle saves; on, a stranger's showcase opens on the catalog before the README with no count; the writer's button and not the reader's; a 390px phone shows two covers to a row with no sideways scroll; the space toggle shows press and not the private drawer to a stranger, the drawer to its owner, and off again; the reader's print button with the booklet's fold, the guessed layout's plain note; off restores the counted shelf. Controls: set-catalog not writing - 3 failures (the suite stops at the phone check); the booklet given the pages note - 2 failures. Gate: 10871 checks, ALL GREEN. surface.lock: migration 0045. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> check-in: 6af3c76864 user: dkords tags: trunk | |
|
2026-11-06
| ||
| 15:52 | 0.118.0 (minor): the site's own 404 for every address that is nothing (ticket d39cf20d27) An address that matched no route - /nope - got the framework's bare 404 page: no header, no footer, a different typeface, "message: /nope". A two-segment address that is no project already got the site's "Nothing here"; now every other GET does too, through a /* route registered last in the UI, which the named routes and the two-segment wire mounts are more specific than and keep winning. Found by a Playwright audit over nineteen signed-out pages for ticket d39cf20d27 (one header shape, one footer shape across all of them; the ticket carries the findings). Tests: aiok.e2e (+2): /nope-such-page is 404 with Nothing here and the site's footer and not the framework's page; /nobody/nothing the same. Control: the catch-all redirecting to the front page instead - 1 failure. Gate: 10815 checks, ALL GREEN, 8:48. surface.lock: GET /*. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> check-in: fe71540376 user: dkords tags: trunk | |
| 15:35 | 0.117.0 (minor): /about - why a creator decides who may take the work, at length (ticket ebd30b8e3e) GUARDIAN.md holds the design; this is the public argument for it, written for the person deciding where to put a decade of work, in English and Japanese, linked from every footer. Nine paragraphs: a repository holds somebody's work and showing it is not the same act as handing all of it to anyone who asks at once; here those are two decisions, exposure (a lock) and the AI policy (a declaration, advertised four ways); ask-first is the honest reading of silence, since a licence that permits copying was written for people and their tools and says nothing either way about collection; this is not a position against machines - a package manager, a CI runner, a mirror, an index takes the same path a person does, an account, a request, a grant, and is then never judged; an anonymous client asking for source is judged cheaply and locally by what it does and every verdict is written down; the decoy, where an owner allows it, with its cost stated openly; what all this buys and what it does not (certainty: a determined taker with a browser reads what the showcase shows, because the showcase is for people); nothing is hidden; the vocabulary is this site's until the IETF's settles. Then how it works in six lines and the three machine-readable links. Cached an hour. Tests: aiok.e2e (+4): /about answers a visitor with the argument's sentences and the well-known link, the footer of the front page links it, the Japanese title under Accept-Language ja, a phone without sideways scroll; i18n-test: every new string has Japanese and context. Control: the page moved off its address - 2 failures (the page and its Japanese). Gate: 10811 checks, ALL GREEN, 8:42. surface.lock: GET /about. Ticket ebd30b8e3e. FEATURES.md ledger. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> check-in: ec46cfa0df user: dkords tags: trunk | |
| 15:21 | 0.116.0 (minor): a token's life - minted to expire, or not (SPEC.md s13, ticket aa2d00daa4) Every token has lived until revoked. Now a life is chosen when it is minted and never changed after (a token that should live longer is a new token): on the settings form a select - until you revoke it, 30, 90 or 365 days - read back on the confirm page ("It lives 90 days.") and carried into the mint; on POST /api/v0/session/token, expires_in_days (1 to 3650; left out, until revoked; 0 or 5000 is 400). tokens.expires_at (0044) holds the UTC moment; USER-BY-TOKEN answers nobody once it has passed, so the token opens nothing - the API, the wire, the frame - while the row stays, listed to its owner with the moment it passed, as a revoked one stays: the record that it existed. The card says "expires <day>" or "no expiry"; GET /api/v0/user/tokens carries expires_at (null for never). Pairing (0.109.0) and robots (0.110.0) mint without a life, as before. Tests: tokens-test (15; postgres too): the four lives, never and blank and nil as no life, a word and a number of days, too many, zero and nonsense bad; a token without a life and one with a day's life both open the account, the list says never and about a day on; the moment moved into the past, the brief one opens nothing and the other still does, and the brief one is still listed. tokens.e2e (15): minted over the API with 90 days (the body says so) and without (null), 0 and 5000 refused, the list's expires_at about 90 days on and null, both open today; the form offers the four lives, the cards say expires on the day and no expiry, the confirm page says 30 days and carries the choice, minted and shown once, the card and the API agree on the day; a phone. Control: the lookup ignoring expiry - tokens-test 1 failure (the expired token opens the account). Gate: 10803 checks, ALL GREEN, 8:48. surface.lock unchanged. migrate-test counts 44. Ticket aa2d00daa4 (closed: scopes, list, revoke and now expiry are all built). FEATURES.md ledger. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> check-in: a1a7586a1b user: dkords tags: trunk | |
| 14:56 | 0.115.0 (minor): API budgets - a token's requests per minute, and a concurrency ceiling (ticket 7f57cee3ec) The API has been in the footer since the footer existed, and until 0.103.0 nothing anywhere budgeted a request; the guardian slot then scored anonymous clients, and a credential ended its interest. A credential is never a crawler - and it is not a licence to hammer either. Two budgets now, both in the slot's and the layer's own place: A token's budget: every request that carries a token (Bearer or Basic), on every path but /api/v0/ping and /api/v0/health which a monitor polls, is counted under token:<id> in the guardian's memory - a minute window and a count of their own on the client row, written under the row's monitor (the event list is capped at 400, so a budget of 400 could never be exceeded through it, and twenty parallel requests lost pushes: both found by the suite at 400 and fixed); past -Dspice.budget.token a minute (600; 0 for none) the answer is 429 with Retry-After 60, X-Guardian budget and a sentence with the count and the budget. Each token is its own; the minute turns and the budget is back. The concurrency ceiling: spice-http holds -Dspice.concurrency permits (64; 0 for none) in a fair java Semaphore; every route takes one for its length (UNWIND-PROTECT gives it back whatever happens) and a request that gets none within two seconds is answered 503 busy with Retry-After 2 rather than queued without end - the renderer and the engines are the bottleneck on a small host, and MELANGEJ.md calls the pool a correctness prerequisite. The test servers carry -Dspice.budget.token=400 - the other suites' tokens stay well under it. Tests: guardscore-test (+11): five requests within a budget of five, the sixth over and saying six, another token its own, a minute later back, no token and a budget of 0 unbudgeted; the ceiling in the test image, a permit taken and given back, every permit taken and the next asker told no after its wait, all given back. guardian.e2e (+4): a token making 400 requests in a minute, twenty at a time - all served - then the 401st 429 with X-Guardian budget, Retry-After 60 and "budget is 400"; ping never budgeted; a sibling token its own. Control: the budget never refusing - guardscore-test 1 failure, guardian.e2e 1 (the 401st served). Not built, still: cost limits on the expensive paths (timeline, cat, a spawned engine) beyond the ceiling. Gate: 10745 checks, ALL GREEN, 8:44. surface.lock unchanged. Ticket 7f57cee3ec (the per-token budget and the ceiling it asked for; cost limits stay open). FEATURES.md ledger. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> check-in: 190f314a96 user: dkords tags: trunk | |