Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

## Unreleased

- Payload plugin: people are recorded by id, never by name (changes the unreleased proposals, before anyone used them). `proposals.nameOf` is gone. `proposals.personId({ req })` gives an opaque id for the signed-in person (default: their user document's id), and that id is all a proposal stores about its author and all an accepted proposal seals into the new snapshot for the author and the manager. An id is 1 to 64 letters, digits, `.`, `_`, `:` and `-`, starting with a letter or digit, so an email address or a name with a space in it is refused: the action stops, nothing is saved, and the refused value isn't written to the log. A one-word handle would pass; the plugin can't tell it from an id. `proposals.displayName({ req, personId })` is optional and gives the name to show: it is looked up each time a proposal is read (once per person per request), returned as `authorName` on the proposal and in the preview, and never stored or sealed, so a name changed or removed at the site changes what is shown at once and leaves every snapshot's checksum alone. Why: a name inside a sealed snapshot can't be taken out when a person leaves. Core is unchanged apart from its comments and one error text: `acceptIntoSnapshot` still seals the `author` and `acceptedBy` strings it is given. Still sealed as written, and not screened: a proposal's title, each move's note and the manager's note. No endpoint turns an id from a snapshot's record back into a name. Tested on Payload 3.93 with an in-memory MongoDB (142 plugin tests, 526 in all) and through the example site (47 checks); eight deliberate breaks of the new code each failed a test. Not tested: Postgres or SQLite, and a host with more than one auth collection.
- React: names in the drawing are no longer struck through or covered. In rectangular layouts an inner node's name was drawn 6 units above the node, so when its children's rows were one row apart, a child's branch ran straight through it ("Reptilia" in the amniotes classes view). The name now goes in the first of three places the branches to its children leave clear: on the node's own row, just above it, or just below it; when rows are closer than a name is tall, it takes the place with the most room. A folded branch's name started 8 units out, on top of its 12-unit triangle; it now starts 16 units out, past it. The SVG and the canvas use the same rule (`innerLabelAt`, `leafLabelX`), so a name is in the same place in both. A leaf's name hasn't moved. Limits: a long name can still cross a branch further down the tree, since only the node's own children are looked at, and in a radial drawing a folded branch's triangle still points right whatever the branch's angle.
- Example site: `apps/payload-example`, a small Payload 3.93 and Next.js 16 site that uses the Payload plugin (private, never published). It runs on this machine only, on a throwaway in-memory MongoDB that `pnpm dev` starts and seeds: two made-up accounts with a password made up per run, one published snapshot with two views, and one staged snapshot, all from the repository's illustrative fixtures. Accounts have two roles: a curator manages and decides, a member reads and proposes, only a curator hands out roles, and a member sees no account but their own. `proposals.nameOf` gives a public `handle`, not the email address, because the names in an accepted version can't be taken out later. The public pages are Server Components that read as an anonymous visitor and draw each published view with `TreeView` and `TreeOutline`, with what the view leaves out and its sources; a source's address is a link only when it is http or https. `pnpm smoke` builds the site, starts it on a free port and runs 46 checks over HTTP with sign-in cookies: what a visitor reads and can't do through the pages, REST and GraphQL; cookies that count only from the site's own pages; roles; a proposal made, shared, accepted into a staged, sealed version that names both people by handle and holds no email address. CI runs it, and the CI workflow now cancels a run when a newer push arrives and stops a job after 20 minutes. Passed on `next dev` and on the built site on a Mac; the pages were looked at in headless Chrome in light and dark. Not tested: a person clicking through the admin, a screen reader, Postgres or SQLite, a deployed site. The site has no pages for proposals or imports.
- Example site: `apps/payload-example`, a small Payload 3.93 and Next.js 16 site that uses the Payload plugin (private, never published). It runs on this machine only, on a throwaway in-memory MongoDB that `pnpm dev` starts and seeds: two made-up accounts with a password made up per run, one published snapshot with two views, and one staged snapshot, all from the repository's illustrative fixtures. Accounts have two roles: a curator manages and decides, a member reads and proposes, only a curator hands out roles, and a member sees no account but their own. Proposals record people by account id, and `proposals.displayName` looks up a public `handle`, not the email address, when one is shown. The public pages are Server Components that read as an anonymous visitor and draw each published view with `TreeView` and `TreeOutline`, with what the view leaves out and its sources; a source's address is a link only when it is http or https. `pnpm smoke` builds the site, starts it on a free port and runs 47 checks over HTTP with sign-in cookies: what a visitor reads and can't do through the pages, REST and GraphQL; cookies that count only from the site's own pages; roles; a proposal made, shared, accepted into a staged, sealed version that records both people by id and holds no handle and no email address. CI runs it, and the CI workflow now cancels a run when a newer push arrives and stops a job after 20 minutes. Passed on `next dev` and on the built site on a Mac; the pages were looked at in headless Chrome in light and dark. Not tested: a person clicking through the admin, a screen reader, Postgres or SQLite, a deployed site. The site has no pages for proposals or imports.
- React: the package can be imported from a Server Component (Next.js App Router). `CanvasTreeView.tsx`, `Phylo.tsx` and `toolbar.ts` call client hooks and now say `'use client'`; without it, importing anything from the package in a Server Component failed. `layoutFor` moved out of the explorer's client file into its own, so a server can call it, and `TreeView` renders there as SVG with no JavaScript sent. The plain functions still inside client files (`rootedRecord`, `proposeTipLinks`, `linkedPhylogeny`, `supportSummary`, `tipsUnder`, `cameraBox`, the canvas helpers) can only be called in the browser for now; the README lists them. A test pins which files are client modules. Seen working in Next.js 16 through `apps/payload-example`; a tree large enough for the canvas hasn't been tried from a Server Component.
- Payload plugin: tests over HTTP. Twelve new tests send real requests through Payload's own REST router (`handleEndpoints`), signed in with a token from `/api/users/login`: every plugin endpoint is routed and reads its parameters from the URL and the body, the standard routes list a reader only their tenant's published snapshots, another tenant and an anonymous or bad-token request get 404 or 403 and no data, a read route takes no write, a new proposal is its sender's whatever the body says, and accept and decline work from a request body. No code changed. Still not run in a real site behind a web server.
- Payload plugin, part 4 of M7: proposals (still not on npm). With the new `proposals` option, a `taxonomy-proposals` collection holds each signed-in person's own proposals over a snapshot. A proposal is private until its author shares it with the people in their tenant who can read; anonymous visitors never see one. Only the author edits it, and only while it is private. A manager accepts or declines a shared proposal through `POST /api/taxonomy-proposals/:id/accept` and `/decline`, and anyone who can read it gets what it would change from `GET …/:id/preview`. Accepting names the new version and the proposal's checksum as last seen, so a proposal that changed in between is refused. It makes a **new staged snapshot version** and never edits the published one; readers see nothing until a manager publishes it. Two managers accepting at once store one version. The author, the snapshot, the tenant and the decision can't be set through an update, by server code either, and an accepted proposal can't be changed or deleted. Deleting the staged version before it is published opens the proposal again. A third access callback, `access.propose`, says who may propose (default: signed-in users). `proposals.nameOf` gives the names an accepted version carries. **Those names are sealed into the snapshot and can't be taken out later**, so a site should give a public handle, not an email address. Limits: each acceptance is made from the published snapshot alone; without database transactions a crash between the two writes can leave a staged version to delete by hand; there is no interface yet (a proposal is a JSON field in the admin). ADR 0012, point 15. Tested on Payload 3.93 with an in-memory MongoDB, with and without transactions (127 plugin tests); not through Payload's HTTP routing, in a real site, or on Postgres or SQLite.
- Payload plugin, part 4 of M7: proposals (still not on npm). With the new `proposals` option, a `taxonomy-proposals` collection holds each signed-in person's own proposals over a snapshot. A proposal is private until its author shares it with the people in their tenant who can read; anonymous visitors never see one. Only the author edits it, and only while it is private. A manager accepts or declines a shared proposal through `POST /api/taxonomy-proposals/:id/accept` and `/decline`, and anyone who can read it gets what it would change from `GET …/:id/preview`. Accepting names the new version and the proposal's checksum as last seen, so a proposal that changed in between is refused. It makes a **new staged snapshot version** and never edits the published one; readers see nothing until a manager publishes it. Two managers accepting at once store one version. The author, the snapshot, the tenant and the decision can't be set through an update, by server code either, and an accepted proposal can't be changed or deleted. Deleting the staged version before it is published opens the proposal again. A third access callback, `access.propose`, says who may propose (default: signed-in users). An accepted version records its author and the manager by person id, not by name (see the entry above). Limits: each acceptance is made from the published snapshot alone; without database transactions a crash between the two writes can leave a staged version to delete by hand; there is no interface yet (a proposal is a JSON field in the admin). ADR 0012, point 15. Tested on Payload 3.93 with an in-memory MongoDB, with and without transactions (127 plugin tests at the time); not through Payload's HTTP routing, in a real site, or on Postgres or SQLite.
- Core: `acceptIntoSnapshot(base, proposal, { version, createdAt, acceptedBy, note })` turns a proposal on a classification into a new, unsealed snapshot version. The base isn't changed. The classification's version becomes `<its version>+<proposal id>`, each moved placement names the new source record `proposal:<id>` as its provenance with where it was before, that record says who proposed and who accepted, and `derivedFrom` pins the base (id, version, checksum) and the proposal (new `derivedFrom.proposal`, type `AcceptedProposal`, in the JSON Schema too). It refuses a base that isn't sealed or fails its checksum, the base's own version, a proposal that moves nothing or was already accepted into the dataset, anything `applyToClassification` refuses, and every proposal on a phylogeny: a tree rearranged by hand is a result its study never found, so a new analysis is imported as its own hypothesis instead. `proposalSourceId` is exported.
- Docs: the root README and the limitations page said the packages weren't on npm. The five `@captiveorigins/onetaxonomy-*` packages are, as alpha versions; the Payload plugin isn't.
- OneOrigin brand copy: round joins (owner decision N169, 2026-11-11): the copy in `brand/oneorigin/` is updated from OnePhylogeny's `docs/oneorigin/` at `189c49f`. The guidelines, the brand text and the shared connector icon part now say connectors have round joins. Only the copied brand files and `brand/oneorigin.lock` change; this tool's own icon and drawings are untouched (only OnePhylogeny's icon was redrawn). Checked with `node scripts/oneorigin.mjs check`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The full rules are in [scientific invariants](docs/architecture/scientific-invar
| [`@captiveorigins/onetaxonomy-education`](packages/education) | Lesson decks (scenes, cameras, annotations, reveals) and worksheets with answers computed from the data | Working |
| [`@captiveorigins/payload-plugin-onetaxonomy`](packages/payload) | Payload plugin: sealed snapshots checked by checksum, staged and published, views pinned to them, import jobs that turn source files into staged snapshots, people's own proposals that a curator can accept into a new staged version, and a bounded read API (name search, ancestors, descendants, MRCA, coverage, projected views, exports) behind the host's access rules, with optional tenant scope | Early (M7 parts 1 to 4): tested on Payload with an in-memory MongoDB, not yet in a real site. Not on npm |
| [`apps/studio`](apps/studio) | OneTaxonomy for Mac (Tauri 2): open checklists, Darwin Core Archives, Newick, NEXUS and phyloXML trees and snapshots; browse them; collapse weak branches, add a tree to an open checklist and link its tips to taxa (proposed by exact name, confirmed by you), compare two trees, and reroot or midpoint-root as a new tree; save views and sealed snapshots (never over the file that was opened), and save and open `.onetaxonomy` bundles (a documented ZIP with a manifest and checksums) | 0.1.0 on GitHub Releases. Not notarized, and it can't update itself yet |
| [`apps/payload-example`](apps/payload-example) | A small Payload and Next.js site that uses the plugin: two roles, public pages that draw published views on the server, and proposals under a public handle. Runs on your machine with a throwaway in-memory database | An example. Checked over HTTP by its smoke test; nobody has clicked through its admin yet |
| [`apps/payload-example`](apps/payload-example) | A small Payload and Next.js site that uses the plugin: two roles, public pages that draw published views on the server, and proposals that record people by id, with a public handle shown beside it. Runs on your machine with a throwaway in-memory database | An example. Checked over HTTP by its smoke test; nobody has clicked through its admin yet |

## Try it

Expand Down
11 changes: 6 additions & 5 deletions apps/payload-example/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# OneTaxonomy Payload example

A small Payload and Next.js site that uses [`@captiveorigins/payload-plugin-onetaxonomy`](../../packages/payload). It shows the plugin set up the way a real site would: accounts with two roles, public pages that draw published views, and proposals that carry a public handle instead of an email address.
A small Payload and Next.js site that uses [`@captiveorigins/payload-plugin-onetaxonomy`](../../packages/payload). It shows the plugin set up the way a real site would: accounts with two roles, public pages that draw published views, and proposals that record a person by an id, with a public handle shown beside it and no email address.

> **Status: an example, not a product.** It runs on this machine only, on a throwaway in-memory MongoDB, with made-up accounts and the repository's illustrative fixtures. Tested: 46 checks over HTTP with sign-in cookies (`pnpm smoke`), on `next dev` and on the built site, with Payload 3.93 and Next.js 16; the three public pages were looked at in headless Chrome, in light and dark, at desktop width and at 520 px. Not tested: a person clicking through the admin, a screen reader, a phone, Postgres or SQLite, or a deployed site.
> **Status: an example, not a product.** It runs on this machine only, on a throwaway in-memory MongoDB, with made-up accounts and the repository's illustrative fixtures. Tested: 47 checks over HTTP with sign-in cookies (`pnpm smoke`), on `next dev` and on the built site, with Payload 3.93 and Next.js 16; the three public pages were looked at in headless Chrome, in light and dark, at desktop width and at 520 px. Not tested: a person clicking through the admin, a screen reader, a phone, Postgres or SQLite, or a deployed site.

## What it shows

- **Public pages.** `/` lists the published views. `/views/<id>` draws one as an SVG tree with its keyboard outline, what the view leaves out, and its sources. Both are Server Components: the tree is drawn on the server and no tree code is sent to the browser. The pages read as an anonymous visitor, so they only ever show what is published.
- **Two roles.** A curator manages snapshots, views and import jobs, uses the admin, and decides on proposals. A member reads what is published and makes proposals. Only a curator hands out roles: a member can't raise their own, and sees no account but their own.
- **Proposals under a handle.** `proposals.nameOf` gives each person's `handle`, not their email address. When a curator accepts a proposal, the new snapshot version names the proposer and the curator by handle, inside its checksum. Those names can't be taken out later, which is why the handle field says so where people fill it in.
- **Proposals by id, with a handle shown.** A proposal stores its author's account id, and when a curator accepts one, the new snapshot version records the proposer and the curator by those ids, inside its checksum. No name or email address is sealed. `proposals.displayName` looks up each person's `handle` when a proposal is shown, so a changed handle shows at once and a removed one leaves the id alone.
- **Staged, then published.** The seed stores one published snapshot with two views, and one staged snapshot that only the curator sees. An accepted proposal makes a new staged version; visitors see nothing until the curator publishes it.
- **Cookies from this site only.** Payload's `csrf` list holds the site's own address, so a sign-in cookie sent from another site's page counts for nothing.

Expand Down Expand Up @@ -52,8 +52,9 @@ This builds the site, starts it on a free port with its own database, and sends
- a visitor can't store a snapshot, make a proposal, or list accounts or proposals;
- a sign-in cookie can't be read by scripts, and counts for nothing from another site;
- a member can't make themself a curator, sees only their own account, and is sent away from the admin;
- a member's proposal is stored under their handle whatever the request says, with no email address in it, and stays private until shared;
- only the curator can accept it, and the new version is staged, sealed, names both people by handle, holds no email address, and isn't public;
- a member's proposal is stored under their account's id whatever the request says, shows their handle, has no email address in it, and stays private until shared;
- only the curator can accept it, and the new version is staged, sealed, records both people by id, holds no handle and no email address, and isn't public;
- a changed handle shows on the accepted proposal at once, and the sealed version doesn't change;
- an accepted proposal can't be deleted.

`--dev` runs `next dev` instead of a build, and `--skip-build` reuses the last build. CI runs the smoke test on every pull request.
Expand Down
Loading
Loading