From 19fb0e76b8402c46971c0f55077eafc9967a0f4b Mon Sep 17 00:00:00 2001 From: Michael Gilardi <11632652+mgilardi@users.noreply.github.com> Date: Sat, 14 Nov 2026 06:24:51 -0700 Subject: [PATCH] Move OneTaxonomy to the @extantape scope and ExtantApe repo EXTANT APE owns the One tools, so the packages publish under @extantape from 0.1.0-alpha.4 and the repo lives at ExtantApe/OneTaxonomy. This does for OneTaxonomy what OnePedigree already did. All six packages are renamed and bumped in lockstep, every GitHub URL points at the ExtantApe repos, and the Release workflow and RELEASING.md name the mdgilardi npm account. Text that named the old owner or the site that used the tool now says EXTANT APE or uses neutral words, and old task and decision numbers are gone from docs, comments and test names. The shared brand files are copied again so they match OnePedigree byte for byte, and the lock is updated. The example site's import map was regenerated with payload generate:importmap. Kept on purpose: the Mac app's identifier, which still carries the old name, because changing it resets people's settings, and the brand guidelines' line about app identifiers, which is a shared file. The license is unchanged. Tested: build, typecheck, 633 tests, the example site's smoke test (74 checks), the leak check and the OneOrigin check, which warns as expected that the brand source differs until OnePhylogeny moves too. Nothing is published: the @extantape packages need their first release staged and approved. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 6 +-- CHANGELOG.md | 24 +++++----- README.md | 24 +++++----- RELEASING.md | 6 +-- apps/payload-example/README.md | 8 ++-- .../app/(payload)/admin/importMap.js | 16 +++---- apps/payload-example/app/(site)/layout.tsx | 2 +- apps/payload-example/app/(site)/site.css | 2 +- .../app/(site)/views/[id]/ViewFigure.tsx | 4 +- .../app/(site)/views/[id]/page.tsx | 2 +- apps/payload-example/package.json | 8 ++-- apps/payload-example/payload.config.ts | 2 +- apps/payload-example/scripts/smoke.mjs | 2 +- apps/payload-example/src/seed.ts | 4 +- apps/playground/app.tsx | 8 ++-- apps/playground/bench.tsx | 6 +-- apps/playground/package.json | 10 ++--- apps/studio/package.json | 8 ++-- apps/studio/src-tauri/Cargo.toml | 2 +- apps/studio/src-tauri/tauri.conf.json | 2 +- apps/studio/src/App.tsx | 6 +-- apps/studio/src/app.css | 4 +- apps/studio/src/bundle.ts | 6 +-- apps/studio/src/main.tsx | 2 +- apps/studio/src/open.ts | 4 +- apps/studio/test/app.test.tsx | 4 +- apps/studio/test/bundle.test.ts | 4 +- apps/studio/test/open.test.ts | 4 +- apps/studio/test/theme.test.ts | 2 +- brand/README.md | 2 +- brand/icon.svg | 4 +- brand/oneorigin.lock | 24 +++++----- brand/oneorigin/README.md | 14 +++--- brand/oneorigin/brand.md | 28 ++++++------ brand/oneorigin/guidelines.md | 28 ++++++------ brand/oneorigin/icons/connector.svg | 2 +- brand/oneorigin/icons/heart-badge.svg | 6 +-- brand/oneorigin/icons/tile-onepedigree.svg | 2 +- brand/oneorigin/icons/tile-onephylogeny.svg | 2 +- brand/oneorigin/icons/tile-onetaxonomy.svg | 2 +- brand/oneorigin/tokens.json | 10 ++--- brand/oneorigin/tokens.schema.json | 2 +- .../adr/0001-monorepo-and-tooling.md | 2 +- docs/architecture/adr/0002-pure-core.md | 2 +- docs/architecture/adr/0007-releases.md | 2 +- docs/architecture/adr/0008-package-scope.md | 10 +++-- docs/architecture/adr/0009-license.md | 2 +- .../adr/0010-outline-is-the-keyboard-path.md | 4 +- .../adr/0011-bundle-not-sqlite.md | 4 +- docs/architecture/adr/0012-payload-plugin.md | 6 +-- docs/architecture/adr/README.md | 8 ++-- docs/architecture/architecture.md | 2 +- docs/architecture/backlog.md | 18 ++++---- docs/architecture/bundle-format.md | 2 +- docs/architecture/data-schema.md | 4 +- docs/architecture/limitations.md | 2 +- docs/architecture/phase-plan.md | 4 +- docs/test-audit.md | 24 +++++----- examples/build-fixtures.mjs | 4 +- examples/newick.mjs | 4 +- examples/package.json | 6 +-- examples/reptilia-is-not-a-clade.mjs | 4 +- examples/selective-import.mjs | 4 +- fixtures/README.md | 2 +- package.json | 4 +- packages/core/README.md | 10 ++--- packages/core/package.json | 10 ++--- packages/core/schema/snapshot.schema.json | 2 +- packages/core/src/validate.ts | 2 +- packages/education/README.md | 8 ++-- packages/education/package.json | 12 ++--- packages/education/src/index.ts | 2 +- packages/education/test/education.test.ts | 2 +- packages/import/README.md | 12 ++--- packages/import/package.json | 12 ++--- packages/import/src/adapter.ts | 2 +- packages/import/src/bundle.ts | 2 +- packages/import/src/checklist.ts | 2 +- packages/import/src/dwca.ts | 2 +- packages/import/src/json.ts | 2 +- packages/import/src/newick.ts | 2 +- packages/import/src/nexus.ts | 2 +- packages/import/src/phyloxml.ts | 2 +- packages/import/src/preset.ts | 2 +- packages/import/src/reconcile.ts | 2 +- packages/import/src/selective.ts | 2 +- packages/import/test/bundle.test.ts | 2 +- packages/import/test/checklist.test.ts | 2 +- packages/import/test/curation.test.ts | 2 +- packages/import/test/newick.test.ts | 2 +- packages/import/test/nexus.test.ts | 2 +- packages/import/test/phyloxml.test.ts | 2 +- packages/layout/README.md | 6 +-- packages/layout/package.json | 12 ++--- packages/layout/src/index.ts | 2 +- packages/layout/test/layout.test.ts | 2 +- packages/payload/README.md | 8 ++-- packages/payload/package.json | 16 +++---- packages/payload/src/api.ts | 2 +- .../payload/src/client/ImportPanelView.tsx | 2 +- packages/payload/src/client/ViewPanelView.tsx | 4 +- packages/payload/src/client/panels.css | 2 +- packages/payload/src/client/wired.tsx | 4 +- packages/payload/src/collections.ts | 2 +- packages/payload/src/imports.ts | 4 +- packages/payload/src/proposals.ts | 2 +- packages/payload/src/summary.ts | 2 +- packages/payload/src/types.ts | 2 +- packages/payload/src/views-api.ts | 2 +- packages/payload/test/api.test.ts | 2 +- packages/payload/test/helpers.ts | 2 +- packages/payload/test/http.test.ts | 2 +- packages/payload/test/imports.test.ts | 4 +- packages/payload/test/panels-wired.test.tsx | 2 +- packages/payload/test/panels.test.tsx | 2 +- packages/payload/test/proposals-plain.test.ts | 2 +- packages/payload/test/proposals.test.ts | 2 +- packages/payload/test/snapshots.test.ts | 2 +- packages/payload/test/summary.test.ts | 2 +- packages/payload/test/tenants.test.ts | 2 +- packages/payload/test/view-reads.test.ts | 2 +- packages/payload/test/views.test.ts | 2 +- packages/react/README.md | 12 ++--- packages/react/package.json | 16 +++---- packages/react/src/CanvasTreeView.tsx | 2 +- packages/react/src/Explorer.tsx | 4 +- packages/react/src/Lesson.tsx | 6 +-- packages/react/src/Phylo.tsx | 2 +- packages/react/src/TipLinks.tsx | 2 +- packages/react/src/TreeOutline.tsx | 2 +- packages/react/src/TreeView.tsx | 4 +- packages/react/src/canvasPlan.ts | 4 +- packages/react/src/drag.ts | 4 +- packages/react/src/export.ts | 2 +- packages/react/src/icons.tsx | 2 +- packages/react/src/layout.ts | 4 +- packages/react/src/nearNames.ts | 2 +- packages/react/src/shapes.tsx | 2 +- packages/react/src/styles.css | 4 +- packages/react/test/a11y.test.tsx | 8 ++-- packages/react/test/bigTree.ts | 2 +- packages/react/test/canvas.test.tsx | 4 +- packages/react/test/label-place.test.tsx | 4 +- packages/react/test/phylo.test.tsx | 2 +- packages/react/test/react.test.tsx | 6 +-- packages/react/test/reveal.test.tsx | 2 +- packages/react/test/svg-unchanged.test.tsx | 4 +- packages/react/test/tiplinks.test.tsx | 2 +- packages/react/test/tipnear.test.tsx | 2 +- pnpm-lock.yaml | 44 +++++++++---------- scripts/oneorigin.mjs | 2 +- 152 files changed, 401 insertions(+), 397 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39de66b..1c57699 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: # Builds the example Payload site, starts it with a throwaway in-memory database on the runner, and checks it # over HTTP with sign-in cookies. About a minute. - name: Example site smoke test - run: pnpm --filter @captiveorigins/onetaxonomy-payload-example smoke + run: pnpm --filter @extantape/onetaxonomy-payload-example smoke # The OneOrigin brand copy (brand/oneorigin/) must match brand/oneorigin.lock, and the CSS built from it must be current. # The source is OnePhylogeny's docs/oneorigin/; the check warns, without failing, when it has moved ahead of the copy. - name: OneOrigin brand check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30146e5..7057cd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ # Release: pushing a version tag (vX.Y.Z) builds, tests and *stages* every OneTaxonomy package on npm. -# Nothing goes public until the maintainer npm account approves each with 2FA +# Nothing goes public until the mdgilardi npm account, which owns the @extantape scope, approves each with 2FA # (npmjs.com → your avatar → Staged Packages → Approve, or `npm stage approve `). # No token is stored: npm trusted publishing (OIDC) lets only this workflow stage. name: Release @@ -27,14 +27,14 @@ jobs: GH_TOKEN: ${{ github.token }} run: | test "$(gh api repos/${{ github.repository }} -q .description)" = "$(node -p "require('./package.json').description")" \ - || { echo "::error::GitHub's description differs from package.json: sync it (npm run packages:sync in captive-origins) and re-tag."; exit 1; } + || { echo "::error::GitHub's description differs from package.json: copy the description to GitHub and re-tag."; exit 1; } - name: Tag matches every package run: for p in packages/*/; do test "${GITHUB_REF_NAME#v}" = "$(node -p "require('./${p}package.json').version")"; done - run: pnpm build && pnpm typecheck && pnpm test - name: Stage on npm run: | VERSION=$(node -p "require('./packages/core/package.json').version") - # payload (@captiveorigins/payload-plugin-onetaxonomy) isn't staged: its first npm version is published by hand + # payload (@extantape/payload-plugin-onetaxonomy) isn't staged: its first npm version is published by hand # by the owner, and trusted publishing is set up after that. Add it here then. for p in $(ls packages | grep -v -x payload); do NAME=$(node -p "require('./packages/$p/package.json').name") diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c5d5b..bdc1b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,15 @@ # Changelog -## Unreleased +## 0.1.0-alpha.4 + +- **New home and package names:** the packages now publish under `@extantape`: `@extantape/onetaxonomy-core`, `@extantape/onetaxonomy-import`, `@extantape/onetaxonomy-layout`, `@extantape/onetaxonomy-react`, `@extantape/onetaxonomy-education` and `@extantape/payload-plugin-onetaxonomy` (still not on npm). The repo now lives at [ExtantApe/OneTaxonomy](https://github.com/ExtantApe/OneTaxonomy). The old names of the five published packages are deprecated, with a pointer to these. To move, change the scope in your `package.json` and imports; nothing else in the API changes. EXTANT APE owns and licenses the tools, and the license is unchanged. - **OneOrigin brand copy: the new palette and marks (approved by the owner, 2026-11-12):** `brand/oneorigin/` is copied again from OnePhylogeny's `docs/oneorigin/` and relocked. It adds the suite's own palette (`palette` in `tokens.json`), the five marks in `marks/` (the suite and the four tools, each light, dark and small), and Palette and Marks sections in `brand.md`. This tool's app icon, tile, heart badge and interface colors are unchanged, because they aren't decided yet. The built CSS is unchanged too, because it doesn't read the palette. - Example site: import jobs, and a script that clicks through the admin. `apps/payload-example` now turns the plugin's `imports` option on over a new `sources` upload collection that only a curator can read, add to, change or delete, so nobody else can fetch a source file by its address. The seed adds a job waiting to be run and one that failed on purpose, both over made-up files; a job doesn't run when it is made. Uploaded files go in a throwaway folder that is deleted when the site stops (`EXAMPLE_UPLOADS` names a folder to keep instead). The smoke test goes from 58 to 74 checks. New `pnpm click-through` builds the site, opens a Chrome already on the machine with no window and a throwaway profile, signs in as the made-up curator and does a curator's work on all four panels, 40 checks; it installs nothing, needs the network for Payload's code editor, fails on any other request off the site, and isn't in CI. - Payload plugin: three things that script found in the real admin (changes the unreleased plugin). The panels' paragraphs and lists sat flush against each other, because the panel's margin reset outranked its spacing rule; jsdom doesn't lay out, so no test saw it. The reset now carries no weight and the import panel's status and message are spaced too. The form under a panel repeated it: a snapshot's label, id, version, checksum and counts, and an import job's message, checksums, diagnostics, warnings and progress (three of them in Payload's code editor) are now hidden in the form with `admin.hidden`, still in its data, the API and the list's columns. And the "Made-up example data" tag touched the source's title. The docs said every button asks first; running an import never did, and they now say so. - Payload plugin: a snapshot's content is read only when it is asked for by name (changes the unreleased plugin). A stored snapshot can be 15 MB, and Payload read all of it for every list, every relationship it filled in and every admin page. Now a read, create, update or delete that names no `select` answers with every field except `snapshot`; `select: { snapshot: true }`, REST `?select[snapshot]=true` or GraphQL's `select: true` gives the content, and the export endpoint is as before. A caller's own `select` is left exactly as asked. In the admin the content field is on the new-snapshot form only: a stored snapshot's page is sent without the content and without the JSON editor, and Payload's own Save there sends the status alone. Hosts' code that read `doc.snapshot` from a plain read, or from the document an after-read or after-change hook is given, has to select it. The lock on sealed content doesn't depend on this. Fixed on the way: the admin panels showed the old status after Payload's own Save; they now read from the site again whenever Payload reports the document saved. Tested: 10 new tests (8 on a real Payload with an in-memory MongoDB, one of them over HTTP, and 2 in jsdom), 3 new checks over HTTP in the example site (58 in all), and five deliberate breaks, each caught. Also run in headless Chrome on the example site, outside CI: a stored snapshot's page has no content and no editor, a sealed snapshot pasted into the new-snapshot form is stored with the same checksum, and Payload's Save sends one small request. Not explained: in that run, after a snapshot was made through the form and its page loaded again, Payload's read-only Counts group showed no fields under its heading; four shorter runs of the same steps drew them. Not tested: Postgres, SQLite, a browser with a window. See [ADR 0012](docs/architecture/adr/0012-payload-plugin.md), point 17. -- Payload plugin, part 5 of M7: admin panels (still not on npm). Each of the plugin's four collections gets a panel at the top of its edit page, on unless `admin: { panels: false }`. The snapshot panel says the status in words, what the snapshot holds (counts, a line per classification and phylogeny, sources with their licenses) and, for one made by accepting a proposal, which proposal and the two sealed person ids; a manager gets Publish and Withdraw. The view panel draws the saved view (the first 200 nodes) with the same tree as an outline for the keyboard and the lines readers are told, and holds Publish, with the reason, until the form is saved and the snapshot is published. The import panel shows the steps of a run, its warnings and diagnostics, looks again every 3 seconds while a job runs, and offers to run a queued, failed or interrupted job. The proposal panel says what a proposal would change, and a manager accepts it (naming the new version) or declines it. Publish, Withdraw, Accept and Decline ask first in the page and say what can't be undone (running an import doesn't ask: it stores at most a staged snapshot); accepting lists what gets sealed for good and asks the manager to check it for personal details. The panels have no powers of their own: they call the host's site from the browser as the signed-in person, so the host's `access` callbacks decide, and a refusal reads "Nothing was changed." with the site's reason. New: the `@captiveorigins/payload-plugin-onetaxonomy/client` entry (found through Payload's import map, so run `payload generate:importmap` after adding the plugin), the `admin.panels` option, and one endpoint, `GET /api//:id/summary` (and `summarizeSnapshot`), which answers counts and capped lists, never names or trees, with the same 404 as the other endpoints. For a snapshot made from a proposal it gives the name the host shows for each sealed id to signed-in requesters only, looked up per request and never stored. `@payloadcms/ui` and `react` are new optional peer dependencies, and the plugin now depends on `@captiveorigins/onetaxonomy-react` for the drawing. Colours come from Payload's theme variables; a test reads the installed `@payloadcms/ui` stylesheets and checks text at 4.5:1 and control edges at 3:1 in light and dark, and a status is never colour alone. The example site shows the panels to its curator and turns Payload's Gravatar avatar off. Tested: 94 new tests (74 render the panels in jsdom with Payload's hooks and `fetch` replaced by stand-ins, 11 on the stylesheet, 9 on the summary against a real Payload with an in-memory MongoDB), and 8 new checks over HTTP in the example site (55 in all). The snapshot, view and proposal panels were also clicked through once in headless Chrome on the example site, in light and dark; that run is not in CI. Not tested: the import panel in a real admin, a screen reader, Safari, Firefox, a phone, or a custom Payload theme. Found on the way and written in the README: Payload's own admin loads its JSON editor from cdn.jsdelivr.net and, unless `admin.avatar` is `'default'`, asks gravatar.com with a hash of the signed-in person's email address. See [ADR 0012](docs/architecture/adr/0012-payload-plugin.md), point 16. +- Payload plugin, part 5 of M7: admin panels (still not on npm). Each of the plugin's four collections gets a panel at the top of its edit page, on unless `admin: { panels: false }`. The snapshot panel says the status in words, what the snapshot holds (counts, a line per classification and phylogeny, sources with their licenses) and, for one made by accepting a proposal, which proposal and the two sealed person ids; a manager gets Publish and Withdraw. The view panel draws the saved view (the first 200 nodes) with the same tree as an outline for the keyboard and the lines readers are told, and holds Publish, with the reason, until the form is saved and the snapshot is published. The import panel shows the steps of a run, its warnings and diagnostics, looks again every 3 seconds while a job runs, and offers to run a queued, failed or interrupted job. The proposal panel says what a proposal would change, and a manager accepts it (naming the new version) or declines it. Publish, Withdraw, Accept and Decline ask first in the page and say what can't be undone (running an import doesn't ask: it stores at most a staged snapshot); accepting lists what gets sealed for good and asks the manager to check it for personal details. The panels have no powers of their own: they call the host's site from the browser as the signed-in person, so the host's `access` callbacks decide, and a refusal reads "Nothing was changed." with the site's reason. New: the `@extantape/payload-plugin-onetaxonomy/client` entry (found through Payload's import map, so run `payload generate:importmap` after adding the plugin), the `admin.panels` option, and one endpoint, `GET /api//:id/summary` (and `summarizeSnapshot`), which answers counts and capped lists, never names or trees, with the same 404 as the other endpoints. For a snapshot made from a proposal it gives the name the host shows for each sealed id to signed-in requesters only, looked up per request and never stored. `@payloadcms/ui` and `react` are new optional peer dependencies, and the plugin now depends on `@extantape/onetaxonomy-react` for the drawing. Colours come from Payload's theme variables; a test reads the installed `@payloadcms/ui` stylesheets and checks text at 4.5:1 and control edges at 3:1 in light and dark, and a status is never colour alone. The example site shows the panels to its curator and turns Payload's Gravatar avatar off. Tested: 94 new tests (74 render the panels in jsdom with Payload's hooks and `fetch` replaced by stand-ins, 11 on the stylesheet, 9 on the summary against a real Payload with an in-memory MongoDB), and 8 new checks over HTTP in the example site (55 in all). The snapshot, view and proposal panels were also clicked through once in headless Chrome on the example site, in light and dark; that run is not in CI. Not tested: the import panel in a real admin, a screen reader, Safari, Firefox, a phone, or a custom Payload theme. Found on the way and written in the README: Payload's own admin loads its JSON editor from cdn.jsdelivr.net and, unless `admin.avatar` is `'default'`, asks gravatar.com with a hash of the signed-in person's email address. See [ADR 0012](docs/architecture/adr/0012-payload-plugin.md), point 16. - Benchmarks: frame times in a browser. `apps/playground/bench.html` (built with the playground) pans, zooms and scrolls made-up trees of 125 to 10,000 tips as SVG and on the canvas and times every frame; open it in any browser, press Run and save the numbers. `node scripts/bench-browser.mjs` runs it in the Chrome already installed, headless, at pixel density 1 and 2 and with the main thread slowed 4 times, and writes `docs/benchmarks-browser.md`; `--from ` adds a section from numbers saved in another browser. It uses a throwaway profile that it deletes, listens on this machine only and sends nothing anywhere. First numbers (headless Chrome 157, M2 Max, other work running): the canvas held 60 frames a second at every size up to 19,999 nodes, slowed thread included; SVG held it through 1,999 nodes, and through 499 on the slowed thread. A second section is from a headless build of WebKit 27.2 (Playwright's: the engine Safari and the Mac app use, but neither program), driven by hand as the doc describes, at normal speed only: the canvas held 60 frames a second at every size, and SVG held it through 1,999 nodes. When a browser's clock ticks in whole milliseconds, as WebKit's does, the script says so in the section and prints no decimal places. The 1,000-node switch is unchanged. Not measured: Safari itself, the Mac app's web view, Firefox, a window on a real screen, a real slow device, radial layouts. The benchmark page and script have no tests of their own. - Payload plugin docs: moving a departed person's proposals to another account. The README's "When someone leaves" now says that `author` and `decidedBy` can't be changed through Payload, that the lock is a hook and so doesn't cover a host's own database code, and that a host making that move there must change only those two fields and leave `proposal`, `checksum` and the snapshot alone. A new test makes the move on MongoDB and checks that the proposal still matches its checksum, the version made from it still verifies, and the proposal stays locked. No code changed; an option to allow the move through Payload was considered and not built (ADR 0012). - 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. @@ -17,18 +19,18 @@ - 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). 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 `+`, each moved placement names the new source record `proposal:` 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`. -- Payload plugin, part 3 of M7 (still not on npm). One run at a time: a run now claims its import job with one conditional update on the job's `attempts` counter, so when several runs of a job start at the same moment one runs it and the others get a 409 and change nothing. Before, both could proceed and one could fail on the unique index. On MongoDB the claim is a single `findOneAndUpdate`; it hasn't been checked on Postgres or SQLite, and a run still working after `staleAfterMinutes` can still be taken over. Views: every view read through Payload itself (REST, GraphQL, the Local API) has a `source` field, `{ doc, id, version, checksum, status, withdrawn }`, the same object the projection and export endpoints give, so a view says its snapshot was withdrawn wherever it is read. It isn't stored, a `source` sent with a view is ignored, it is `null` when the snapshot is gone, and a page of views reads each snapshot's status once. The withdrawn snapshot itself stays hidden from readers. ADR 0012's point 14 (a published view of a withdrawn snapshot stays readable and is flagged) was accepted by the area lead. `@payloadcms/graphql` is a dev dependency for the tests only. Tested on Payload 3.93 with an in-memory MongoDB, through the Local API and a real GraphQL query; not through Payload's HTTP routing, in the admin, or in a real site. -- Payload plugin, part 2 of M7 (`@captiveorigins/payload-plugin-onetaxonomy`, still not on npm; the Release workflow still skips it). Import jobs: with the new `imports` option, a `taxonomy-imports` collection takes a file from the host's upload collection, or a URL through a `fetch` the host passes (nothing is fetched otherwise), runs the matching OneTaxonomy adapter (CSV/TSV checklist, Darwin Core Archive, OneTaxonomy JSON, Newick, NEXUS, phyloXML), seals the result with core's `sealSnapshot`, checks the checksum, validates it and stores it as a staged snapshot. The job records its progress, the adapter's warnings and diagnostics with the line or position. A malformed file or a dataset with validation errors fails the job and stores nothing. The same file with the same settings stores nothing new and says "already imported". An interrupted job can be run again after `staleAfterMinutes` and starts from the file, and one that stopped after storing finds its own snapshot instead of storing it twice. Only managers, in their own tenant, can make, see or run jobs, and a job's input and results can't be edited. Jobs run when made, or through `POST /api/taxonomy-imports/:id/run` and `runImport`. Read API: `coverage` (core's `getCoverageReport`) and `export` (the sealed snapshot as stored) on snapshots, `projection` (core's `projectView` with `describeView`'s disclosure) and `export` on views, with part 1's access, limits and 404 rules. ADR 0012, point 14: a published view of a withdrawn snapshot stays readable and is flagged (`source.withdrawn`, and a "Source withdrawn" disclosure line), never silently hidden. Tested on Payload 3.93 with an in-memory MongoDB and a host fetch answered from memory; not run in a real site, through HTTP routing, with cloud upload storage or over a real network. +- Docs: the root README and the limitations page said the packages weren't on npm. The five `onetaxonomy-*` packages are, as alpha versions; the Payload plugin isn't. +- OneOrigin brand copy: round joins (owner decision, 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`. +- Payload plugin, part 3 of M7 (still not on npm). One run at a time: a run now claims its import job with one conditional update on the job's `attempts` counter, so when several runs of a job start at the same moment one runs it and the others get a 409 and change nothing. Before, both could proceed and one could fail on the unique index. On MongoDB the claim is a single `findOneAndUpdate`; it hasn't been checked on Postgres or SQLite, and a run still working after `staleAfterMinutes` can still be taken over. Views: every view read through Payload itself (REST, GraphQL, the Local API) has a `source` field, `{ doc, id, version, checksum, status, withdrawn }`, the same object the projection and export endpoints give, so a view says its snapshot was withdrawn wherever it is read. It isn't stored, a `source` sent with a view is ignored, it is `null` when the snapshot is gone, and a page of views reads each snapshot's status once. The withdrawn snapshot itself stays hidden from readers. ADR 0012's point 14 (a published view of a withdrawn snapshot stays readable and is flagged) was accepted by the maintainers. `@payloadcms/graphql` is a dev dependency for the tests only. Tested on Payload 3.93 with an in-memory MongoDB, through the Local API and a real GraphQL query; not through Payload's HTTP routing, in the admin, or in a real site. +- Payload plugin, part 2 of M7 (`@extantape/payload-plugin-onetaxonomy`, still not on npm; the Release workflow still skips it). Import jobs: with the new `imports` option, a `taxonomy-imports` collection takes a file from the host's upload collection, or a URL through a `fetch` the host passes (nothing is fetched otherwise), runs the matching OneTaxonomy adapter (CSV/TSV checklist, Darwin Core Archive, OneTaxonomy JSON, Newick, NEXUS, phyloXML), seals the result with core's `sealSnapshot`, checks the checksum, validates it and stores it as a staged snapshot. The job records its progress, the adapter's warnings and diagnostics with the line or position. A malformed file or a dataset with validation errors fails the job and stores nothing. The same file with the same settings stores nothing new and says "already imported". An interrupted job can be run again after `staleAfterMinutes` and starts from the file, and one that stopped after storing finds its own snapshot instead of storing it twice. Only managers, in their own tenant, can make, see or run jobs, and a job's input and results can't be edited. Jobs run when made, or through `POST /api/taxonomy-imports/:id/run` and `runImport`. Read API: `coverage` (core's `getCoverageReport`) and `export` (the sealed snapshot as stored) on snapshots, `projection` (core's `projectView` with `describeView`'s disclosure) and `export` on views, with part 1's access, limits and 404 rules. ADR 0012, point 14: a published view of a withdrawn snapshot stays readable and is flagged (`source.withdrawn`, and a "Source withdrawn" disclosure line), never silently hidden. Tested on Payload 3.93 with an in-memory MongoDB and a host fetch answered from memory; not run in a real site, through HTTP routing, with cloud upload storage or over a real network. -- React explorer: the drawing follows the outline's selection (ADR 0010's follow-up, K495). Choosing a taxon in the outline pans the drawing, SVG or canvas, when the taxon is off screen or within 40 px of the edge, and only then, the shortest way. With several taxa selected it keeps them all in view, zooming out only as far as they need; it never zooms in. The pan takes 200 ms, or jumps when reduced motion is asked for or can't be read. Focus stays in the outline, a plain pan announces nothing, and a zoom out says "Zoomed out to show the selection." once (new label `zoomedToSelection`). A click in the drawing doesn't move it, and panning or zooming by hand stops a pan in progress. Tested in jsdom with the frame clock and `matchMedia` stubbed; not tried in a browser, and no screen reader was used. -- Payload plugin, part 1 of M7 (`@captiveorigins/payload-plugin-onetaxonomy` in `packages/payload`, name approved by the owner 2026-11-11, N158; not on npm, and the Release workflow skips it until its first version is published by hand). A `taxonomy-snapshots` collection stores each sealed snapshot whole: one without a checksum, with a checksum that doesn't match, of another schema version or with validation errors is refused, its content can't change afterwards (not even from server code), the same id and version can't be stored twice, and the checksum is checked again when the API first reads it, so a snapshot changed in the database is refused rather than answered from. Snapshots start staged, are published by a signed-in person (recorded), and are withdrawn rather than deleted. A `taxonomy-views` collection holds views that must pin their snapshot's id, version and checksum and pass `validateView`; published views are locked. Read endpoints for name search (exact then prefix, every concept sharing a name listed, common names only in the locale asked for), ancestors, descendants and MRCA run as the requester, cap their results, answer 404 for anything the requester can't read, refuse MRCA on a classification, and answer `indeterminate` in unrooted trees. Access goes through the host's `read` and `manage` callbacks, where only `true` allows; an optional tenant scope keeps tenants apart, cross-tenant reads included. ADR 0012 (proposed) records the design and the later parts. Tested on Payload 3.93 with an in-memory MongoDB; not run in a real site or through Payload's HTTP routing. +- React explorer: the drawing follows the outline's selection (ADR 0010's follow-up). Choosing a taxon in the outline pans the drawing, SVG or canvas, when the taxon is off screen or within 40 px of the edge, and only then, the shortest way. With several taxa selected it keeps them all in view, zooming out only as far as they need; it never zooms in. The pan takes 200 ms, or jumps when reduced motion is asked for or can't be read. Focus stays in the outline, a plain pan announces nothing, and a zoom out says "Zoomed out to show the selection." once (new label `zoomedToSelection`). A click in the drawing doesn't move it, and panning or zooming by hand stops a pan in progress. Tested in jsdom with the frame clock and `matchMedia` stubbed; not tried in a browser, and no screen reader was used. +- Payload plugin, part 1 of M7 (`@extantape/payload-plugin-onetaxonomy` in `packages/payload`, name approved by the owner 2026-11-11; not on npm, and the Release workflow skips it until its first version is published by hand). A `taxonomy-snapshots` collection stores each sealed snapshot whole: one without a checksum, with a checksum that doesn't match, of another schema version or with validation errors is refused, its content can't change afterwards (not even from server code), the same id and version can't be stored twice, and the checksum is checked again when the API first reads it, so a snapshot changed in the database is refused rather than answered from. Snapshots start staged, are published by a signed-in person (recorded), and are withdrawn rather than deleted. A `taxonomy-views` collection holds views that must pin their snapshot's id, version and checksum and pass `validateView`; published views are locked. Read endpoints for name search (exact then prefix, every concept sharing a name listed, common names only in the locale asked for), ancestors, descendants and MRCA run as the requester, cap their results, answer 404 for anything the requester can't read, refuse MRCA on a classification, and answer `indeterminate` in unrooted trees. Access goes through the host's `read` and `manage` callbacks, where only `true` allows; an optional tenant scope keeps tenants apart, cross-tenant reads included. ADR 0012 (proposed) records the design and the later parts. Tested on Payload 3.93 with an in-memory MongoDB; not run in a real site or through Payload's HTTP routing. -- One OneOrigin brand source (owner decision N105, 2026-11-11): the brand now has one source, OnePhylogeny's `docs/oneorigin/`, and this repo carries a copy in `brand/oneorigin/`: the accepted suite guidelines, the brand text, `tokens.json` and the shared icon parts. `brand/oneorigin.lock` records the source commit and a sha256 per file, and CI runs `node scripts/oneorigin.mjs check`, which fails on a hand edit or a stale built file and warns when the source differs. `apps/studio/src/oneorigin.css` is built from the tokens under `--ot-` and loaded before `app.css`; it adds variables and changes no color in use. The explorer's own `--ot-` colors are set on `.ot-root` and its siblings by the React package, and the studio's appearance overrides must equal them (an existing test), so none was a clean swap; they're unchanged, and the React package isn't touched. `brand/README.md` is now a short pointer, identical in the three tools. +- One OneOrigin brand source (owner decision, 2026-11-11): the brand now has one source, OnePhylogeny's `docs/oneorigin/`, and this repo carries a copy in `brand/oneorigin/`: the accepted suite guidelines, the brand text, `tokens.json` and the shared icon parts. `brand/oneorigin.lock` records the source commit and a sha256 per file, and CI runs `node scripts/oneorigin.mjs check`, which fails on a hand edit or a stale built file and warns when the source differs. `apps/studio/src/oneorigin.css` is built from the tokens under `--ot-` and loaded before `app.css`; it adds variables and changes no color in use. The explorer's own `--ot-` colors are set on `.ot-root` and its siblings by the React package, and the studio's appearance overrides must equal them (an existing test), so none was a clean swap; they're unchanged, and the React package isn't touched. `brand/README.md` is now a short pointer, identical in the three tools. - `.onetaxonomy` bundles. Import: `writeBundle` and `readBundle` write and read a ZIP holding `manifest.json` (format version 1, when and by what it was made, each file's size and SHA-256, and the snapshots its views and lessons were made with, pinned by id, version and checksum), an optional embedded sealed snapshot, views, lessons and media, all plain JSON or the media's own format, so `unzip` reads it (format in `docs/architecture/bundle-format.md`). `readBundle` checks everything before returning anything and names each problem with its file: not a ZIP, no manifest, bad JSON, an unknown format version, a missing file, a size or checksum mismatch, a snapshot that isn't the one pinned, a view pinned to nothing, a path outside its folder. A damaged bundle is refused whole. `writeZip` and `crc32` are exported, and core gains `sha256Bytes`. Mac app: Save as bundle… asks whether to include the dataset, lists what goes in and anything that will point to the version on screen, and saves with the same temporary-file-and-rename path as other saves (Cancel writes nothing, and the opened file is never written over); Open… reads `.onetaxonomy` files, with problems in the alert region and what was open left as it was. When a bundle pins another version of the open dataset, the app asks whether to keep the pinned one (when it's embedded) or relink to the open one; relinking checks every view and lesson first and is refused whole if one doesn't fit. Lessons and media are carried through but not shown yet. The app may now write binary files (`fs:allow-write-file`), which a test checks. No SQLite: ADR 0011 (proposed) says why plain files and the bundle cover M6's persistence needs, and what would change that. Tested with Tauri mocked and the system `unzip`; not tried in the built app, and no screen reader was used. - React: a canvas renderer for large trees. `TreeView` draws SVG up to 1,000 drawn nodes and a canvas above that (`renderer="auto"`, the default; `canvasThreshold` changes the number, and `renderer="svg"` or `"canvas"` forces one). The canvas uses the same layouts, highlight shapes and colors, and the same title and description as the SVG; it is one `role="img"`, and the outline stays the keyboard path (ADR 0010). Level of detail: names and support values are drawn only when their text would be at least 9 px tall and doesn't overlap one already placed (in radial layouts, when the next leaf is far enough away); the selection's name is always drawn. A subtree whose leaves would sit within one pixel is drawn as one wedge, and one holding a highlight shows it. What that leaves out is counted and said in the description and in a visible note ("891 branches are too small to draw at this zoom and are shown as wedges holding 18,218 nodes. …", for a made-up tree of 10,000 tips). Clicks are hit-tested, so selecting, double-clicking to fold and dragging work as on the SVG, and the canvas is drawn at the screen's pixel density. In the explorer, PNG download draws the canvas with the disclosure underneath; SVG download on a canvas tree says why it isn't available instead of doing nothing. `planCanvas`, `prepareTree`, `hitTest`, `drawPlan` and `describeLod` are exported for other renderers. `onNodeClick`'s event is now `MouseEvent`, since it can come from the canvas. Below the threshold the SVG is unchanged (a snapshot test holds it). Benchmarks of the planning steps in Node are in `docs/benchmarks.md`; nothing has been run in a browser yet, so paint time and smoothness are unmeasured. -- Focus ring in the darker orange on light backgrounds (owner decision N154, 2026-11-11): `--ot-focus` is now `#a65f20` in light (4.91:1 on the surface, 4.61:1 on the panel, where `#cf8137` was 3.07:1 and 2.88:1), and the Mac app's focus rings use a new `--focus` (`#a65f20` in light, 4.63:1 on the toolbar, where `--accent` was 2.89:1). Dark mode keeps `#e7a066`. The brand orange stays for fills, accents and selection. `brand/README.md` records the rule in all three One tools. +- Focus ring in the darker orange on light backgrounds (owner decision, 2026-11-11): `--ot-focus` is now `#a65f20` in light (4.91:1 on the surface, 4.61:1 on the panel, where `#cf8137` was 3.07:1 and 2.88:1), and the Mac app's focus rings use a new `--focus` (`#a65f20` in light, 4.63:1 on the toolbar, where `--accent` was 2.89:1). Dark mode keeps `#e7a066`. The brand orange stays for fills, accents and selection. `brand/README.md` records the rule in all three One tools. - React `TipLinker`: where a tip's name matches nothing exactly, a separate "Possible matches, not exact" list offers near names: underscores, spacing or letter case aside; a trailing specimen code or voucher left off ("Chelonoidis_carbonarius_MZUSP1234"); or one letter different (names of five letters or more). Each is labelled not exact with the rule that found it, is linked only when the person chooses it (a toggle button, or the row's list), is never taken by "Accept all exact unique matches", and is recorded as `near-name-confirmed` with the name and rule. `nearNameMatches`, `withoutSpecimenCode` and `isOneEdit` are exported. Trees over 200 tips (`pageSize`) are paged, with Previous and Next and a "List tips" filter by kind of match. Highlights and trait states have twelve shapes instead of six (star, plus, cross, left and right triangles, hourglass added), in the drawing and the legend. Drawing nodes stay out of the Tab order, with the reasons in ADR 0010 (proposed). No screen reader was used. - Core: `describeView` now says how a rooted hypothesis was rooted, on the line after the hypothesis: "Rooting (outgroup): …" with the record's note, or the outgroup and source record when there's no note. Every caller and export gets it, not only the React explorer, which now shows core's line instead of adding its own; the explorer's `rooting` label is gone. Mac app and explorer: the two dropdowns shown as "Show" have their own names, "Show tree or classification" (the Mac app's header) and "Show living or extinct" (the explorer's filter, label `statusFilter`). No screen reader was used. @@ -77,7 +79,7 @@ Fixes saving in the Mac app. 0.1.0 wasn't given permission to rename its tempora - First release of OneTaxonomy for Mac. - The Mac app can't update itself yet. Releases have no signing key, so download the new DMG instead. Signed updates can come in a later release. - Documentation: names the OneOrigin suite. -- The repository moved to github.com/CaptiveOrigins, including the address the Mac app checks for updates. +- The repository moved to a new GitHub home, including the address the Mac app checks for updates. ## 0.1.0-alpha.2 diff --git a/README.md b/README.md index 3aa1e42..2fb7f35 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ A source-available framework for importing, curating, browsing and publishing **scientifically faithful** taxonomic classifications and phylogenetic trees: for developers, and for teachers, museums, researchers and animal keepers who don't write code. -Part of the [OneOrigin suite](https://github.com/CaptiveOrigins/onephylogeny/blob/main/docs/oneorigin/README.md): independent tools, with no subscriptions, that work alone or together. +Part of the [OneOrigin suite](https://github.com/ExtantApe/OnePhylogeny/blob/main/docs/oneorigin/README.md): independent tools, with no subscriptions, that work alone or together. -> **Status: early (Phase 1, milestone 4 of 8).** The pure TypeScript core, the importers (Newick, NEXUS, phyloXML, CSV/TSV checklists, Darwin Core Archives, OneTaxonomy JSON) and curated selective imports with coverage reports, views with undo/redo, layouts and the React explorer work and are tested (see `apps/playground`). A first Mac app (`apps/studio`) opens these files and saves views. Profiles and the Studio's project files are planned, and the Payload plugin has its first four parts (snapshots, views, import jobs, proposals and a small read API, tested on an in-memory database) and a small example site (`apps/payload-example`); see [the phase plan](docs/architecture/phase-plan.md). The five `@captiveorigins/onetaxonomy-*` packages are on npm as alpha versions; the Payload plugin isn't yet. +> **Status: early (Phase 1, milestone 4 of 8).** The pure TypeScript core, the importers (Newick, NEXUS, phyloXML, CSV/TSV checklists, Darwin Core Archives, OneTaxonomy JSON) and curated selective imports with coverage reports, views with undo/redo, layouts and the React explorer work and are tested (see `apps/playground`). A first Mac app (`apps/studio`) opens these files and saves views. Profiles and the Studio's project files are planned, and the Payload plugin has its first four parts (snapshots, views, import jobs, proposals and a small read API, tested on an in-memory database) and a small example site (`apps/payload-example`); see [the phase plan](docs/architecture/phase-plan.md). The five `onetaxonomy-*` packages are on npm as alpha versions, under `@extantape` from 0.1.0-alpha.4; the Payload plugin isn't yet. ## Principles @@ -20,12 +20,12 @@ The full rules are in [scientific invariants](docs/architecture/scientific-invar | Package | What it does | Status | | --- | --- | --- | -| [`@captiveorigins/onetaxonomy-core`](packages/core) | Records, validation, bounded traversal, MRCA, tri-state monophyly, curated selections, coverage reports, snapshot checksums and diffs, views (actions, undo/redo, filters, highlights, projection), JSON Schema. No dependencies; runs anywhere. | Working | -| [`@captiveorigins/onetaxonomy-import`](packages/import) | Source adapters (CSV/TSV, Darwin Core Archive, JSON, Newick, NEXUS, phyloXML), reconciliation, selective imports, community presets | Working | -| [`@captiveorigins/onetaxonomy-layout`](packages/layout) | Rectangular, phylogram/time (only when valid), radial and sunburst layouts as plain coordinates | Working | -| [`@captiveorigins/onetaxonomy-react`](packages/react) | Server-renderable SVG tree, accessible keyboard outline, explorer (undo/redo, drag, overlays, inspector, clade selector, SVG/PNG/JSON exports), lesson player | Working | -| [`@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, a bounded read API (name search, ancestors, descendants, MRCA, coverage, projected views, exports) behind the host's access rules, with optional tenant scope, and admin panels for publishing, import progress and deciding on proposals | Early (M7 parts 1 to 5): tested on Payload with an in-memory MongoDB and in the example site on this machine, not yet in a deployed site. Not on npm | +| [`@extantape/onetaxonomy-core`](packages/core) | Records, validation, bounded traversal, MRCA, tri-state monophyly, curated selections, coverage reports, snapshot checksums and diffs, views (actions, undo/redo, filters, highlights, projection), JSON Schema. No dependencies; runs anywhere. | Working | +| [`@extantape/onetaxonomy-import`](packages/import) | Source adapters (CSV/TSV, Darwin Core Archive, JSON, Newick, NEXUS, phyloXML), reconciliation, selective imports, community presets | Working | +| [`@extantape/onetaxonomy-layout`](packages/layout) | Rectangular, phylogram/time (only when valid), radial and sunburst layouts as plain coordinates | Working | +| [`@extantape/onetaxonomy-react`](packages/react) | Server-renderable SVG tree, accessible keyboard outline, explorer (undo/redo, drag, overlays, inspector, clade selector, SVG/PNG/JSON exports), lesson player | Working | +| [`@extantape/onetaxonomy-education`](packages/education) | Lesson decks (scenes, cameras, annotations, reveals) and worksheets with answers computed from the data | Working | +| [`@extantape/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, a bounded read API (name search, ancestors, descendants, MRCA, coverage, projected views, exports) behind the host's access rules, with optional tenant scope, and admin panels for publishing, import progress and deciding on proposals | Early (M7 parts 1 to 5): tested on Payload with an in-memory MongoDB and in the example site on this machine, not yet in a deployed 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 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 | @@ -35,11 +35,11 @@ The full rules are in [scientific invariants](docs/architecture/scientific-invar pnpm install pnpm build pnpm test -pnpm --filter @captiveorigins/onetaxonomy-examples start +pnpm --filter @extantape/onetaxonomy-examples start ``` ```js -import { assessTaxon, indexDataset } from '@captiveorigins/onetaxonomy-core' +import { assessTaxon, indexDataset } from '@extantape/onetaxonomy-core' const data = indexDataset(snapshot) const result = assessTaxon(data.classification('traditional'), 'c:reptilia', data.phylogeny('archelosauria')) @@ -49,7 +49,7 @@ const result = assessTaxon(data.classification('traditional'), 'c:reptilia', dat Curate a selective import from a checklist (see `examples/selective-import.mjs`): ```js -import { csvAdapter, selectiveImport, toSnapshot } from '@captiveorigins/onetaxonomy-import' +import { csvAdapter, selectiveImport, toSnapshot } from '@extantape/onetaxonomy-import' const read = await csvAdapter({ source: mySource }).read(csvText) // Darwin Core column names by default const full = toSnapshot(read, { id: 'my-checklist', version: '2026-03', createdAt }) @@ -68,4 +68,4 @@ const { snapshot, coverage } = selectiveImport(full, { ## License -[PolyForm Noncommercial 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0) for the software: free for noncommercial use, with attribution; commercial use needs a separate license from Captive Origins. Versions published before this change remain under MIT. Data you import keeps its own license; OneTaxonomy records it with every snapshot. +[PolyForm Noncommercial 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0) for the software: free for noncommercial use, with attribution; commercial use needs a separate license from EXTANT APE. Versions published before this change remain under MIT. Data you import keeps its own license; OneTaxonomy records it with every snapshot. diff --git a/RELEASING.md b/RELEASING.md index 6e1daeb..fc79b65 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -4,12 +4,12 @@ One description everywhere: each `package.json` (`description`, `keywords`) is t 1. Bump the same `version` in every `packages/*/package.json` (lockstep, ADR 0007), update `CHANGELOG.md`, commit as `Release x.y.z`, tag `vx.y.z`, and push the commit and the tag. 2. The Release workflow builds, tests and **stages** every package on npm (trusted publishing, no stored token). Prereleases go to the `alpha` dist-tag once a package has a stable version; before that they are also `latest`. -3. The maintainer account approves each with 2FA: npmjs.com → avatar → **Staged Packages** → **Approve**, or `npm stage list` then `npm stage approve `. Only then are they public. +3. The `mdgilardi` npm account, which owns the `@extantape` scope, approves each with 2FA: npmjs.com → avatar → **Staged Packages** → **Approve**, or `npm stage list` then `npm stage approve `. Only then are they public. -`@captiveorigins/payload-plugin-onetaxonomy` (`packages/payload`) moves in lockstep but isn't staged yet: the Release workflow skips it until the owner has published its first version by hand and set up its trust. Then remove `grep -v -x payload` from the workflow's stage step. +`@extantape/payload-plugin-onetaxonomy` (`packages/payload`) moves in lockstep but isn't staged yet: the Release workflow skips it until the owner has published its first version by hand and set up its trust. Then remove `grep -v -x payload` from the workflow's stage step. The first version of each package is published by hand (`pnpm publish --access public` in each `packages/*` folder, `core` first; pnpm turns `workspace:^` into the real version), because npm can only trust a workflow for a package that exists; then, once per package: ```bash -npm trust github --repo CaptiveOrigins/onetaxonomy --file release.yml --allow-stage-publish --yes +npm trust github --repo ExtantApe/OneTaxonomy --file release.yml --allow-stage-publish --yes ``` diff --git a/apps/payload-example/README.md b/apps/payload-example/README.md index cb7ff51..bcb993f 100644 --- a/apps/payload-example/README.md +++ b/apps/payload-example/README.md @@ -1,6 +1,6 @@ # 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, import jobs over uploaded files, and proposals that record a person by an id, with a public handle shown beside it and no email address. +A small Payload and Next.js site that uses [`@extantape/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, import jobs over uploaded files, 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: 74 checks over HTTP with sign-in cookies (`pnpm smoke`, in CI), on `next dev` and on the built site, with Payload 3.93 and Next.js 16; the admin's panels clicked through in headless Chrome as the curator, in light and dark (`pnpm click-through`, 40 checks, not in CI: it needs Chrome and the network); and the three public pages looked at in headless Chrome by hand, in light and dark, at desktop width and at 520 px. Not tested: a screen reader, Safari, Firefox, a phone, Postgres or SQLite, or a deployed site. @@ -24,7 +24,7 @@ From the repository root, with Node 22 or later and pnpm: ```bash pnpm install pnpm build # the packages the site uses -pnpm --filter @captiveorigins/onetaxonomy-payload-example dev +pnpm --filter @extantape/onetaxonomy-payload-example dev ``` The first run downloads a MongoDB binary (about 140 MB on a Mac, kept in `~/.cache/mongodb-binaries`). Then it prints the site's address, the two accounts and a password made up for this run: @@ -48,7 +48,7 @@ Stop it with Ctrl-C. The database and the uploaded files go with it: every run s ## Check it ```bash -pnpm --filter @captiveorigins/onetaxonomy-payload-example smoke +pnpm --filter @extantape/onetaxonomy-payload-example smoke ``` This builds the site, starts it on a free port with its own database, and sends it requests the way a browser would. It takes about 15 seconds on a recent Mac. It checks that: @@ -76,7 +76,7 @@ This builds the site, starts it on a free port with its own database, and sends ### In a browser ```bash -pnpm --filter @captiveorigins/onetaxonomy-payload-example click-through +pnpm --filter @extantape/onetaxonomy-payload-example click-through ``` This builds the site, starts it the same way, opens a Chrome or Chromium already on the machine with no window and a throwaway profile, signs in as the curator and does what a curator would: publishes a snapshot with the keyboard, changes and saves a draft view, runs the three import jobs, renames a finished one with Payload's own Save, accepts a proposal into a new version, pastes a sealed snapshot into the new-snapshot form, and publishes it with Payload's own status field. It takes about two minutes. It checks what the panels say at each step, what was sent, that the form under a panel doesn't repeat it, that the panel's parts are spaced apart as the browser lays them out, and that no page asks any site but this one, apart from Payload's code editor. diff --git a/apps/payload-example/app/(payload)/admin/importMap.js b/apps/payload-example/app/(payload)/admin/importMap.js index 2355697..2261072 100644 --- a/apps/payload-example/app/(payload)/admin/importMap.js +++ b/apps/payload-example/app/(payload)/admin/importMap.js @@ -1,14 +1,14 @@ -import { SnapshotPanel as SnapshotPanel_577f3b5b503ff777fe77b01de8cacfa5 } from '@captiveorigins/payload-plugin-onetaxonomy/client' -import { ViewPanel as ViewPanel_577f3b5b503ff777fe77b01de8cacfa5 } from '@captiveorigins/payload-plugin-onetaxonomy/client' -import { ImportPanel as ImportPanel_577f3b5b503ff777fe77b01de8cacfa5 } from '@captiveorigins/payload-plugin-onetaxonomy/client' -import { ProposalPanel as ProposalPanel_577f3b5b503ff777fe77b01de8cacfa5 } from '@captiveorigins/payload-plugin-onetaxonomy/client' +import { SnapshotPanel as SnapshotPanel_6db27829aa354dff1e248cee79348b1f } from '@extantape/payload-plugin-onetaxonomy/client' +import { ViewPanel as ViewPanel_6db27829aa354dff1e248cee79348b1f } from '@extantape/payload-plugin-onetaxonomy/client' +import { ImportPanel as ImportPanel_6db27829aa354dff1e248cee79348b1f } from '@extantape/payload-plugin-onetaxonomy/client' +import { ProposalPanel as ProposalPanel_6db27829aa354dff1e248cee79348b1f } from '@extantape/payload-plugin-onetaxonomy/client' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' /** @type import('payload').ImportMap */ export const importMap = { - "@captiveorigins/payload-plugin-onetaxonomy/client#SnapshotPanel": SnapshotPanel_577f3b5b503ff777fe77b01de8cacfa5, - "@captiveorigins/payload-plugin-onetaxonomy/client#ViewPanel": ViewPanel_577f3b5b503ff777fe77b01de8cacfa5, - "@captiveorigins/payload-plugin-onetaxonomy/client#ImportPanel": ImportPanel_577f3b5b503ff777fe77b01de8cacfa5, - "@captiveorigins/payload-plugin-onetaxonomy/client#ProposalPanel": ProposalPanel_577f3b5b503ff777fe77b01de8cacfa5, + "@extantape/payload-plugin-onetaxonomy/client#SnapshotPanel": SnapshotPanel_6db27829aa354dff1e248cee79348b1f, + "@extantape/payload-plugin-onetaxonomy/client#ViewPanel": ViewPanel_6db27829aa354dff1e248cee79348b1f, + "@extantape/payload-plugin-onetaxonomy/client#ImportPanel": ImportPanel_6db27829aa354dff1e248cee79348b1f, + "@extantape/payload-plugin-onetaxonomy/client#ProposalPanel": ProposalPanel_6db27829aa354dff1e248cee79348b1f, "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } diff --git a/apps/payload-example/app/(site)/layout.tsx b/apps/payload-example/app/(site)/layout.tsx index 82d12b5..95759fd 100644 --- a/apps/payload-example/app/(site)/layout.tsx +++ b/apps/payload-example/app/(site)/layout.tsx @@ -1,4 +1,4 @@ -import '@captiveorigins/onetaxonomy-react/styles.css' +import '@extantape/onetaxonomy-react/styles.css' import './site.css' import type { Metadata } from 'next' diff --git a/apps/payload-example/app/(site)/site.css b/apps/payload-example/app/(site)/site.css index edb7bcf..d9c1476 100644 --- a/apps/payload-example/app/(site)/site.css +++ b/apps/payload-example/app/(site)/site.css @@ -1,4 +1,4 @@ -/* The example site's own page styles. The trees are styled by @captiveorigins/onetaxonomy-react/styles.css. */ +/* The example site's own page styles. The trees are styled by @extantape/onetaxonomy-react/styles.css. */ :root { --ink: #4c312c; --text: #1d2330; diff --git a/apps/payload-example/app/(site)/views/[id]/ViewFigure.tsx b/apps/payload-example/app/(site)/views/[id]/ViewFigure.tsx index 83dbccc..0466d2d 100644 --- a/apps/payload-example/app/(site)/views/[id]/ViewFigure.tsx +++ b/apps/payload-example/app/(site)/views/[id]/ViewFigure.tsx @@ -1,5 +1,5 @@ -import type { Highlight, LayoutKind, Projection } from '@captiveorigins/onetaxonomy-core' -import { layoutFor, TreeOutline, treeViewBox, TreeView } from '@captiveorigins/onetaxonomy-react' +import type { Highlight, LayoutKind, Projection } from '@extantape/onetaxonomy-core' +import { layoutFor, TreeOutline, treeViewBox, TreeView } from '@extantape/onetaxonomy-react' /** The parts of a stored view the drawing needs. */ export type StoredView = { layout: { kind: LayoutKind }; highlights: Highlight[] } diff --git a/apps/payload-example/app/(site)/views/[id]/page.tsx b/apps/payload-example/app/(site)/views/[id]/page.tsx index 4322321..338cd9e 100644 --- a/apps/payload-example/app/(site)/views/[id]/page.tsx +++ b/apps/payload-example/app/(site)/views/[id]/page.tsx @@ -1,4 +1,4 @@ -import { projectedView } from '@captiveorigins/payload-plugin-onetaxonomy' +import { projectedView } from '@extantape/payload-plugin-onetaxonomy' import config from '@payload-config' import type { Metadata } from 'next' import { notFound } from 'next/navigation' diff --git a/apps/payload-example/package.json b/apps/payload-example/package.json index afbadb0..b8ca93b 100644 --- a/apps/payload-example/package.json +++ b/apps/payload-example/package.json @@ -1,5 +1,5 @@ { - "name": "@captiveorigins/onetaxonomy-payload-example", + "name": "@extantape/onetaxonomy-payload-example", "private": true, "type": "module", "description": "A small Payload site that uses the OneTaxonomy plugin: a seeded snapshot, a published view drawn on a public page, and the plugin's API behind Payload's sign-in.", @@ -12,9 +12,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@captiveorigins/onetaxonomy-core": "workspace:^", - "@captiveorigins/onetaxonomy-react": "workspace:^", - "@captiveorigins/payload-plugin-onetaxonomy": "workspace:^", + "@extantape/onetaxonomy-core": "workspace:^", + "@extantape/onetaxonomy-react": "workspace:^", + "@extantape/payload-plugin-onetaxonomy": "workspace:^", "@payloadcms/db-mongodb": "3.93.0", "@payloadcms/next": "3.93.0", "graphql": "^16.15.0", diff --git a/apps/payload-example/payload.config.ts b/apps/payload-example/payload.config.ts index 7fd13e0..3d768c5 100644 --- a/apps/payload-example/payload.config.ts +++ b/apps/payload-example/payload.config.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' -import { oneTaxonomy } from '@captiveorigins/payload-plugin-onetaxonomy' +import { oneTaxonomy } from '@extantape/payload-plugin-onetaxonomy' import { mongooseAdapter } from '@payloadcms/db-mongodb' import { buildConfig } from 'payload' diff --git a/apps/payload-example/scripts/smoke.mjs b/apps/payload-example/scripts/smoke.mjs index b95cfaa..2cdc482 100644 --- a/apps/payload-example/scripts/smoke.mjs +++ b/apps/payload-example/scripts/smoke.mjs @@ -14,7 +14,7 @@ import { createServer } from 'node:net' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { verifySnapshot } from '@captiveorigins/onetaxonomy-core' +import { verifySnapshot } from '@extantape/onetaxonomy-core' const flags = new Set(process.argv.slice(2)) const unknown = [...flags].filter((f) => !['--dev', '--skip-build'].includes(f)) diff --git a/apps/payload-example/src/seed.ts b/apps/payload-example/src/seed.ts index 71a02ae..ce43cd6 100644 --- a/apps/payload-example/src/seed.ts +++ b/apps/payload-example/src/seed.ts @@ -1,8 +1,8 @@ import { readFile } from 'node:fs/promises' import path from 'node:path' -import { createView, indexDataset, sealSnapshot, type DatasetSnapshot } from '@captiveorigins/onetaxonomy-core' -import { runImport } from '@captiveorigins/payload-plugin-onetaxonomy' +import { createView, indexDataset, sealSnapshot, type DatasetSnapshot } from '@extantape/onetaxonomy-core' +import { runImport } from '@extantape/payload-plugin-onetaxonomy' import { createLocalReq, type Payload, type TypedUser } from 'payload' import amniotes from '../../../fixtures/amniotes.json' diff --git a/apps/playground/app.tsx b/apps/playground/app.tsx index 1784350..ec2b1e2 100644 --- a/apps/playground/app.tsx +++ b/apps/playground/app.tsx @@ -1,6 +1,6 @@ -import { createView, indexDataset, makeHighlight, type DatasetSnapshot, type TaxonomicView } from '@captiveorigins/onetaxonomy-core' -import { createLesson, sceneFromView, type LessonDeck } from '@captiveorigins/onetaxonomy-education' -import { Explorer, LessonPlayer } from '@captiveorigins/onetaxonomy-react' +import { createView, indexDataset, makeHighlight, type DatasetSnapshot, type TaxonomicView } from '@extantape/onetaxonomy-core' +import { createLesson, sceneFromView, type LessonDeck } from '@extantape/onetaxonomy-education' +import { Explorer, LessonPlayer } from '@extantape/onetaxonomy-react' import { useMemo, useState } from 'react' import { createRoot } from 'react-dom/client' @@ -10,7 +10,7 @@ import structures from '../../fixtures/structures.json' /** * Playground (M4, M5 overlays and exports): the explorer over the illustrative fixtures. Build with - * `pnpm --filter @captiveorigins/onetaxonomy-playground build`, then open dist/index.html. + * `pnpm --filter @extantape/onetaxonomy-playground build`, then open dist/index.html. */ // M5 overlays: an illustrative trait set and support values added to the fixtures here (not science). const am = indexDataset({ diff --git a/apps/playground/bench.tsx b/apps/playground/bench.tsx index 1264ea4..c85f16b 100644 --- a/apps/playground/bench.tsx +++ b/apps/playground/bench.tsx @@ -9,9 +9,9 @@ // browser holds the next frame back, so a steady frame time means the page kept up, not that painting was free. import { flushSync } from 'react-dom' import { createRoot } from 'react-dom/client' -import { createView, indexDataset, projectView, type DatasetSnapshot } from '@captiveorigins/onetaxonomy-core' -import { rectangular } from '@captiveorigins/onetaxonomy-layout' -import { canvasPlanOf, TreeView, type ViewBox } from '@captiveorigins/onetaxonomy-react' +import { createView, indexDataset, projectView, type DatasetSnapshot } from '@extantape/onetaxonomy-core' +import { rectangular } from '@extantape/onetaxonomy-layout' +import { canvasPlanOf, TreeView, type ViewBox } from '@extantape/onetaxonomy-react' const STAGE = 1000 // CSS pixels, square const FRAMES = 180 diff --git a/apps/playground/package.json b/apps/playground/package.json index f90fdaa..19d7365 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -1,17 +1,17 @@ { - "name": "@captiveorigins/onetaxonomy-playground", + "name": "@extantape/onetaxonomy-playground", "private": true, "type": "module", "scripts": { "build": "esbuild app.tsx --bundle --format=iife --minify --outfile=dist/app.js --jsx=automatic --define:process.env.NODE_ENV='\"production\"' && esbuild bench.tsx --bundle --format=iife --minify --outfile=dist/bench.js --jsx=automatic --define:process.env.NODE_ENV='\"production\"' && cp index.html bench.html dist/ && cp ../../packages/react/src/styles.css dist/" }, "dependencies": { - "@captiveorigins/onetaxonomy-core": "workspace:^", - "@captiveorigins/onetaxonomy-layout": "workspace:^", - "@captiveorigins/onetaxonomy-react": "workspace:^", + "@extantape/onetaxonomy-core": "workspace:^", + "@extantape/onetaxonomy-layout": "workspace:^", + "@extantape/onetaxonomy-react": "workspace:^", "react": "^19.3.1", "react-dom": "^19.3.1", - "@captiveorigins/onetaxonomy-education": "workspace:^" + "@extantape/onetaxonomy-education": "workspace:^" }, "devDependencies": { "esbuild": "^0.28.2" diff --git a/apps/studio/package.json b/apps/studio/package.json index 02e5a5b..196c97e 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -14,10 +14,10 @@ "app:build": "tauri build" }, "dependencies": { - "@captiveorigins/onetaxonomy-core": "workspace:^", - "@captiveorigins/onetaxonomy-education": "workspace:^", - "@captiveorigins/onetaxonomy-import": "workspace:^", - "@captiveorigins/onetaxonomy-react": "workspace:^", + "@extantape/onetaxonomy-core": "workspace:^", + "@extantape/onetaxonomy-education": "workspace:^", + "@extantape/onetaxonomy-import": "workspace:^", + "@extantape/onetaxonomy-react": "workspace:^", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-fs": "^2", diff --git a/apps/studio/src-tauri/Cargo.toml b/apps/studio/src-tauri/Cargo.toml index df38d06..7293ddb 100644 --- a/apps/studio/src-tauri/Cargo.toml +++ b/apps/studio/src-tauri/Cargo.toml @@ -2,7 +2,7 @@ name = "onetaxonomy" version = "0.2.0" description = "OneTaxonomy for Mac" -authors = ["Captive Origins"] +authors = ["EXTANT APE"] license-file = "../../../LICENSE" edition = "2021" diff --git a/apps/studio/src-tauri/tauri.conf.json b/apps/studio/src-tauri/tauri.conf.json index 4967d04..aadb0a8 100644 --- a/apps/studio/src-tauri/tauri.conf.json +++ b/apps/studio/src-tauri/tauri.conf.json @@ -59,7 +59,7 @@ "plugins": { "updater": { "endpoints": [ - "https://github.com/CaptiveOrigins/onetaxonomy/releases/latest/download/latest.json" + "https://github.com/ExtantApe/OneTaxonomy/releases/latest/download/latest.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE1RDA3Q0JFMDdENDU1QTEKUldTaFZkUUh2bnpRRlY5YVpwVnFLNGJvdUdrNHNxelZtcG1CMTI0ZXB4RDkvQzB2Tzc0WVlsbGwK" } diff --git a/apps/studio/src/App.tsx b/apps/studio/src/App.tsx index 6e6962e..1902428 100644 --- a/apps/studio/src/App.tsx +++ b/apps/studio/src/App.tsx @@ -1,6 +1,6 @@ -import { createView, indexDataset, parseView, serializeView, validateDataset, validateView, type DatasetSnapshot, type Phylogeny, type TaxonomicView } from '@captiveorigins/onetaxonomy-core' -import { readBundle, type ChecklistColumns } from '@captiveorigins/onetaxonomy-import' -import { Explorer, TipLinker } from '@captiveorigins/onetaxonomy-react' +import { createView, indexDataset, parseView, serializeView, validateDataset, validateView, type DatasetSnapshot, type Phylogeny, type TaxonomicView } from '@extantape/onetaxonomy-core' +import { readBundle, type ChecklistColumns } from '@extantape/onetaxonomy-import' +import { Explorer, TipLinker } from '@extantape/onetaxonomy-react' import { invoke } from '@tauri-apps/api/core' import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' diff --git a/apps/studio/src/app.css b/apps/studio/src/app.css index 42bfadc..95ef656 100644 --- a/apps/studio/src/app.css +++ b/apps/studio/src/app.css @@ -7,7 +7,7 @@ --muted: #5b6577; --border: #eeddde; --accent: #cf8137; - /* Focus ring: the darker orange on light backgrounds (owner decision N154, 2026-11-11), 4.63:1 on the panel. Dark keeps #e7a066. */ + /* Focus ring: the darker orange on light backgrounds (owner decision, 2026-11-11), 4.63:1 on the panel. Dark keeps #e7a066. */ --focus: #a65f20; --field: #ffffff; /* Text-field borders: 3:1 against the field and the page (WCAG 1.4.11). */ @@ -27,7 +27,7 @@ /* * The explorer's own colors follow the system through a media query, so a chosen appearance has to set them too; * otherwise Light on a dark system draws light text on a white page. Same values as the light and dark blocks in - * @captiveorigins/onetaxonomy-react/styles.css (test/theme.test.ts keeps them equal). + * @extantape/onetaxonomy-react/styles.css (test/theme.test.ts keeps them equal). */ :root[data-appearance='light'] :is(.ot-root, .ot-explorer, .ot-lesson, .ot-tip-links) { --ot-ink: #4c312c; --ot-text: #1d2330; --ot-muted: #5b6577; --ot-edge: #a08a83; --ot-node: var(--ot-ink); diff --git a/apps/studio/src/bundle.ts b/apps/studio/src/bundle.ts index 9348569..5f542ee 100644 --- a/apps/studio/src/bundle.ts +++ b/apps/studio/src/bundle.ts @@ -1,6 +1,6 @@ -import { indexDataset, snapshotChecksum, validateView, type DatasetSnapshot, type TaxonomicView } from '@captiveorigins/onetaxonomy-core' -import { validateLesson, type LessonDeck } from '@captiveorigins/onetaxonomy-education' -import { writeBundle, type Bundle, type BundleLesson, type BundleMedia, type BundleRead, type SnapshotPin } from '@captiveorigins/onetaxonomy-import' +import { indexDataset, snapshotChecksum, validateView, type DatasetSnapshot, type TaxonomicView } from '@extantape/onetaxonomy-core' +import { validateLesson, type LessonDeck } from '@extantape/onetaxonomy-education' +import { writeBundle, type Bundle, type BundleLesson, type BundleMedia, type BundleRead, type SnapshotPin } from '@extantape/onetaxonomy-import' import pkg from '../package.json' diff --git a/apps/studio/src/main.tsx b/apps/studio/src/main.tsx index 17656e9..f6e572e 100644 --- a/apps/studio/src/main.tsx +++ b/apps/studio/src/main.tsx @@ -1,4 +1,4 @@ -import '@captiveorigins/onetaxonomy-react/styles.css' +import '@extantape/onetaxonomy-react/styles.css' import './oneorigin.css' import './app.css' import { createRoot } from 'react-dom/client' diff --git a/apps/studio/src/open.ts b/apps/studio/src/open.ts index f8100dd..a890014 100644 --- a/apps/studio/src/open.ts +++ b/apps/studio/src/open.ts @@ -1,5 +1,5 @@ -import { sealSnapshot, type DatasetSnapshot, type Phylogeny, type ViewTarget } from '@captiveorigins/onetaxonomy-core' -import { parseDelimited, parseNewick, readChecklistTable, readDwca, readNexus, readPhyloXml, readSnapshotJson, toSnapshot, type ChecklistColumns, type ImportResult } from '@captiveorigins/onetaxonomy-import' +import { sealSnapshot, type DatasetSnapshot, type Phylogeny, type ViewTarget } from '@extantape/onetaxonomy-core' +import { parseDelimited, parseNewick, readChecklistTable, readDwca, readNexus, readPhyloXml, readSnapshotJson, toSnapshot, type ChecklistColumns, type ImportResult } from '@extantape/onetaxonomy-import' /** Turning file contents into a dataset. No file access or app code here, so it can be tested alone. */ diff --git a/apps/studio/test/app.test.tsx b/apps/studio/test/app.test.tsx index ba9b090..3d4e57a 100644 --- a/apps/studio/test/app.test.tsx +++ b/apps/studio/test/app.test.tsx @@ -6,8 +6,8 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createView, indexDataset, verifySnapshot, type DatasetSnapshot } from '@captiveorigins/onetaxonomy-core' -import { readBundle, writeBundle } from '@captiveorigins/onetaxonomy-import' +import { createView, indexDataset, verifySnapshot, type DatasetSnapshot } from '@extantape/onetaxonomy-core' +import { readBundle, writeBundle } from '@extantape/onetaxonomy-import' const written = new Map() const writtenBytes = new Map() diff --git a/apps/studio/test/bundle.test.ts b/apps/studio/test/bundle.test.ts index eca49ed..1d7963c 100644 --- a/apps/studio/test/bundle.test.ts +++ b/apps/studio/test/bundle.test.ts @@ -2,8 +2,8 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import { describe, expect, it } from 'vitest' -import { createView, indexDataset, sealSnapshot, snapshotChecksum, type DatasetSnapshot } from '@captiveorigins/onetaxonomy-core' -import { readBundle, writeBundle, type BundleLesson, type BundleRead } from '@captiveorigins/onetaxonomy-import' +import { createView, indexDataset, sealSnapshot, snapshotChecksum, type DatasetSnapshot } from '@extantape/onetaxonomy-core' +import { readBundle, writeBundle, type BundleLesson, type BundleRead } from '@extantape/onetaxonomy-import' import { APP, BundleRefusal, bundleBytes, keepPinned, NOTHING_CARRIED, pinOf, planBundleOpen, planBundleSave, relinkToAvailable, type BundleOpening } from '../src/bundle' diff --git a/apps/studio/test/open.test.ts b/apps/studio/test/open.test.ts index e25fd44..c588113 100644 --- a/apps/studio/test/open.test.ts +++ b/apps/studio/test/open.test.ts @@ -1,5 +1,5 @@ -import { indexDataset, createView, midpointRoot, projectView, sealSnapshot, validateDataset, verifySnapshot } from '@captiveorigins/onetaxonomy-core' -import { Explorer } from '@captiveorigins/onetaxonomy-react' +import { indexDataset, createView, midpointRoot, projectView, sealSnapshot, validateDataset, verifySnapshot } from '@extantape/onetaxonomy-core' +import { Explorer } from '@extantape/onetaxonomy-react' import { createElement } from 'react' import { renderToString } from 'react-dom/server' import { describe, expect, it } from 'vitest' diff --git a/apps/studio/test/theme.test.ts b/apps/studio/test/theme.test.ts index 0037a2c..34ef8c6 100644 --- a/apps/studio/test/theme.test.ts +++ b/apps/studio/test/theme.test.ts @@ -45,7 +45,7 @@ describe('studio colors', () => { } }) - it('draws focus rings in the darker orange in light, the lighter orange in dark, and keeps the brand orange as the accent (N154, 2026-11-11)', () => { + it('draws focus rings in the darker orange in light, the lighter orange in dark, and keeps the brand orange as the accent (owner decision, 2026-11-11)', () => { expect(light['--focus']).toBe('#a65f20') expect(dark['--focus']).toBe('#e7a066') expect(light['--accent']).toBe('#cf8137') diff --git a/brand/README.md b/brand/README.md index f02add9..67e93d4 100644 --- a/brand/README.md +++ b/brand/README.md @@ -1,6 +1,6 @@ # Brand -OnePedigree, OneTaxonomy and OnePhylogeny share one brand: the [OneOrigin suite](oneorigin/README.md). Its text, rules and values live in one place, OnePhylogeny's [`docs/oneorigin/`](https://github.com/CaptiveOrigins/onephylogeny/tree/main/docs/oneorigin), and each tool carries a copy here: +OnePedigree, OneTaxonomy and OnePhylogeny share one brand: the [OneOrigin suite](oneorigin/README.md). Its text, rules and values live in one place, OnePhylogeny's [`docs/oneorigin/`](https://github.com/ExtantApe/OnePhylogeny/tree/main/docs/oneorigin), and each tool carries a copy here: - [oneorigin/brand.md](oneorigin/brand.md): the app icon, colors, controls and the focus ring. - [oneorigin/guidelines.md](oneorigin/guidelines.md): the suite guidelines, accepted by the owner on 2026-11-11. diff --git a/brand/icon.svg b/brand/icon.svg index 02803c9..fd3dfee 100644 --- a/brand/icon.svg +++ b/brand/icon.svg @@ -1,7 +1,7 @@ OneTaxonomy + (orange or pink); cream blocks, brown connectors, the heart badge as the maker's mark. --> @@ -18,7 +18,7 @@ - + diff --git a/brand/oneorigin.lock b/brand/oneorigin.lock index b2a30f7..be70fe5 100644 --- a/brand/oneorigin.lock +++ b/brand/oneorigin.lock @@ -1,22 +1,22 @@ { - "about": "Copy of https://github.com/CaptiveOrigins/onephylogeny docs/oneorigin at the commit below. Written by `node scripts/oneorigin.mjs lock`; `check` fails on any file that differs.", + "about": "Copy of https://github.com/ExtantApe/OnePhylogeny docs/oneorigin at the commit below. Written by `node scripts/oneorigin.mjs lock`; `check` fails on any file that differs.", "source": { - "repo": "https://github.com/CaptiveOrigins/onephylogeny", + "repo": "https://github.com/ExtantApe/OnePhylogeny", "branch": "main", "path": "docs/oneorigin", "commit": "35c7993d6915fb8c0622035a50f88b3e1970f4eb" }, "copy": "brand/oneorigin", "files": { - "README.md": "sha256:25b8cfbe5df31ba8b18d63138bd82a6511ed88b9c154f081369cd0f003cd8489", - "brand.md": "sha256:94556ff48b6e4c29d65d7939cd6ddd37bc568e7bddb17afe977ee2eea19a5763", - "guidelines.md": "sha256:8dfd221189b7d68fcafcb9bc2652830e537374323203fc86436d1936f97fec3f", + "README.md": "sha256:f8afbcd53fe2d32e5e85947cab9e0542e44073fe01e2fd1047ef6209276d5994", + "brand.md": "sha256:d0ae807c738efecd6235444037d73f9f1a26d9495493e597a911a022a657a6ac", + "guidelines.md": "sha256:6f537a0aa4d30dbff6f9004b33d4dfe1f0a51ece985e0707a51b8d80ac80bddb", "icons/block.svg": "sha256:0ebfc31700e1e6ae1ed4c795fc83741503dcb42d73675d120ba3d6d9a0106c12", - "icons/connector.svg": "sha256:5e545f134c8514c58282df7dae6c2e97cfab37d404b3df42dc80fe8ba63b0d0d", - "icons/heart-badge.svg": "sha256:f755eda3873ad972b660f2a8999d22b65965ffc3f4375294dff0c0a065bb9415", - "icons/tile-onepedigree.svg": "sha256:13729ff38e9957e5234515278bf694eb0c1279d7f0cf85e4b62f120758f79c88", - "icons/tile-onephylogeny.svg": "sha256:61d6878f648dece5824c96959a2cb1a4a3858a001e8e0ee17284d0d460295f3d", - "icons/tile-onetaxonomy.svg": "sha256:154bd2e7ce127e6fcf7f6b1ca2ab048f60f22483a7f96608170869de022b991c", + "icons/connector.svg": "sha256:3de223d5c838932db727dd05cd7b5469ef2a41f63b1c8e1543419dc19990f85f", + "icons/heart-badge.svg": "sha256:65bad1c87b8537e6f7db0854a12ee83fb1b8be10ce9a48d103ca6503c96589db", + "icons/tile-onepedigree.svg": "sha256:3bcbed862e7ed45dd80f4c06d57395706e1ce8101d54ee1b533a3a2a03918f77", + "icons/tile-onephylogeny.svg": "sha256:2b35565c2b5fe294ff8e98b9f196b480519c8dc91b50ec218b1e5b9f51213504", + "icons/tile-onetaxonomy.svg": "sha256:c9992e5369c79332accdcac6d4757e80148eabbbaf83fe9e176096e4acc88047", "marks/onelineage-dark.svg": "sha256:fa8f1fa2b94b00c58a855feaaf57ea3e225cb6d55671dfdb30e5dcaa8169af53", "marks/onelineage-light.svg": "sha256:16dc0b05e215de1eeb5cfb2e6b8aac1fc9090327f7aa2ab6df47c879d8f05bdf", "marks/onelineage-small.svg": "sha256:9d7bd756409427a8a2e5f66cf55ea424f3554e64431bca068d2185489a796a43", @@ -32,8 +32,8 @@ "marks/onetaxonomy-dark.svg": "sha256:00a491b0b7acb3372ada4feadc9d1e574e49bd3d7b071055032479819b07e91a", "marks/onetaxonomy-light.svg": "sha256:4cd7410ff4cf6b2cc5c953310cbc05349169816f1116574b2b844c19e77cd7e7", "marks/onetaxonomy-small.svg": "sha256:ae236afd68280a82d60af711c6be344e5d4cb5c157548d13b830755c7ce6c089", - "tokens.json": "sha256:d87f3fc103e7d3e145f72dd145ae0d48dea4208957d0be20e26786b70253b3a2", - "tokens.schema.json": "sha256:337fa1a63f1fa77a6a8816ee61cf074941e02a428dd64890b2b57cff3bc7cc9b" + "tokens.json": "sha256:9b88e3c0caa75b223860b9ca57c55fb48e0d773c2d1b24abba2aee194637b6bd", + "tokens.schema.json": "sha256:e7e38666c36015eab49cb802448567137ef6e44870a335ffe34eb89ed9bea3cc" }, "outputs": { "apps/studio/src/oneorigin.css": "ot" diff --git a/brand/oneorigin/README.md b/brand/oneorigin/README.md index ca8181e..e5d96f5 100644 --- a/brand/oneorigin/README.md +++ b/brand/oneorigin/README.md @@ -2,13 +2,13 @@ **Good tools for understanding where animals come from, without a subscription.** -OneOrigin is a suite of three independent tools from Captive Origins. Each does one job well and works on its own. Use one, two or all three, in whatever combination fits your work. +OneOrigin is a suite of three independent tools from EXTANT APE. Each does one job well and works on its own. Use one, two or all three, in whatever combination fits your work. | Tool | The question it answers | | --- | --- | -| [OnePedigree](https://github.com/CaptiveOrigins/onepedigree) | How is this individual related to its known relatives? | -| [OneTaxonomy](https://github.com/CaptiveOrigins/onetaxonomy) | What do sources claim about how life is organized, and how do we browse it? | -| [OnePhylogeny](https://github.com/CaptiveOrigins/onephylogeny) | Given DNA, what does the evidence support, and where does this sample fit? | +| [OnePedigree](https://github.com/ExtantApe/OnePedigree) | How is this individual related to its known relatives? | +| [OneTaxonomy](https://github.com/ExtantApe/OneTaxonomy) | What do sources claim about how life is organized, and how do we browse it? | +| [OnePhylogeny](https://github.com/ExtantApe/OnePhylogeny) | Given DNA, what does the evidence support, and where does this sample fit? | ## Why this exists @@ -22,10 +22,10 @@ So the OneOrigin tools are built differently. - **No account needed.** The Mac apps run on your computer. They use the internet only when you ask for public data or check for updates. - **Your files stay yours.** Everything is saved as plain, documented files you can open without us. If we vanished tomorrow, your work would still be readable. - **Your source files aren't touched.** Your extra information lives beside them. -- **The code is open to read and improve.** Anyone can read, change and share it for noncommercial use, with attribution, under [PolyForm Noncommercial](https://polyformproject.org/licenses/noncommercial/1.0.0). A business that wants to use it commercially can ask for a separate license. This license makes the code source-available, which isn't quite the same as open source. +- **The code is open to read and improve.** Anyone can read, change and share it for noncommercial use, with attribution, under [PolyForm Noncommercial](https://polyformproject.org/licenses/noncommercial/1.0.0). A business that wants to use it commercially can ask for a separate license. This license makes the code source-available. It isn't an open source license. - **Honesty over polish.** The tools say what they don't know and what a result can't tell you. Unknown stays unknown. -These commitments cover the tools. Services that have real ongoing costs, such as lab testing through Captive Origins, are separate and priced as what they are. +These commitments cover the tools. Services that have real ongoing costs, such as lab testing, are separate and priced as what they are. ## Separately or together @@ -50,4 +50,4 @@ The tools share a look, a way of behaving and a set of beliefs. Each keeps its o - **OneOrigin** is the name of the suite. It isn't a package or a product you install. - Each tool keeps its own name, repo and app. -- Packages stay under each project's scope, such as `@captiveorigins/onephylogeny-core`. +- Packages stay under each project's scope, such as `@extantape/onephylogeny-core`. diff --git a/brand/oneorigin/brand.md b/brand/oneorigin/brand.md index 8ae0d9b..32ca435 100644 --- a/brand/oneorigin/brand.md +++ b/brand/oneorigin/brand.md @@ -6,7 +6,7 @@ This is the canonical brand text. It lives in OnePhylogeny's `docs/oneorigin/`, ## Palette -The OneOrigin palette has its own colors, no longer taken from Captive Origins. Approved by the owner on 2026-11-12. The values are in `palette` in [tokens.json](tokens.json). +The OneOrigin palette has its own colors, separate from the earlier ones still used by the app icons and the interface below. Approved by the owner on 2026-11-12. The values are in `palette` in [tokens.json](tokens.json). | | OneTaxonomy | OnePhylogeny | OneLineage | OnePedigree | | --- | --- | --- | --- | --- | @@ -53,42 +53,42 @@ Five marks, each as light, dark and small SVGs in [marks/](marks/): `oneorigin`, ## App icon -The app icons, their tiles and the heart badge below are the earlier ones, drawn from Captive Origins colors. They stay as they are for now: new app icons, tiles and the maker's mark aren't decided yet. +The app icons, their tiles and the heart badge below are the earlier ones, drawn from the earlier palette. They stay as they are for now: new app icons, tiles and the maker's mark aren't decided yet. | | OnePedigree | OneTaxonomy | OnePhylogeny | | --- | --- | --- | --- | -| Tile gradient (top → bottom) | `#f8dcc0` → `#eeb684` (pastel Captive Origins orange) | `#f8d6d7` → `#eda9ac` (pastel Captive Origins pink) | `#e6d6d2` → `#c9aca5` (pastel Captive Origins mauve) | +| Tile gradient (top → bottom) | `#f8dcc0` → `#eeb684` (pastel orange) | `#f8d6d7` → `#eda9ac` (pastel pink) | `#e6d6d2` → `#c9aca5` (pastel mauve) | | Glyph | Two parents joined above one offspring | A rooted tree read left to right | A sample on a branching tree | -Both use Captive Origins brown `#4c312c` as ink (connectors, UI structure). Owner's choice 2026-11-02 (option A of the palette study; the swap, option C, stays possible: exchange the two hues). +Both use the earlier palette's brown `#4c312c` as ink (connectors, UI structure). Owner's choice 2026-11-02 (option A of the palette study; the swap, option C, stays possible: exchange the two hues). Shared by every tool: -- Tile: 896 × 896 on a 1024 canvas, corner radius 200, vertical pastel gradient of a Captive Origins color. -- Blocks: cream `#fbf8f2`, rounded (radius ≈ 36–40), connectors 34 px in Captive Origins brown with round joins (owner's choice 2026-11-11, N169: "A: round (Recommended)"; the three icons already drew them round, and the text said square). -- The Captive Origins heart badge, exactly as in the Captive Origins logo (brown ring, pink disc, white heart; paths copied from the logo), as the maker's mark, placed on "the one" (the focal individual, the selected taxon). +- Tile: 896 × 896 on a 1024 canvas, corner radius 200, vertical pastel gradient of a color from the earlier palette. +- Blocks: cream `#fbf8f2`, rounded (radius ≈ 36–40), connectors 34 px in brown `#4c312c` with round joins (owner's choice 2026-11-11, "A: round (Recommended)"; the three icons already drew them round, and the text said square). +- The heart badge (brown ring, pink disc, white heart; the paths in `icons/heart-badge.svg`), as the maker's mark, placed on "the one" (the focal individual, the selected taxon). - Small sizes (16–32 px: menu bar, file lists, favicons): `brand/icon-small.svg`, the same tile and glyph without the badge, a larger tile and heavier lines. From 64 px up, the full icon. -- Masters: `brand/icon.svg` (source) and `brand/icon-1024.png` (transparent, for app icon generators such as `tauri icon`). A future tool takes a pastel of another Captive Origins color (mauve `#91645d`, a lighter or deeper orange/pink) and its own glyph from the same blocks. +- Masters: `brand/icon.svg` (source) and `brand/icon-1024.png` (transparent, for app icon generators such as `tauri icon`). A future tool takes a pastel of another color from the earlier palette (mauve `#91645d`, a lighter or deeper orange/pink) and its own glyph from the same blocks. ### Icon parts -The shared parts, cut from the three icons without changing a value, are in [icons/](icons/): each tool's tile (`tile-onepedigree.svg`, `tile-onetaxonomy.svg`, `tile-onephylogeny.svg`), a cream block (`block.svg`), a connector (`connector.svg`) and the heart badge (`heart-badge.svg`, its paths exactly as in the logo). Each tool's glyph and the badge's place on it stay in that tool's own `brand/icon.svg`. +The shared parts, cut from the three icons without changing a value, are in [icons/](icons/): each tool's tile (`tile-onepedigree.svg`, `tile-onetaxonomy.svg`, `tile-onephylogeny.svg`), a cream block (`block.svg`), a connector (`connector.svg`) and the heart badge (`heart-badge.svg`, its paths exactly as in the icons). Each tool's glyph and the badge's place on it stay in that tool's own `brand/icon.svg`. ## Interface -The interface colors below are also the earlier ones, from Captive Origins colors, and the apps and React packages still use them. Whether they move to the palette above isn't decided yet. +The interface colors below are also the earlier ones, and the apps and React packages still use them. Whether they move to the palette above isn't decided yet. -Both React packages share these (colors from the Captive Origins palette: orange `#cf8137`, pink `#e48185`, brown `#4c312c`), under their own prefixes (`op-`, `ot-`), so neither depends on the other: +Both React packages share these (colors from the earlier palette: orange `#cf8137`, pink `#e48185`, brown `#4c312c`), under their own prefixes (`op-`, `ot-`), so neither depends on the other: -- **Icons**: the same 24 × 24 line icons in `icons.tsx`, drawn from Lucide (ISC; the set Captive Origins uses), 1.75 stroke, round caps and joins, `currentColor`. Shown next to text where a label helps; alone only in compact floating toolbars, always with `aria-label` and a visible tooltip on hover and keyboard focus (`data-tip`). Icons standing alone must say the action (a magnifier for zoom, not a bare +). Buttons are at least 32 px (WCAG asks 24); icon strokes keep 3:1 contrast; overlays use shape as well as color. +- **Icons**: the same 24 × 24 line icons in `icons.tsx`, drawn from Lucide (ISC), 1.75 stroke, round caps and joins, `currentColor`. Shown next to text where a label helps; alone only in compact floating toolbars, always with `aria-label` and a visible tooltip on hover and keyboard focus (`data-tip`). Icons standing alone must say the action (a magnifier for zoom, not a bare +). Buttons are at least 32 px (WCAG asks 24); icon strokes keep 3:1 contrast; overlays use shape as well as color. - **Controls**: 32 px buttons, radius 8, 1 px border; floating toolbar at the top right of the canvas; cards and panels radius 10 with a soft shadow; focus ring 2 px, darker orange `#a65f20` on light backgrounds and `#e7a066` in dark mode (see Focus ring below). - **Canvas**: drag the background to pan, Ctrl/⌘ + wheel or pinch to zoom, `+` `-` `0` keys, Fit, Download SVG, Print (chart only). -- **Selection**: Captive Origins orange for the selected item; the darker orange for focus in light mode; brown for structure; pink for the brand mark and highlights. +- **Selection**: the brand orange for the selected item; the darker orange for focus in light mode; brown for structure; pink for the brand mark and highlights. - Each tool keeps its own functionality and layout; only the backbone is shared. ## Focus ring -Owner's decision 2026-11-11 (N154, option A, "darker orange ring"): "keep the brand orange everywhere else, but use the darker orange #a65f20 for focus rings on light backgrounds (4.6 to 1 or better). Dark mode keeps the lighter orange, which already passes." +Owner's decision 2026-11-11 (option A, "darker orange ring"): "keep the brand orange everywhere else, but use the darker orange #a65f20 for focus rings on light backgrounds (4.6 to 1 or better). Dark mode keeps the lighter orange, which already passes." - **Light backgrounds**: focus rings are `#a65f20`. That's 4.91:1 on white and 4.61:1 or better on the cream panels (`#fbf7f4`, `#f7f8fa`, `#fdf7f7`). The brand orange was 3.07:1 on white and 2.88:1 on the panels, under the 3:1 that WCAG 2.2 asks of focus indicators (1.4.11, 2.4.13). - **Dark mode**: focus rings stay `#e7a066`, 7.1:1 or better on every dark background the tools use. diff --git a/brand/oneorigin/guidelines.md b/brand/oneorigin/guidelines.md index 12523a5..b858329 100644 --- a/brand/oneorigin/guidelines.md +++ b/brand/oneorigin/guidelines.md @@ -1,6 +1,6 @@ # OneOrigin suite guidelines -**Status: accepted by the owner on 2026-11-11 (N105: "A: send it, accept guidelines (Recommended)").** Drafted 2026-11-04. These apply to OnePedigree, OneTaxonomy and OnePhylogeny, the [OneOrigin suite](README.md). The brand text that used to be hand-copied into each repo's `brand/README.md` now lives in [brand.md](brand.md), and the machine-readable values in [tokens.json](tokens.json). Each tool carries a locked copy of this folder (section 10). +**Status: accepted by the owner on 2026-11-11 ("A: send it, accept guidelines (Recommended)").** Drafted 2026-11-04. These apply to OnePedigree, OneTaxonomy and OnePhylogeny, the [OneOrigin suite](README.md). The brand text that used to be hand-copied into each repo's `brand/README.md` now lives in [brand.md](brand.md), and the machine-readable values in [tokens.json](tokens.json). Each tool carries a locked copy of this folder (section 10). They exist so the three tools feel like one suite and keep working together as they change, while each stays its own and none depends on another. @@ -39,12 +39,12 @@ The app icons and the interface colors below are the earlier ones. They stay unt Shared by every tool: -- **Tile:** 896 × 896 on a 1024 canvas, corner radius 200, a vertical pastel gradient of a Captive Origins color. -- **Blocks and connectors:** cream `#fbf8f2` blocks with radius about 36 to 40, and 34 px Captive Origins brown `#4c312c` connectors with round joins (owner decision N169, 2026-11-11). Draw a connector's corners as one path, so the join applies; separate strokes that meet at a corner leave a notch. -- **Badge:** the Captive Origins heart badge, exactly as in the logo, as the maker's mark, placed on "the one": the focal individual, selected taxon or tested sample. +- **Tile:** 896 × 896 on a 1024 canvas, corner radius 200, a vertical pastel gradient of a color from the earlier palette. +- **Blocks and connectors:** cream `#fbf8f2` blocks with radius about 36 to 40, and 34 px brown `#4c312c` connectors with round joins (owner decision, 2026-11-11). Draw a connector's corners as one path, so the join applies; separate strokes that meet at a corner leave a notch. +- **Badge:** the heart badge, exactly as in `icons/heart-badge.svg`, as the maker's mark, placed on "the one": the focal individual, selected taxon or tested sample. - **Small sizes:** `brand/icon-small.svg` for 16 to 32 px, with the same tile and glyph, no badge, a larger tile and heavier lines. The full icon is used from 64 px up. - **Masters:** every repo must have `brand/icon.svg`, `brand/icon-small.svg` and `brand/icon-1024.png` (transparent, for `tauri icon`). -- **A future tool** takes a pastel of another Captive Origins color and its own glyph from the same blocks. +- **A future tool** takes a pastel of another color from the earlier palette and its own glyph from the same blocks. ### Color @@ -76,7 +76,7 @@ The system font stack in every tool, with monospace only for sequences, logs and - Buttons are 32 px, radius 8, with a 1 px border. - Cards and panels have radius 10 and a soft shadow. -- The focus ring is 2 px: the darker orange `#a65f20` on light backgrounds and `#e7a066` in dark mode. The brand orange `#cf8137` stays for fills, accents and selection. Owner's decision 2026-11-11 (N154, "A: darker orange ring"). +- The focus ring is 2 px: the darker orange `#a65f20` on light backgrounds and `#e7a066` in dark mode. The brand orange `#cf8137` stays for fills, accents and selection. Owner's decision 2026-11-11 ("A: darker orange ring"). - Interactive canvases have a floating toolbar at the top right. Drag the background to pan, use Ctrl/⌘ + wheel or pinch to zoom, use `+` `-` `0` keys, and offer Fit, Download SVG and Print (chart only). ### Voice @@ -155,7 +155,7 @@ fixtures/ small, illustrative, with a README - pnpm workspaces, Node 22 or later, ESM only, built with tsup and tested with Vitest. - Strict TypeScript, with `noUncheckedIndexedAccess` on. -- **Package names:** `@captiveorigins/-` and `@captiveorigins/payload-plugin-`. +- **Package names:** `@extantape/-` and `@extantape/payload-plugin-`. - **Dependencies point downward:** `core` has no runtime dependencies and compiles without DOM or Node types. It must not depend on React, Payload, databases or Tauri. Exceptions need an ADR. - No empty scaffolding. Planned work lives in the roadmap. @@ -185,8 +185,8 @@ Each decision with alternatives gets an ADR. Limitations are kept current, and n - Lockstep versions across a tool's packages. - Tag `vx.y.z` runs a workflow that tests and **stages** every package through trusted publishing, with no stored token. A maintainer approves with 2FA. - A package's first version is published by hand, then `npm trust` is set up. -- `package.json` description and keywords are the source of truth, synced to GitHub with `npm run packages:sync` in Captive Origins. Release workflows refuse to stage when they differ. -- Each repo is registered in `packages-sync.mjs` and `npm-trust.sh`. +- `package.json` description and keywords are the source of truth, copied to GitHub before each release. Release workflows refuse to stage when they differ. +- Each repo is registered in the shared description sync and trusted publishing scripts. ### Mac app @@ -200,7 +200,7 @@ Each decision with alternatives gets an ADR. Limitations are kept current, and n - **Bridges:** integration lives in an optional package such as `onephylogeny-onetaxonomy`, which depends on both sides. Sister tools expose generic extension points and never depend on a bridge. - **Plain files between apps:** one tool's export is another's import. A tool that can't open a file should say which tool can. - **Payload plugins** map onto a host's collections rather than owning them, keep permission-aware endpoints, enforce rules on the server, and never hold exact sensitive locations. Compute that can't run on a host's servers runs elsewhere, and the plugin stores what was computed. -- **Captive Origins** consumes these packages from npm. Nothing here knows about Captive Origins, and nothing in a tool's core knows about a species or a site. +- **Sites** install these packages from npm like anyone else. Nothing here knows about any one site, and nothing in a tool's core knows about a species or a site. ## 9. Parity today @@ -226,7 +226,7 @@ From a review of the three repos on 2026-11-04, updated on 2026-11-07 after the | Cancel for long work | Not checked | n/a | Yes | | npm release workflow | Yes | Yes | Yes, first version published by hand | | Payload plugin | Yes | **Planned** | Yes | -| Installed in Captive Origins | Yes | **No** | **No** | +| Used by a live site | Yes | **No** | **No** | | License | PolyForm Noncommercial | PolyForm Noncommercial | PolyForm Noncommercial | | `core` compiles without Node types | Yes | Yes | Yes | @@ -234,7 +234,7 @@ From a review of the three repos on 2026-11-04, updated on 2026-11-07 after the ## 10. Keeping this true -- **One source:** this folder in OnePhylogeny (`docs/oneorigin/`) is the source of truth. Each tool carries a copy in `brand/oneorigin/`, with `brand/oneorigin.lock` recording the source commit and a checksum of every file. CI fails when a copy is edited by hand, and warns when the source has moved ahead. Copies, not a dependency, so each tool still builds alone. Accepted by the owner on 2026-11-11 (N105). +- **One source:** this folder in OnePhylogeny (`docs/oneorigin/`) is the source of truth. Each tool carries a copy in `brand/oneorigin/`, with `brand/oneorigin.lock` recording the source commit and a checksum of every file. CI fails when a copy is edited by hand, and warns when the source has moved ahead. Copies, not a dependency, so each tool still builds alone. Accepted by the owner on 2026-11-11. - **Changing it:** a pull request here says which tools it affects, and those tools get a parity row until they catch up. - **Pull request checklist:** - Does this change something the family shares? Then update this page. @@ -244,6 +244,6 @@ From a review of the three repos on 2026-11-04, updated on 2026-11-07 after the ### Open questions for the owner -1. Where should this live long term? Answered 2026-11-11 (N105): here, in `docs/oneorigin/`, copied into each tool. -2. Should shared look move into a shared package? Answered 2026-11-11 (N105): no. Copies with a lock and a drift check, so no tool depends on another. +1. Where should this live long term? Answered 2026-11-11: here, in `docs/oneorigin/`, copied into each tool. +2. Should shared look move into a shared package? Answered 2026-11-11: no. Copies with a lock and a drift check, so no tool depends on another. 3. Should OnePedigree's green accent change to the family orange? diff --git a/brand/oneorigin/icons/connector.svg b/brand/oneorigin/icons/connector.svg index 0302daa..6281758 100644 --- a/brand/oneorigin/icons/connector.svg +++ b/brand/oneorigin/icons/connector.svg @@ -1,6 +1,6 @@ OneOrigin connector - diff --git a/brand/oneorigin/icons/heart-badge.svg b/brand/oneorigin/icons/heart-badge.svg index 3e1f274..d6dd488 100644 --- a/brand/oneorigin/icons/heart-badge.svg +++ b/brand/oneorigin/icons/heart-badge.svg @@ -1,7 +1,7 @@ - Captive Origins heart badge - diff --git a/brand/oneorigin/icons/tile-onepedigree.svg b/brand/oneorigin/icons/tile-onepedigree.svg index d5b835e..ddcecf5 100644 --- a/brand/oneorigin/icons/tile-onepedigree.svg +++ b/brand/oneorigin/icons/tile-onepedigree.svg @@ -1,7 +1,7 @@ OnePedigree tile + a top-to-bottom pastel gradient of orange. Values in tokens.json (tools.onepedigree.tile, icon.tile). --> diff --git a/brand/oneorigin/icons/tile-onephylogeny.svg b/brand/oneorigin/icons/tile-onephylogeny.svg index 86885fc..1365ecd 100644 --- a/brand/oneorigin/icons/tile-onephylogeny.svg +++ b/brand/oneorigin/icons/tile-onephylogeny.svg @@ -1,7 +1,7 @@ OnePhylogeny tile + a top-to-bottom pastel gradient of mauve. Values in tokens.json (tools.onephylogeny.tile, icon.tile). --> diff --git a/brand/oneorigin/icons/tile-onetaxonomy.svg b/brand/oneorigin/icons/tile-onetaxonomy.svg index e825dae..78d05c3 100644 --- a/brand/oneorigin/icons/tile-onetaxonomy.svg +++ b/brand/oneorigin/icons/tile-onetaxonomy.svg @@ -1,7 +1,7 @@ OneTaxonomy tile + a top-to-bottom pastel gradient of pink. Values in tokens.json (tools.onetaxonomy.tile, icon.tile). --> diff --git a/brand/oneorigin/tokens.json b/brand/oneorigin/tokens.json index b4e91e9..b1df47c 100644 --- a/brand/oneorigin/tokens.json +++ b/brand/oneorigin/tokens.json @@ -5,9 +5,9 @@ "about": "Shared look of the OneOrigin tools, read by scripts/oneorigin.mjs build to write each tool's CSS variables. brand.md and guidelines.md explain the rules. Colors are per mode: light, then dark.", "color": { "accent": { "light": "#cf8137", "dark": "#e7a066", "use": "Selection, fills, primary buttons. Not focus rings." }, - "focus": { "light": "#a65f20", "dark": "#e7a066", "use": "Keyboard focus rings only. Owner's decision 2026-11-11 (N154): 4.6:1 or better on every light background." }, + "focus": { "light": "#a65f20", "dark": "#e7a066", "use": "Keyboard focus rings only. Owner's decision 2026-11-11: 4.6:1 or better on every light background." }, "brand-pink": { "light": "#e48185", "dark": "#eda9ac", "use": "The brand mark and highlights only." }, - "structure": { "light": "#4c312c", "dark": "#eda9ac", "use": "Lines and connectors: Captive Origins brown in light mode." }, + "structure": { "light": "#4c312c", "dark": "#eda9ac", "use": "Lines and connectors: brown in light mode." }, "mauve": { "light": "#91645d", "dark": "#c79a92", "use": "Secondary data marks." }, "text": { "light": "#1d2330", "dark": "#e8ecf2", "use": "Body text." }, "muted": { "light": "#5b6577", "dark": "#a3acbb", "use": "Supporting text." }, @@ -50,7 +50,7 @@ "onepedigree": { "name": "OnePedigree", "prefix": "op", - "tile": { "top": "#f8dcc0", "bottom": "#eeb684", "hue": "Captive Origins orange" }, + "tile": { "top": "#f8dcc0", "bottom": "#eeb684", "hue": "orange" }, "glyph": "Two parents joined above one offspring", "color": { "panel": { "light": "#f7f8fa", "dark": "#181c24" }, @@ -60,7 +60,7 @@ "onetaxonomy": { "name": "OneTaxonomy", "prefix": "ot", - "tile": { "top": "#f8d6d7", "bottom": "#eda9ac", "hue": "Captive Origins pink" }, + "tile": { "top": "#f8d6d7", "bottom": "#eda9ac", "hue": "pink" }, "glyph": "A rooted tree read left to right", "color": { "panel": { "light": "#fdf7f7", "dark": "#181c24" }, @@ -72,7 +72,7 @@ "onephylogeny": { "name": "OnePhylogeny", "prefix": "ophy", - "tile": { "top": "#e6d6d2", "bottom": "#c9aca5", "hue": "Captive Origins mauve" }, + "tile": { "top": "#e6d6d2", "bottom": "#c9aca5", "hue": "mauve" }, "glyph": "A sample on a branching tree", "color": { "panel": { "light": "#fbf7f4", "dark": "#181c24" }, diff --git a/brand/oneorigin/tokens.schema.json b/brand/oneorigin/tokens.schema.json index db3eb1d..e93013d 100644 --- a/brand/oneorigin/tokens.schema.json +++ b/brand/oneorigin/tokens.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/CaptiveOrigins/onephylogeny/blob/main/docs/oneorigin/tokens.schema.json", + "$id": "https://github.com/ExtantApe/OnePhylogeny/blob/main/docs/oneorigin/tokens.schema.json", "title": "OneOrigin tokens", "description": "The shared look of the OneOrigin tools: colors per mode, the brand palette, radii, sizes, icon parts and each tool's tile.", "type": "object", diff --git a/docs/architecture/adr/0001-monorepo-and-tooling.md b/docs/architecture/adr/0001-monorepo-and-tooling.md index 9c1f19b..6dbfe9d 100644 --- a/docs/architecture/adr/0001-monorepo-and-tooling.md +++ b/docs/architecture/adr/0001-monorepo-and-tooling.md @@ -4,4 +4,4 @@ **Decision.** Same toolchain as OnePedigree: pnpm workspaces (`packages/*`, `apps/*`, `examples`), `tsconfig.base.json` with `strict` and `noUncheckedIndexedAccess`, ESM-only packages built with tsup, Vitest per package, GitHub Actions CI (install → build → typecheck → test). Playwright arrives with the first UI (M4); a linter/formatter arrives with the second contributor-facing package rather than as an empty config. -**Consequences.** One mental model across Captive Origins' frameworks. Workspace packages are tested against each other's built output, so CI builds before typechecking. +**Consequences.** One mental model across the One tools. Workspace packages are tested against each other's built output, so CI builds before typechecking. diff --git a/docs/architecture/adr/0002-pure-core.md b/docs/architecture/adr/0002-pure-core.md index f3c29b7..4b84918 100644 --- a/docs/architecture/adr/0002-pure-core.md +++ b/docs/architecture/adr/0002-pure-core.md @@ -2,6 +2,6 @@ **Context.** The core must run in browsers, Node, workers, Tauri webviews and a Payload server, with no React, Payload, database, Node-only or Tauri dependency. -**Decision.** `@captiveorigins/onetaxonomy-core` has no runtime dependencies and is type-checked with `lib: ES2023` and `types: []` (no DOM, no Node), so any platform API use fails to compile. Records are plain JSON. Checksums use a small pure SHA-256 (verified against the standard test vectors) because platform crypto is asynchronous and differs by environment. Traversal is iterative (no recursion limits) and bounded where results can be large. +**Decision.** `@extantape/onetaxonomy-core` has no runtime dependencies and is type-checked with `lib: ES2023` and `types: []` (no DOM, no Node), so any platform API use fails to compile. Records are plain JSON. Checksums use a small pure SHA-256 (verified against the standard test vectors) because platform crypto is asynchronous and differs by environment. Traversal is iterative (no recursion limits) and bounded where results can be large. **Consequences.** Slightly more code (hashing, index building) in exchange for identical, synchronous behaviour everywhere. Tests may use Node types (`tsconfig.test.json`); source may not. diff --git a/docs/architecture/adr/0007-releases.md b/docs/architecture/adr/0007-releases.md index ee8b588..3cd41ba 100644 --- a/docs/architecture/adr/0007-releases.md +++ b/docs/architecture/adr/0007-releases.md @@ -2,6 +2,6 @@ **Context.** The specification suggests Changesets. The owner's projects (OnePedigree and the Payload plugins) share one release process: a version tag triggers a GitHub workflow that tests, builds and **stages** the packages on npm through trusted publishing (no stored token); a maintainer approves them with 2FA on npmjs.com. -**Decision.** All OneTaxonomy packages (`@captiveorigins/onetaxonomy-*`, `@captiveorigins/payload-plugin-onetaxonomy`) share one version (lockstep), bumped together, released by tag through the same staged workflow (`.github/workflows/release.yml`, `RELEASING.md`). Changesets is not used: with lockstep versions it adds a step without adding information, and per-package changelogs live in `CHANGELOG.md`. +**Decision.** All OneTaxonomy packages (`@extantape/onetaxonomy-*`, `@extantape/payload-plugin-onetaxonomy`) share one version (lockstep), bumped together, released by tag through the same staged workflow (`.github/workflows/release.yml`, `RELEASING.md`). Changesets is not used: with lockstep versions it adds a step without adding information, and per-package changelogs live in `CHANGELOG.md`. **Consequences.** Every release publishes every package, even unchanged ones. Revisit if packages need independent versions after 1.0. diff --git a/docs/architecture/adr/0008-package-scope.md b/docs/architecture/adr/0008-package-scope.md index 08de7d8..178694d 100644 --- a/docs/architecture/adr/0008-package-scope.md +++ b/docs/architecture/adr/0008-package-scope.md @@ -1,7 +1,9 @@ -# 0008 — npm names `@captiveorigins/onetaxonomy-*` +# 0008 — npm names `@extantape/onetaxonomy-*` -**Context.** The specification names packages `@onetaxonomy/*`, which would need a separate npm organization. The owner's other framework, OnePedigree, publishes under the existing `@captiveorigins` scope (`@captiveorigins/onepedigree-core`, `-react`, `@captiveorigins/payload-plugin-onepedigree`). +**Context.** The specification names packages `@onetaxonomy/*`, which would need a separate npm organization. The owner's other framework, OnePedigree, already published under the owner's existing npm scope. -**Decision (owner, 2026-11-02).** Follow OnePedigree: `@captiveorigins/onetaxonomy-core`, `-import`, `-layout`, `-react`, `-education` (and `-live` in Phase 2), and the Payload plugin `@captiveorigins/payload-plugin-onetaxonomy`. The scope is only a publisher namespace: the code stays independent of Captive Origins (ADR 0002) and imports nothing from it. +**Decision (owner, 2026-11-02).** Follow OnePedigree: `onetaxonomy-core`, `-import`, `-layout`, `-react`, `-education` (and `-live` in Phase 2), and the Payload plugin `payload-plugin-onetaxonomy`, all under the same scope as OnePedigree. The scope is only a publisher namespace: the code stays independent of any site that uses it (ADR 0002) and imports nothing from one. -**Consequences.** One npm account and one trusted-publisher setup for all of the owner's packages. Moving to a dedicated scope later means publishing under new names and deprecating these. +**Moved (owner, 2026-11-14).** EXTANT APE owns OneTaxonomy. The repo moves to `ExtantApe/OneTaxonomy` and the packages publish under `@extantape` from 0.1.0-alpha.4: `@extantape/onetaxonomy-core` and the rest, and `@extantape/payload-plugin-onetaxonomy`. OnePedigree made the same move. The old names of the published packages get deprecated with a pointer to the new ones. + +**Consequences.** One npm account and one trusted-publisher setup for all of the owner's packages. The move to `@extantape` meant publishing under new names and deprecating the old ones; anyone installing them changes the scope in their `package.json` and imports. diff --git a/docs/architecture/adr/0009-license.md b/docs/architecture/adr/0009-license.md index 1ddf268..48f6bb2 100644 --- a/docs/architecture/adr/0009-license.md +++ b/docs/architecture/adr/0009-license.md @@ -4,4 +4,4 @@ **Decision.** PolyForm Noncommercial 1.0.0 for all three, with a `Required Notice` line in each `LICENSE`. General Payload plugins stay MIT; a One product's Payload plugin follows its product. -**Consequences.** The projects are source-available, not open source. Commercial use needs a separate license from Captive Origins. Noncommercial users may keep their changes private. Versions already published stay MIT. Outside contributions need an agreement that keeps the owner's right to license separately. Needs legal review before the first stable release. +**Consequences.** The projects are source-available, not open source. Commercial use needs a separate license from EXTANT APE. Noncommercial users may keep their changes private. Versions already published stay MIT. Outside contributions need an agreement that keeps the owner's right to license separately. Needs legal review before the first stable release. diff --git a/docs/architecture/adr/0010-outline-is-the-keyboard-path.md b/docs/architecture/adr/0010-outline-is-the-keyboard-path.md index 2e6f595..c413632 100644 --- a/docs/architecture/adr/0010-outline-is-the-keyboard-path.md +++ b/docs/architecture/adr/0010-outline-is-the-keyboard-path.md @@ -1,6 +1,6 @@ # 0010 — The outline is the keyboard path into the tree; drawing nodes stay out of the Tab order -**Status.** Accepted by Science (the area lead), 2026-11-11: a technical accessibility choice, not a product decision. Follow-up logged: pan the drawing to whatever is selected in the outline (done 2026-11-11, see Consequences). +**Status.** Accepted by the maintainers, 2026-11-11: a technical accessibility choice, not a product decision. Follow-up logged: pan the drawing to whatever is selected in the outline (done 2026-11-11, see Consequences). **Context.** The explorer's drawing is an SVG with `role="img"`, a title and a description. Its nodes have `tabindex="-1"`, so they take focus when clicked but not with Tab. Every action on a node has a keyboard path through the outline beside it, a `role="tree"` with one Tab stop and the tree view keys (Up and Down, Right and Left to open, close and move, Home and End, type-ahead, Space and Enter, Alt+Up and Alt+Down to reorder), and through the inspector. The audit of 2026-11-11 asked whether the drawing's nodes could be reached with Tab too, in a roving-tabindex pattern. @@ -13,4 +13,4 @@ **Consequences.** `TreeView` keeps `tabindex="-1"` on nodes and `role="img"` on the SVG; a test in `a11y.test.tsx` holds both, and that the outline is one Tab stop. If the drawing ever needs its own focus, the cheaper step is to pan it to the outline's selection when that is out of view, which adds no second keyboard model. -Done 2026-11-11 (K495): choosing a taxon in the outline (Space, Enter on a tip, or a click on its row) pans the drawing, SVG or canvas, when the taxon is off screen or within 40 px of the edge, the shortest way that brings it 40 px inside. With several selected, it keeps them all in view and zooms out only when they can't fit at the current zoom, by the smallest factor that fits them; it never zooms in. The pan glides over 200 ms, or jumps when the person asks for reduced motion or the browser can't say. Focus stays in the outline. A plain pan announces nothing; a zoom out says "Zoomed out to show the selection." once in a polite status region. A click in the drawing never moves it, and any pan or zoom by the person stops a glide. Tests in `packages/react/test/reveal.test.tsx`. +Done 2026-11-11: choosing a taxon in the outline (Space, Enter on a tip, or a click on its row) pans the drawing, SVG or canvas, when the taxon is off screen or within 40 px of the edge, the shortest way that brings it 40 px inside. With several selected, it keeps them all in view and zooms out only when they can't fit at the current zoom, by the smallest factor that fits them; it never zooms in. The pan glides over 200 ms, or jumps when the person asks for reduced motion or the browser can't say. Focus stays in the outline. A plain pan announces nothing; a zoom out says "Zoomed out to show the selection." once in a polite status region. A click in the drawing never moves it, and any pan or zoom by the person stops a glide. Tests in `packages/react/test/reveal.test.tsx`. diff --git a/docs/architecture/adr/0011-bundle-not-sqlite.md b/docs/architecture/adr/0011-bundle-not-sqlite.md index 95be3d8..d7ba4a1 100644 --- a/docs/architecture/adr/0011-bundle-not-sqlite.md +++ b/docs/architecture/adr/0011-bundle-not-sqlite.md @@ -1,6 +1,6 @@ # 0011 — The Mac app keeps its work in files: a `.onetaxonomy` bundle, no SQLite for now -**Status.** Accepted by Science (the area lead), 2026-11-11. It follows the owner's rule that files stay plain and readable without the tool; revisit only on the measured triggers below. +**Status.** Accepted by the maintainers, 2026-11-11. It follows the owner's rule that files stay plain and readable without the tool; revisit only on the measured triggers below. **Context.** M6 listed SQLite persistence, backups and migrations beside a `.onetaxonomy` bundle. The Mac app already works on plain files: it opens checklists, archives, trees and sealed snapshots, never writes over the file it opened, and saves views and sealed snapshots with a temporary file and a rename. The owner's rules for these tools say a person's files stay theirs, as plain documented files readable without the tool, and that a feature which adds something to look after has to take away more than it adds. @@ -19,7 +19,7 @@ What M6 needs from storage: 2. The manifest lists every file with its size and SHA-256, and each snapshot is sealed. The reader checks all of it before using any of it, and names each problem: no manifest, bad JSON, an unknown format version, a missing file, a size or checksum mismatch, a snapshot that isn't the one pinned, a path outside its folder. A damaged bundle is refused whole; it is never half-opened. 3. The manifest pins each snapshot by id, version and checksum, and every view and lesson must point at a pin. When the open dataset is another version, the app asks: keep the pinned one (when it's embedded) or relink to the open one, which is checked first and refused whole if anything doesn't fit. 4. Saves already go through a temporary file and a rename (`writeFileSafely`); bundles use the same path. -5. Each format has a version (`formatVersion` in the manifest, `schemaVersion` in snapshots, `formatVersion` in views and lessons). A reader refuses a version it doesn't know, with a message that says so. A migration, when one is needed, is a function from one JSON shape to the next in `@captiveorigins/onetaxonomy-import`, as data-schema.md already says. +5. Each format has a version (`formatVersion` in the manifest, `schemaVersion` in snapshots, `formatVersion` in views and lessons). A reader refuses a version it doesn't know, with a message that says so. A migration, when one is needed, is a function from one JSON shape to the next in `@extantape/onetaxonomy-import`, as data-schema.md already says. 6. A bundle is a single file, so Time Machine, iCloud Drive, a USB stick or email back it up, and a copy is a backup. The app holds no state of its own beyond Settings. SQLite would add a database the person can't read without a tool, a second copy of the data to keep in step with their files, a native dependency in the Tauri build, and its own migrations and backup story. It would take nothing away that the files don't already handle. diff --git a/docs/architecture/adr/0012-payload-plugin.md b/docs/architecture/adr/0012-payload-plugin.md index 18c59cf..3cddac1 100644 --- a/docs/architecture/adr/0012-payload-plugin.md +++ b/docs/architecture/adr/0012-payload-plugin.md @@ -1,6 +1,6 @@ # 0012 — The Payload plugin stores sealed snapshots whole and answers bounded questions about them -**Status.** Accepted by Science (the area lead), 2026-11-11, within the plugin the owner approved by name (N158). Part 2 (2026-11-11) adds points 12 to 14, built and tested; the lead accepted point 14 the same day. Part 3 (2026-11-11) makes claiming an import job one atomic update (point 12) and carries point 14's flag into Payload's own reads of a view. Part 4 (2026-11-11) adds proposals (point 15), built and tested, with core's `acceptIntoSnapshot`. Part 5 (2026-11-11) adds the admin panels and a snapshot's summary (point 16), built and tested. Later parts are listed below. +**Status.** Accepted by the maintainers, 2026-11-11, within the plugin the owner approved by name. Part 2 (2026-11-11) adds points 12 to 14, built and tested; the maintainers accepted point 14 the same day. Part 3 (2026-11-11) makes claiming an import job one atomic update (point 12) and carries point 14's flag into Payload's own reads of a view. Part 4 (2026-11-11) adds proposals (point 15), built and tested, with core's `acceptIntoSnapshot`. Part 5 (2026-11-11) adds the admin panels and a snapshot's summary (point 16), built and tested. Later parts are listed below. **Context.** M7 asks for a Payload plugin with configurable slugs, locale, permissions, an optional tenant scope and source adapters; admin views; a bounded read API with access checks and cross-tenant denial tests; resumable, idempotent import jobs; an example app; and per-user proposals a curator accepts into a new staged version. OneTaxonomy's records are immutable, checksummed snapshots ([data schema](../data-schema.md)), views pinned to them ([ADR 0006](0006-view-actions.md)), and a core that runs anywhere and knows nothing of Payload ([ADR 0002](0002-pure-core.md)). The sister plugins for OnePedigree and OnePhylogeny already set the house pattern: the plugin adds its own collections, takes the host's access rules, refuses what it can't check, and is tested on a real Payload with an in-memory MongoDB. @@ -25,7 +25,7 @@ - **Never half a snapshot.** The snapshot is stored last, as one document, after every check passed, so a run that stops leaves either no snapshot or a whole one. A job left `running` is refused (409) until `imports.staleAfterMinutes` (default 15) have passed, then starts again from the file. If it had stopped after storing, the re-run finds its own snapshot by checksum and finishes `done` without storing it twice. - **One run at a time.** A run starts by claiming the job with one conditional update: it succeeds only if the job's `attempts` counter is still what the run read, and it adds one. Every run that starts moves the counter, so when several start together one claims the job and the rest get a 409 and change nothing. On MongoDB the check and the write are a single `findOneAndUpdate`; inside a transaction the loser gets MongoDB's write conflict, which is answered with the same 409. The claim is written straight through the database adapter, because Payload's `update` reads and then writes. Limits: it hasn't been checked on Payload's SQL adapters, where the unique index on a snapshot's id and version is still the backstop; and it guards the start only, so a run still working after `staleAfterMinutes` can be taken over and will write its own progress lines when it wakes. The two can't both store a snapshot. 13. **The rest of the bounded API.** `GET /api//:id/coverage` runs core's `getCoverageReport` for a classification, optionally a phylogeny, listed groups and a listed selection (every concept when none is listed), capped by `limits.maxResults` groups and `limits.maxCoverageIds` concepts, and says knowledge of what science knows is not stated. `GET …/:id/export` downloads the sealed snapshot exactly as stored, after the checksum check. `GET /api//:id/projection` runs core's `projectView`, capped at `limits.maxNodes` (default 5,000) with `truncated`, and returns `describeView`'s disclosure beside it; `GET /api//:id/export` downloads the view file with the snapshot it pins and the disclosure lines. All of them run as the requester with part 1's rules: readers see published records, managers every status, all in their tenant, and anything else is the same 404. A view's snapshot is read through the view, and must still match the pin and its checksum, or the answer is a 500 with the reason. -14. **Accepted by Science (the area lead), 2026-11-11: withdrawing a snapshot doesn't hide its published views.** A published view of a withdrawn snapshot stays readable, and every answer about it carries the flag: `source.status: 'withdrawn'`, `source.withdrawn: true`, and a first-class disclosure line ("Source withdrawn: snapshot … was withdrawn by its publisher after this view was published. …") in projections and exports. The snapshot itself stays hidden from readers, as point 4 says. Why: views are cited and linked; hiding them silently breaks those links and hides that something changed; saved references are never rewritten or dropped silently (I18). A flag keeps the change visible. The alternative, hiding the views, was one access rule away and wasn't chosen. +14. **Accepted by the maintainers, 2026-11-11: withdrawing a snapshot doesn't hide its published views.** A published view of a withdrawn snapshot stays readable, and every answer about it carries the flag: `source.status: 'withdrawn'`, `source.withdrawn: true`, and a first-class disclosure line ("Source withdrawn: snapshot … was withdrawn by its publisher after this view was published. …") in projections and exports. The snapshot itself stays hidden from readers, as point 4 says. Why: views are cited and linked; hiding them silently breaks those links and hides that something changed; saved references are never rewritten or dropped silently (I18). A flag keeps the change visible. The alternative, hiding the views, was one access rule away and wasn't chosen. - **Payload's own reads carry it too (part 3).** A view has a `source` field that isn't stored: it is worked out each time the view is read, from the snapshot's head, and is the same object the endpoints answer with (`doc`, `id`, `version`, `checksum`, `status`, `withdrawn`). So a view read over REST, GraphQL or the Local API says its snapshot was withdrawn without a second request. It is null when the snapshot is gone. A `source` sent with a view is ignored. The snapshot's status is read once per request for each snapshot, and read again if the snapshot changes in that request. Populating the view's `snapshot` relationship still gives readers only its id once it is withdrawn, as point 4 says. 15. **Proposals are people's own, and accepting one makes a new staged version (part 4).** With the `proposals` option, a `taxonomy-proposals` collection holds one document per proposal: the snapshot it is drawn over, core's `Proposal` (title, notes, base, moves), a status, and a checksum of the proposal. The plugin sets the proposal's id, author (a person id) and date; anything else sent with it is dropped. @@ -41,7 +41,7 @@ 16. **The admin panels are a page over the same API, with no powers of their own (part 5).** Each of the plugin's four collections gets a panel at the top of its edit page, on unless `admin: { panels: false }`. - **A field on Payload's own edit page, not a custom view.** The panel is a `ui` field. Payload's save button, permissions, versions and the fields under the panel stay as they are, and there is no second edit page to keep in step with Payload. At first the cost was that a snapshot's edit page loaded the whole snapshot into Payload's JSON field below the panel; point 17 takes that away. - - **Browser code is a separate entry.** The panels are client components in `@captiveorigins/payload-plugin-onetaxonomy/client`, which Payload finds through its import map. `@payloadcms/ui` and `react` are optional peer dependencies, so a host that turns the panels off, or uses the plugin without an admin, needs neither. A test checks that the server entry imports no browser code and the client entry no server code. + - **Browser code is a separate entry.** The panels are client components in `@extantape/payload-plugin-onetaxonomy/client`, which Payload finds through its import map. `@payloadcms/ui` and `react` are optional peer dependencies, so a host that turns the panels off, or uses the plugin without an admin, needs neither. A test checks that the server entry imports no browser code and the client entry no server code. - **Every answer is the site's.** The panels call the host's own site from the browser, as the signed-in person, with their cookie. Publishing and withdrawing are Payload's standard `PATCH` of `status`, asking back only the fields the panel shows, so the answer never carries a snapshot's content. Running an import, accepting and declining are the endpoints of points 12 and 15. Which buttons appear follows Payload's own word on whether this person may save the document; that is a courtesy, and a button shown by mistake still gets the site's 403 or 404. A refusal is shown as "Nothing was changed." with the site's reason. - **One new endpoint.** `GET /api//:id/summary` (and `summarizeSnapshot`) answers what a snapshot holds, small enough for a page: its status, counts, sources with their licenses, a line per classification and phylogeny, and what it was made from. Lists are capped (20 by default, never past `limits.maxResults`) and say when they were cut. It never returns names, concepts or trees. Same access and the same 404 as point 8, after the checksum check of point 3. For a snapshot made from an accepted proposal it gives the two sealed person ids, and beside each the name `proposals.displayName` gives now: for a signed-in requester only, looked up per request, never stored. A visitor who may read the snapshot gets the ids and no names. - **Ask first, and say what can't be undone.** Publish, Withdraw, Accept and Decline open a confirm in the page, and nothing is sent before it. Running an import doesn't ask: the most it does is store a staged snapshot, which only managers see and which can be deleted. Publishing and withdrawing say that nothing moves back. Accepting lists what is sealed into the new version for good (the title, the notes and move notes, the manager's note, the date and the two ids) and asks the manager to check that none of it holds personal details, because point 15 doesn't screen free text. Declining says the author will see the note. Accepting sends the checksum the panel showed, so a proposal that changed meanwhile is refused (point 15). diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index d880cb4..f997050 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -9,8 +9,8 @@ | [0005](0005-source-ownership.md) | Sources belong to the project owner; OneTaxonomy ships adapters, not data | Accepted | | [0006](0006-view-actions.md) | Views change only through serializable actions | Accepted (implemented in M4) | | [0007](0007-releases.md) | Lockstep versions; tag releases staged on npm, approved by a maintainer (instead of Changesets) | Accepted | -| [0008](0008-package-scope.md) | npm names `@captiveorigins/onetaxonomy-*` | Accepted (owner) | +| [0008](0008-package-scope.md) | npm names `@extantape/onetaxonomy-*` | Accepted (owner) | | [0009](0009-license.md) | PolyForm Noncommercial 1.0.0 | Accepted (owner 2026-11-04) | -| [0010](0010-outline-is-the-keyboard-path.md) | The outline is the keyboard path into the tree; drawing nodes stay out of the Tab order | Accepted (Science, 2026-11-11) | -| [0011](0011-bundle-not-sqlite.md) | The Mac app keeps its work in files: a `.onetaxonomy` bundle, no SQLite for now | Accepted (Science, 2026-11-11) | -| [0012](0012-payload-plugin.md) | The Payload plugin stores sealed snapshots whole and answers bounded questions about them | Accepted (Science, 2026-11-11) | +| [0010](0010-outline-is-the-keyboard-path.md) | The outline is the keyboard path into the tree; drawing nodes stay out of the Tab order | Accepted (2026-11-11) | +| [0011](0011-bundle-not-sqlite.md) | The Mac app keeps its work in files: a `.onetaxonomy` bundle, no SQLite for now | Accepted (2026-11-11) | +| [0012](0012-payload-plugin.md) | The Payload plugin stores sealed snapshots whole and answers bounded questions about them | Accepted (2026-11-11) | diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md index db29c83..98a9951 100644 --- a/docs/architecture/architecture.md +++ b/docs/architecture/architecture.md @@ -1,6 +1,6 @@ # OneTaxonomy architecture -OneTaxonomy imports, curates, browses, rearranges the *presentation* of, and publishes scientifically faithful taxonomic classifications and phylogenetic trees. It is independent of any application: Captive Origins is its first consumer, but nothing here knows about Captive Origins, reptiles, Payload, Next.js or MongoDB. +OneTaxonomy imports, curates, browses, rearranges the *presentation* of, and publishes scientifically faithful taxonomic classifications and phylogenetic trees. It is independent of any application: nothing here knows about a particular site, reptiles, Payload, Next.js or MongoDB. The authoritative specification is the *OneTaxonomy — Architecture & Implementation Specification* (v1.0, November 2026). This document records how the repository implements it. Decisions with alternatives are in [`adr/`](adr/), the rules every component must keep are in [`scientific-invariants.md`](scientific-invariants.md), and the record formats are in [`data-schema.md`](data-schema.md). diff --git a/docs/architecture/backlog.md b/docs/architecture/backlog.md index 305f465..6a8aa5e 100644 --- a/docs/architecture/backlog.md +++ b/docs/architecture/backlog.md @@ -1,6 +1,6 @@ # Backlog -Since 2026-11-10 the open items below are tracked as tasks in the maintainers' task log (K235 to K241). This page stays as the record of what each milestone covers; the task log is where open work is ranked. +Since 2026-11-10 the open items below are tracked in the maintainers' own task list. This page stays as the record of what each milestone covers; that list is where open work is ranked. Task-level plan per milestone ([`phase-plan.md`](phase-plan.md)). `[x]` done with tests, `[ ]` to do. Integration dependencies on the project owner are marked **(owner)**. @@ -11,7 +11,7 @@ Task-level plan per milestone ([`phase-plan.md`](phase-plan.md)). `[x]` done wit - [x] ADRs 0001–0008 - [x] Illustrative fixtures: classification forest + conflicting hypotheses (amniotes); polytomy, forest, unrooted, fossil/time-calibrated, split names (structures) - [x] Release workflow (tag → staged npm publish → owner approval), lockstep versions -- [x] GitHub repository `CaptiveOrigins/onetaxonomy` (public); npm names `@captiveorigins/onetaxonomy-*` (ADR 0008) +- [x] GitHub repository `ExtantApe/OneTaxonomy` (public); npm names `@extantape/onetaxonomy-*` (ADR 0008) ## M2 — Core model, parser, validation, traversal ✅ @@ -21,7 +21,7 @@ Task-level plan per milestone ([`phase-plan.md`](phase-plan.md)). `[x]` done wit - [x] `validateDataset` diagnostics: duplicates, dangling refs, missing sources, cycles, multiple parents, self-edges, units, time calibration - [x] Snapshot sealing: canonical JSON, pure SHA-256 checksum, deep freeze, verification - [x] Name lookup with ambiguity (`findByName`) -- [x] Newick reader/writer (`@captiveorigins/onetaxonomy-import`): quoting, comments, support, rooting stated explicitly, explicit concept mapping +- [x] Newick reader/writer (`@extantape/onetaxonomy-import`): quoting, comments, support, rooting stated explicitly, explicit concept mapping - [x] Runnable example (`examples/`) - [x] JSON Schema export of the record types (M3: `packages/core/schema/snapshot.schema.json`) @@ -44,8 +44,8 @@ Task-level plan per milestone ([`phase-plan.md`](phase-plan.md)). `[x]` done wit - [x] `TaxonomicView` + `ViewAction` reducer (`applyViewAction`), undo/redo, `serializeView`, `validateView`, `projectView` - [x] Filters (status incl. unknown, rank, geography, traits, period, source, custom) that disclose hidden counts - [x] Highlights: `group` vs validated `clade` (I3) -- [x] `@captiveorigins/onetaxonomy-layout`: rectangular cladogram, radial, phylogram/time-scaled only when valid, classification sunburst (labelled as classification) -- [x] `@captiveorigins/onetaxonomy-react`: SVG tree viewer, explorer state hook, inspector, view controls; keyboard and screen-reader navigation; reduced motion +- [x] `@extantape/onetaxonomy-layout`: rectangular cladogram, radial, phylogram/time-scaled only when valid, classification sunburst (labelled as classification) +- [x] `@extantape/onetaxonomy-react`: SVG tree viewer, explorer state hook, inspector, view controls; keyboard and screen-reader navigation; reduced motion - [x] Clade selector UI (pick members, see the assessment) (M5, 2026-11-02); Canvas rendering with level of detail for very large trees → M8 - [x] `apps/playground` - [x] Acceptance (core): drag/collapse/filter/rotate with undo/redo; snapshot checksum unchanged (`view.test.ts`) and through the explorer (`react.test.tsx`) @@ -54,7 +54,7 @@ Task-level plan per milestone ([`phase-plan.md`](phase-plan.md)). `[x]` done wit - [x] Profiles and evidence in the inspector; support, fossil, extinction and trait overlays (observed traits: `TraitSet`); citations - [x] Export SVG/PNG/PDF (labelled classification vs phylogeny, hidden branches disclosed), JSON view, embeds (PDF via print; embeds = server-rendered `TreeView`) -- [x] `@captiveorigins/onetaxonomy-education`: scenes, annotations, camera states, reveals (step-by-step view actions; animated transitions are the renderer's), lesson decks, worksheets with computed answers; Present, Explore and Studio modes (`LessonPlayer`) +- [x] `@extantape/onetaxonomy-education`: scenes, annotations, camera states, reveals (step-by-step view actions; animated transitions are the renderer's), lesson decks, worksheets with computed answers; Present, Explore and Studio modes (`LessonPlayer`) ## Proposed mode (owner 2026-11-03; done 2026-11-05) @@ -71,11 +71,11 @@ Task-level plan per milestone ([`phase-plan.md`](phase-plan.md)). `[x]` done wit ## M7 — Payload plugin -- [ ] `@captiveorigins/payload-plugin-onetaxonomy` with configurable slugs, locale, permissions, optional tenant scope, source adapters (part 1, 2026-11-11, [ADR 0012](adr/0012-payload-plugin.md): slugs, locale for common-name search, read and manage callbacks that fail closed, tenant scope, sealed snapshots and pinned views with staged/published/withdrawn states; part 2, 2026-11-11: the six import adapters run in import jobs, configured per job; part 4, 2026-11-11: proposals, with a third callback, `access.propose`. Not ticked: admin views remain; the example app is done, below) +- [ ] `@extantape/payload-plugin-onetaxonomy` with configurable slugs, locale, permissions, optional tenant scope, source adapters (part 1, 2026-11-11, [ADR 0012](adr/0012-payload-plugin.md): slugs, locale for common-name search, read and manage callbacks that fail closed, tenant scope, sealed snapshots and pinned views with staged/published/withdrawn states; part 2, 2026-11-11: the six import adapters run in import jobs, configured per job; part 4, 2026-11-11: proposals, with a third callback, `access.propose`. Not ticked: admin views remain; the example app is done, below) - [ ] Admin views: import progress, conflicts, curation preview, tree authoring, profiles, publishing, lessons (part 5, 2026-11-11, [ADR 0012](adr/0012-payload-plugin.md), point 16: panels on the four collections' edit pages for import progress with re-runs, publishing and withdrawing snapshots and views with a bounded drawing of a view, and a manager's look at a proposal before accepting or declining. Tested in jsdom and over HTTP in the example site, and clicked through once in headless Chrome; no screen reader, Safari or Firefox. Not built: list views, conflicts, tree authoring, profiles, lessons, and a page for making a proposal) - [x] Bounded API: search, ancestors/descendants, MRCA, coverage, projected views, exports; access checks; cross-tenant denial tests (part 1, 2026-11-11: search, ancestors, descendants and MRCA with access checks, limits, 404 for anything unreadable and cross-tenant denial tests; part 2, 2026-11-11: coverage, projected views with their disclosure, and snapshot and view exports, under the same rules and tests; 2026-11-11: every endpoint also answers through Payload's own REST router with a signed-in token, in-process (`test/http.test.ts`). 2026-11-11: run in a real site behind Next.js, with cookies, by `apps/payload-example`'s smoke test, below) - [x] Resumable, idempotent import jobs (part 2, 2026-11-11: a `taxonomy-imports` collection, not Payload's jobs queue (ADR 0012, point 12); staged sealed snapshots, "already imported" on a re-run, failures store nothing, interrupted jobs restart from the file; part 3, 2026-11-11: a job is claimed by one conditional update, so runs started together can't both proceed; tested on an in-memory MongoDB, not in a real site or on Postgres or SQLite) -- [x] Withdrawn snapshots' published views stay readable with a "withdrawn source" flag (ADR 0012, point 14, accepted by the area lead 2026-11-11): on the projection and export endpoints (part 2) and as a `source` field on Payload's own reads of a view (part 3) +- [x] Withdrawn snapshots' published views stay readable with a "withdrawn source" flag (ADR 0012, point 14, accepted by the maintainers 2026-11-11): on the projection and export endpoints (part 2) and as a `source` field on Payload's own reads of a view (part 3) - [x] `apps/payload-example` (2026-11-11: a Payload 3.93 and Next.js 16 site on a throwaway in-memory MongoDB, on this machine only. Two roles; proposals record people by account id and show a public handle looked up when read (changed 2026-11-11); public Server Component pages that draw the published views with `TreeView` and `TreeOutline`; a seed of fixture data. `pnpm smoke` builds it, starts it and runs 47 checks over HTTP with sign-in cookies, in CI too. Not tested: a person clicking through the admin, a screen reader, Postgres or SQLite, a deployed site. It has no pages for proposals or imports: those wait for the admin views) ## M8 — Science and performance polish @@ -89,4 +89,4 @@ Task-level plan per milestone ([`phase-plan.md`](phase-plan.md)). `[x]` done wit ## Phase 2 (not started by design) -- [ ] `@captiveorigins/onetaxonomy-live` protocol and client; `services/live-server`; session codes; challenges evaluated by core +- [ ] `@extantape/onetaxonomy-live` protocol and client; `services/live-server`; session codes; challenges evaluated by core diff --git a/docs/architecture/bundle-format.md b/docs/architecture/bundle-format.md index 5e97843..05f739a 100644 --- a/docs/architecture/bundle-format.md +++ b/docs/architecture/bundle-format.md @@ -2,7 +2,7 @@ A bundle keeps a piece of work together: the dataset, the views and lessons made with it, and media. It's a plain ZIP file, so you can open it with Finder, `unzip` or any ZIP library and read every file in it without OneTaxonomy. Why it's a file and not a database: [ADR 0011](adr/0011-bundle-not-sqlite.md). -The code is `writeBundle` and `readBundle` in `@captiveorigins/onetaxonomy-import` ([`bundle.ts`](../../packages/import/src/bundle.ts)). The Mac app's Save as bundle… and Open… use them. +The code is `writeBundle` and `readBundle` in `@extantape/onetaxonomy-import` ([`bundle.ts`](../../packages/import/src/bundle.ts)). The Mac app's Save as bundle… and Open… use them. ## Layout diff --git a/docs/architecture/data-schema.md b/docs/architecture/data-schema.md index 1c3e9da..f42f245 100644 --- a/docs/architecture/data-schema.md +++ b/docs/architecture/data-schema.md @@ -1,6 +1,6 @@ # Data schema (version 1) -The TypeScript types in [`packages/core/src/model.ts`](../../packages/core/src/model.ts) are the definitive schema; this page explains them. All records are plain JSON: ISO date strings, no `Map`s or classes. `schemaVersion` changes only with a migration in `@captiveorigins/onetaxonomy-import`. +The TypeScript types in [`packages/core/src/model.ts`](../../packages/core/src/model.ts) are the definitive schema; this page explains them. All records are plain JSON: ISO date strings, no `Map`s or classes. `schemaVersion` changes only with a migration in `@extantape/onetaxonomy-import`. ## Identity @@ -23,7 +23,7 @@ The TypeScript types in [`packages/core/src/model.ts`](../../packages/core/src/m | `TaxonProfile` | Optional facts about a concept | `summary`, `status`, `geography`, `morphology`, `ecology`, `media[]` (with license/credit), `citations`, `extensions` (consumer-namespaced) | | `TraitSet` | Observed character states from one cited source | `title`, `source`, `states[] { id, label, color? }`, `values[] { concept, state, note? }` (one value per concept; never inferred: unstated concepts stay uncolored) | -`DatasetSnapshot.derivedFrom` records the snapshot (id, version, checksum) another one was made from, and how: the `selection` a subset was extracted with, or the `proposal` a curator accepted (`id`, `title`, `author`, `acceptedBy`, the `classification` version it was drawn over and the concepts `moved`). `acceptIntoSnapshot` makes that second kind: a new version in which the classification's version becomes `+`, each moved placement's provenance names the source record `proposal:`, and that record says who proposed and who accepted. The two names are inside the checksum, so they can't be taken out of a sealed snapshot later. Proposals on a phylogeny are refused. A JSON Schema for snapshots ships in `@captiveorigins/onetaxonomy-core/schema/snapshot.schema.json` for non-TypeScript consumers. +`DatasetSnapshot.derivedFrom` records the snapshot (id, version, checksum) another one was made from, and how: the `selection` a subset was extracted with, or the `proposal` a curator accepted (`id`, `title`, `author`, `acceptedBy`, the `classification` version it was drawn over and the concepts `moved`). `acceptIntoSnapshot` makes that second kind: a new version in which the classification's version becomes `+`, each moved placement's provenance names the source record `proposal:`, and that record says who proposed and who accepted. The two names are inside the checksum, so they can't be taken out of a sealed snapshot later. Proposals on a phylogeny are refused. A JSON Schema for snapshots ships in `@extantape/onetaxonomy-core/schema/snapshot.schema.json` for non-TypeScript consumers. ## Curation (M3) diff --git a/docs/architecture/limitations.md b/docs/architecture/limitations.md index 6375f64..da9eb9f 100644 --- a/docs/architecture/limitations.md +++ b/docs/architecture/limitations.md @@ -18,5 +18,5 @@ Kept current with each milestone. Nothing listed here is claimed as done. - Rooting (2026-11-11): `rerootPhylogeny` (outgroup of tips) and `midpointRoot` make new records and refuse time-calibrated hypotheses. Where the root sits along the outgroup branch is not known from the data; it defaults to halfway and the record says so. Neither is in the explorer or the Mac app yet. - Index building is in memory. Large-tree benchmarks (10k+ taxa) are planned for M8; traversal is iterative and tested on a 20,000-deep chain. - Fixtures are illustrative. They mirror widely discussed relationships (turtles with archosaurs) but are not sources and must not be cited. -- The five `@captiveorigins/onetaxonomy-*` packages are on npm as alpha versions (ADR 0008). The Payload plugin isn't published yet. +- The five `onetaxonomy-*` packages are on npm as alpha versions, under `@extantape` from 0.1.0-alpha.4 (ADR 0008). The Payload plugin isn't published yet. - Proposed mode on a phylogeny leaves a group with one remaining child in place rather than collapsing it. Moved branches lose their lengths, and groups whose members changed lose their support values and ages, so a proposal is not a tested hypothesis. Highlights keep the assessment of the hypothesis they name. Proposals are stored and shared by the Payload plugin (M7 part 4, 2026-11-11) and in the view file; the explorer and the Mac app don't save them anywhere else. A curator can accept a proposal on a classification into a new snapshot version (`acceptIntoSnapshot`); a proposal on a phylogeny is never accepted, because a tree rearranged by hand is a result its study never found. An accepted proposal's author and curator are named in the sealed snapshot for good. diff --git a/docs/architecture/phase-plan.md b/docs/architecture/phase-plan.md index a727713..a498dff 100644 --- a/docs/architecture/phase-plan.md +++ b/docs/architecture/phase-plan.md @@ -22,6 +22,6 @@ Nothing in Phase 1 depends on networking, accounts or a hosted service. Phase 1 - **Evaluation lives in `core`** and returns explanations (`MonophylyResult`, `MrcaResult`), so challenge checks (identify the MRCA, choose the smallest clade, find omitted descendants) reuse it instead of judging pixel positions. - **Lessons and scenes** (M5) are portable files that a live session can reference by id. -## Downstream: Captive Origins +## Downstream: a curated taxonomy site -Captive Origins' curated phylogenetic taxonomy (its own brief, 2026-11-02) depends on M3 (curated selections, coverage, staged import manifests), M4 (views, discovery categories rendered as non-clade groups) and M7 (Payload plugin). That integration lives in the Captive Origins repository and only consumes OneTaxonomy packages. +A site's curated phylogenetic taxonomy (its own brief, 2026-11-02) depends on M3 (curated selections, coverage, staged import manifests), M4 (views, discovery categories rendered as non-clade groups) and M7 (Payload plugin). That integration lives in the site's own repository and only consumes OneTaxonomy packages. diff --git a/docs/test-audit.md b/docs/test-audit.md index 8f12e7e..b056052 100644 --- a/docs/test-audit.md +++ b/docs/test-audit.md @@ -312,7 +312,7 @@ Scope: `packages/payload` only (M7 part 3, ADR 0012 points 12 and 14): the atomi ## 2026-11-11: Payload plugin part 2 -Scope: `packages/payload` only (M7 part 2, ADR 0012 points 12 to 14): import jobs (`src/imports.ts`), coverage and snapshot export (`src/api.ts`), view projection and export (`src/views-api.ts`). No other package changed; the plugin now depends on `@captiveorigins/onetaxonomy-import`. Tests went from 395 to 429: payload 46 to 80 (`imports.test.ts` 20 new, `api.test.ts` 15 to 27, `tenants.test.ts` 7 to 9, and the plugin-shape test in `snapshots.test.ts` widened). All on a real Payload 3.93 with an in-memory MongoDB replica set, no network: the host `fetch` in the tests answers from memory and the upload collection writes to a temporary folder that is deleted afterwards. Endpoints were called through their handlers with a local request, as in part 1. +Scope: `packages/payload` only (M7 part 2, ADR 0012 points 12 to 14): import jobs (`src/imports.ts`), coverage and snapshot export (`src/api.ts`), view projection and export (`src/views-api.ts`). No other package changed; the plugin now depends on `@extantape/onetaxonomy-import`. Tests went from 395 to 429: payload 46 to 80 (`imports.test.ts` 20 new, `api.test.ts` 15 to 27, `tenants.test.ts` 7 to 9, and the plugin-shape test in `snapshots.test.ts` widened). All on a real Payload 3.93 with an in-memory MongoDB replica set, no network: the host `fetch` in the tests answers from memory and the upload collection writes to a temporary folder that is deleted afterwards. Endpoints were called through their handlers with a local request, as in part 1. ### Covered @@ -335,7 +335,7 @@ Scope: `packages/payload` only (M7 part 2, ADR 0012 points 12 to 14): import job - **Interruption inside a store** isn't simulated: that a run leaves no partial snapshot rests on the snapshot being one document written last, not on a test that kills a process mid-write. - **Two runs of one job at once** weren't raced; claiming a job isn't atomic (ADR 0012). Closed in part 3, above. - **Large inputs:** nothing near `maxBytes` was parsed for real, and an import's time on a large file wasn't measured. -- **Payload's own reads of a view** don't carry the withdrawn flag; only the plugin's endpoints do. That waits for the lead's decision on point 14. Accepted and closed in part 3, above. +- **Payload's own reads of a view** don't carry the withdrawn flag; only the plugin's endpoints do. That waits for the maintainers' decision on point 14. Accepted and closed in part 3, above. ### Left out as overkill @@ -343,7 +343,7 @@ Scope: `packages/payload` only (M7 part 2, ADR 0012 points 12 to 14): import job - Every combination of adapter options: they go to the adapter unchanged. ## 2026-11-11: pan to the outline's selection -Scope: `packages/react` only (K495, ADR 0010's follow-up). Tests went from 395 to 408: react 102 to 115, all in the new `test/reveal.test.tsx`. Nothing else changed. +Scope: `packages/react` only (ADR 0010's follow-up). Tests went from 395 to 408: react 102 to 115, all in the new `test/reveal.test.tsx`. Nothing else changed. ### Covered @@ -368,7 +368,7 @@ Scope: `packages/react` only (K495, ADR 0010's follow-up). Tests went from 395 t ## 2026-11-11: Payload plugin, part 1 -Scope: the new `packages/payload` (`@captiveorigins/payload-plugin-onetaxonomy`, M7 part 1, ADR 0012). No other package changed. Tests went from 283 to 329: payload 0 to 46, in five files, each on a real Payload 3.93 with an in-memory MongoDB replica set (indexes built, no network; the MongoDB binary was already cached). Endpoints were called through their handlers with a local request, as the sister plugins do, not through Payload's HTTP routing. +Scope: the new `packages/payload` (`@extantape/payload-plugin-onetaxonomy`, M7 part 1, ADR 0012). No other package changed. Tests went from 283 to 329: payload 0 to 46, in five files, each on a real Payload 3.93 with an in-memory MongoDB replica set (indexes built, no network; the MongoDB binary was already cached). Endpoints were called through their handlers with a local request, as the sister plugins do, not through Payload's HTTP routing. ### Covered @@ -400,7 +400,7 @@ Scope: the new `packages/payload` (`@captiveorigins/payload-plugin-onetaxonomy`, - Every status transition pair: the allowed ones and three refused ones (published to staged, withdrawn to published, and starting published) are tested. ## 2026-11-11: OneOrigin brand source -Owner decision N105 (2026-11-11): "A: send it, accept guidelines (Recommended)". Tests went from 299 to 300: studio (Mac app) 40 to 41 (`apps/studio/test/theme.test.ts`). The rest are unchanged. +Owner decision (2026-11-11): "A: send it, accept guidelines (Recommended)". Tests went from 299 to 300: studio (Mac app) 40 to 41 (`apps/studio/test/theme.test.ts`). The rest are unchanged. - **Promises:** `brand/oneorigin/` matches `brand/oneorigin.lock` byte for byte, and CI fails on a hand edit; `apps/studio/src/oneorigin.css` is what `build` writes from the copy's tokens; the app's shell colors are the suite's tokens; nothing that was drawn changes color. - **Covered:** `node scripts/oneorigin.mjs check` in CI (11 files and the built CSS). `theme.test.ts` (1 new): `--bg`, `--panel`, `--text`, `--muted`, `--border`, `--accent`, `--focus` and `--error` in `app.css` equal the built `--ot-` tokens in light, system dark and a chosen Dark. The script itself is tested in OnePhylogeny, where it comes from (`scripts/oneorigin.test.mjs`, 19 tests: build output, lock and check passing, a hand edit failing, the warning paths); this repo carries the same file. @@ -470,7 +470,7 @@ Scope: M8's canvas renderer and benchmarks. React gains `CanvasTreeView`, chosen - Repeating the explorer's toolbar, outline and inspector tests on a canvas tree: they don't depend on the renderer. ## 2026-11-11: focus ring colour -Scope: the focus-ring gap from the accessibility audit below, closed by owner decision N154 (2026-11-11, option A): "keep the brand orange everywhere else, but use the darker orange #a65f20 for focus rings on light backgrounds (4.6 to 1 or better). Dark mode keeps the lighter orange, which already passes." Only colour variables and focus rules changed; core, import, layout and education are unchanged. Tests went from 281 to 283: react 85 to 86 (`a11y.test.tsx`), studio 39 to 40 (`theme.test.ts`). +Scope: the focus-ring gap from the accessibility audit below, closed by owner decision (2026-11-11, option A): "keep the brand orange everywhere else, but use the darker orange #a65f20 for focus rings on light backgrounds (4.6 to 1 or better). Dark mode keeps the lighter orange, which already passes." Only colour variables and focus rules changed; core, import, layout and education are unchanged. Tests went from 281 to 283: react 85 to 86 (`a11y.test.tsx`), studio 39 to 40 (`theme.test.ts`). - **Before and after (focus colour on the background it sits on):** light, explorer, outline, lesson and tip-links rings (`--ot-focus`): `#cf8137` 3.07:1 on the surface (`#ffffff`), 2.88:1 on the panel (`#fbf7f4`); now `#a65f20`, 4.91:1 and 4.61:1. Light, studio rings: `--accent` `#cf8137` 3.07:1 on the page and fields, 2.89:1 on the toolbar (`#fdf7f7`); now `--focus` `#a65f20`, 4.91:1 and 4.63:1. Dark, unchanged: `#e7a066` 8.36:1 on the explorer surface (`#1c1311`), 7.85:1 on its panel (`#241916`); 8.37:1 on the studio page, 7.82:1 on its panel, 7.11:1 on fields. - **Covered:** `a11y.test.tsx`: the contrast test now checks the focus ring on the panel as well as the surface (it checked only the surface, which is why 2.88:1 passed); a new test pins light `--ot-focus` at `#a65f20` (4.6:1 or better on surface and panel), dark at `#e7a066`, selection still `#cf8137`, and every outline focus rule on `var(--ot-focus)`. `theme.test.ts`: the studio's focus check now reads `--focus` on page, panel and field in light and dark (it read `--accent` on the page only); a new test pins `--focus` at `#a65f20` in light and `#e7a066` in both dark blocks, `--accent` still `#cf8137`, and both studio focus rules on `var(--focus)`. The existing check that a chosen appearance copies the library's values now covers the new `--ot-focus`. @@ -523,7 +523,7 @@ Scope: two gaps from the sections below. The rooting note moves from the explore ## 2026-11-11: accessibility gaps closed -Scope: the small and medium gaps from the accessibility audit below. The brand-orange focus ring is left as it is: that's the owner's decision (N154). Core and import are unchanged. Tests went from 248 to 263: react 58 to 71 (13 new in `a11y.test.tsx`), studio 37 to 39 (2 in `app.test.tsx`; `theme.test.ts` now checks the error color and the alert region). Everything ran in jsdom with Tauri mocked. +Scope: the small and medium gaps from the accessibility audit below. The brand-orange focus ring is left as it is: that's the owner's decision. Core and import are unchanged. Tests went from 248 to 263: react 58 to 71 (13 new in `a11y.test.tsx`), studio 37 to 39 (2 in `app.test.tsx`; `theme.test.ts` now checks the error color and the alert region). Everything ran in jsdom with Tauri mocked. ### Fixed, with tests @@ -538,7 +538,7 @@ Scope: the small and medium gaps from the accessibility audit below. The brand-o ### Gaps left -- **The focus ring** is 2.88:1 on the panels: owner decision N154. Since fixed: see "2026-11-11: focus ring colour". +- **The focus ring** is 2.88:1 on the panels: owner decision. Since fixed: see "2026-11-11: focus ring colour". - **Shapes repeat after six** highlights or trait states. Past that, color and the words in the inspector and legend tell them apart. (Addressed in "possible tip matches" above.) - **Only the first highlight on a node is drawn.** That hasn't changed: a node in two highlights shows one halo. - **The drawing's nodes still aren't reachable with Tab** (the outline is the way in; why is now ADR 0010), and live regions that mount with their first message (the clade check with a second taxon) still need a screen reader to confirm. @@ -565,7 +565,7 @@ Scope: the gap left in the section below ("Comparison in the Mac app always refu ### Gaps left -- **Only exact names are proposed.** Spelling variants, authorship in the label ("Alphus primus Fixture, 2026"), trinomials written differently, and labels with specimen codes ("Alphus_primus_AB123") match nothing and must be found by hand. Fuzzy or rule-based matching would be a separate, labelled method, and is a decision for the lead. (Addressed in "possible tip matches" above.) +- **Only exact names are proposed.** Spelling variants, authorship in the label ("Alphus primus Fixture, 2026"), trinomials written differently, and labels with specimen codes ("Alphus_primus_AB123") match nothing and must be found by hand. Fuzzy or rule-based matching would be a separate, labelled method, and is a decision for the maintainers. (Addressed in "possible tip matches" above.) - **Long trees.** Every tip is a table row with a select. Not timed beyond the fixtures; a tree with thousands of tips may want paging or a filter for unmatched rows. (Addressed in "possible tip matches" above.) - **Not run for real:** VoiceOver reading the table, the selects' optgroups and the status region; the datalist suggestions in WebKit; the Tauri dialogs and writes in the built app. - **Add tree… needs the taxa first.** A tree opened alone can't be linked: the step says so. Opening a snapshot that holds both is the other way in. @@ -589,7 +589,7 @@ Scope: the explorer and Mac app controls for the features in the previous sectio ### Gaps left -- **Comparison in the Mac app always refuses files it imports itself.** Newick, NEXUS and phyloXML tips are never mapped to taxa (by design: no matching by label), and comparison needs mappings. It works on snapshots that carry mappings. A way to map tips explicitly in the app (a mapping table the person confirms) is a feature, for the lead's list. +- **Comparison in the Mac app always refuses files it imports itself.** Newick, NEXUS and phyloXML tips are never mapped to taxa (by design: no matching by label), and comparison needs mappings. It works on snapshots that carry mappings. A way to map tips explicitly in the app (a mapping table the person confirms) is a feature, for the maintainers' list. - **Two controls are named "Show"** in the Mac app: the header's tree or classification picker and the explorer's living/extinct filter. They existed before; a clearer name for the header one ("Tree or classification") is a small wording change left to the owner. (Closed in "rooting in describeView" above.) - **Differing clades aren't highlighted in the drawing.** "Select" puts one in the selection; drawing all of them at once would need a highlight that isn't saved in the view. Clades only in the other tree have no node here, so they're listed only. - **The rooting note is added by the explorer, not by `describeView`.** Exports from the explorer include it; another app calling `describeView` directly won't see it. Adding it in core would change core's output, which this branch was asked not to do. (Closed in "rooting in describeView" above.) @@ -664,7 +664,7 @@ Scope: milestone M8's accessibility audit. A code-level check of the React compo ### Gaps left -- **The brand-orange focus ring is 2.88:1 on the explorer panel** (`#cf8137` on `#fbf7f4`) and 2.89:1 on the studio toolbar (`#fdf7f7`), under 3:1. It's 3.07:1 on white and 7.8:1 or more in dark. `brand/README.md` names this exact color for focus and is kept identical across the One tools, so changing it is the owner's call: a deeper orange such as `#a65f20` for the ring, or panels close to white (`#fefcfb` gives 3.00:1). **Fixed 2026-11-11** (owner decision N154, option A): the ring is now `#a65f20` in light, 4.61:1 on the explorer panel and 4.63:1 on the studio toolbar; dark keeps `#e7a066`. See "2026-11-11: focus ring colour" above. +- **The brand-orange focus ring is 2.88:1 on the explorer panel** (`#cf8137` on `#fbf7f4`) and 2.89:1 on the studio toolbar (`#fdf7f7`), under 3:1. It's 3.07:1 on white and 7.8:1 or more in dark. `brand/README.md` names this exact color for focus and is kept identical across the One tools, so changing it is the owner's call: a deeper orange such as `#a65f20` for the ring, or panels close to white (`#fefcfb` gives 3.00:1). **Fixed 2026-11-11** (owner decision, option A): the ring is now `#a65f20` in light, 4.61:1 on the explorer panel and 4.63:1 on the studio toolbar; dark keeps `#e7a066`. See "2026-11-11: focus ring colour" above. - **Highlights and trait states are told apart by color in the drawing.** Each node's highlights and trait state are in words in the inspector, and the legend names each color, but the halos (35% opacity) and trait dots have no shape or pattern of their own, and trait colors come from the data unchecked. Fixing it means a shape or letter per highlight or state: a design change. - **Dragging has no single-pointer alternative for two actions (2.5.7).** Reordering siblings (Alt+Up/Down in the outline is keyboard only; "Flip children" only reverses) and panning (zoom and fit are buttons, panning isn't). Moving under a new parent has the "Move under" list. Small "move up/down" buttons in the inspector and pan buttons would close it. - **The drawing's nodes are `role="button"` inside an SVG with `role="img"`,** whose children screen readers ignore, and they can't be reached with Tab. The outline is the accessible way to the same nodes, by design. Making the drawing itself navigable is larger and overlaps the planned canvas renderer. @@ -721,7 +721,7 @@ All new tests passed on the current code. No bug was found. ### Gaps left -- **The studio says nothing when it opens an unsealed snapshot.** `readSnapshotJson` reports `unsealed`, but `openSnapshot` only warns on `mismatch`. A sealed file whose `checksum` line was deleted therefore opens with no notice. A notice such as "This file isn't sealed, so later changes to it can't be detected" is a small change to `apps/studio/src/open.ts` plus one test. It changes what the app shows, so it's left for the lead rather than done in an audit. +- **The studio says nothing when it opens an unsealed snapshot.** `readSnapshotJson` reports `unsealed`, but `openSnapshot` only warns on `mismatch`. A sealed file whose `checksum` line was deleted therefore opens with no notice. A notice such as "This file isn't sealed, so later changes to it can't be detected" is a small change to `apps/studio/src/open.ts` plus one test. It changes what the app shows, so it's left for the maintainers rather than done in an audit. - **A failed rename leaves `.tmp` beside the chosen file.** The old file is safe and the error is shown, but the temporary file isn't removed. Minor; fix with the next studio change. - CRC checks on ZIP entries, and large archives read into memory, as in the first audit. Both are stated limitations. - The studio's dialogs and file calls are mocked. Saving on a real disk is only checked by hand. diff --git a/examples/build-fixtures.mjs b/examples/build-fixtures.mjs index 04f2912..5b275ee 100644 --- a/examples/build-fixtures.mjs +++ b/examples/build-fixtures.mjs @@ -1,8 +1,8 @@ // Regenerates fixtures/checklist*.json from the CSV files (run after `pnpm build`): // node examples/build-fixtures.mjs import { readFileSync, writeFileSync } from 'node:fs' -import { sealSnapshot } from '@captiveorigins/onetaxonomy-core' -import { readChecklistTable, DARWIN_CORE_COLUMNS, toSnapshot } from '@captiveorigins/onetaxonomy-import' +import { sealSnapshot } from '@extantape/onetaxonomy-core' +import { readChecklistTable, DARWIN_CORE_COLUMNS, toSnapshot } from '@extantape/onetaxonomy-import' const dir = new URL('../fixtures/', import.meta.url) for (const [file, release] of [['checklist', '1'], ['checklist-v2', '2']]) { diff --git a/examples/newick.mjs b/examples/newick.mjs index b3987d3..3bdcafc 100644 --- a/examples/newick.mjs +++ b/examples/newick.mjs @@ -1,6 +1,6 @@ // Read a Newick tree, keep its polytomy, and ask questions with stated rooting. -import { findMRCA, indexPhylogeny, isMonophyletic } from '@captiveorigins/onetaxonomy-core' -import { parseNewick, toNewick } from '@captiveorigins/onetaxonomy-import' +import { findMRCA, indexPhylogeny, isMonophyletic } from '@extantape/onetaxonomy-core' +import { parseNewick, toNewick } from '@extantape/onetaxonomy-import' const tree = parseNewick('((A,B,C,D)Unresolved,(E,F)Pair)Root;', { id: 'demo', title: 'Demo', version: '1', sources: ['demo'], rooted: true }) const index = indexPhylogeny(tree) diff --git a/examples/package.json b/examples/package.json index 93c5d72..f230736 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,5 +1,5 @@ { - "name": "@captiveorigins/onetaxonomy-examples", + "name": "@extantape/onetaxonomy-examples", "private": true, "type": "module", "scripts": { @@ -8,7 +8,7 @@ "fixtures": "node build-fixtures.mjs" }, "dependencies": { - "@captiveorigins/onetaxonomy-core": "workspace:^", - "@captiveorigins/onetaxonomy-import": "workspace:^" + "@extantape/onetaxonomy-core": "workspace:^", + "@extantape/onetaxonomy-import": "workspace:^" } } diff --git a/examples/reptilia-is-not-a-clade.mjs b/examples/reptilia-is-not-a-clade.mjs index 1eeebb8..5d58cb3 100644 --- a/examples/reptilia-is-not-a-clade.mjs +++ b/examples/reptilia-is-not-a-clade.mjs @@ -1,7 +1,7 @@ // Is traditional class Reptilia a clade? Ask each hypothesis, not the classification. -// Run: pnpm build && pnpm --filter @captiveorigins/onetaxonomy-examples start +// Run: pnpm build && pnpm --filter @extantape/onetaxonomy-examples start import { readFileSync } from 'node:fs' -import { assessTaxon, findMRCA, indexDataset, isMonophyletic, validateDataset } from '@captiveorigins/onetaxonomy-core' +import { assessTaxon, findMRCA, indexDataset, isMonophyletic, validateDataset } from '@extantape/onetaxonomy-core' const snapshot = JSON.parse(readFileSync(new URL('../fixtures/amniotes.json', import.meta.url), 'utf8')) for (const d of validateDataset(snapshot)) console.log(`[${d.severity}] ${d.message}`) diff --git a/examples/selective-import.mjs b/examples/selective-import.mjs index 2c3f375..d566648 100644 --- a/examples/selective-import.mjs +++ b/examples/selective-import.mjs @@ -2,8 +2,8 @@ // only those plus their ancestor paths, and show what was left out. // pnpm build && node examples/selective-import.mjs import { readFileSync } from 'node:fs' -import { indexDataset, sealSnapshot, diffSnapshots, referenceImpact, getCoverageReport } from '@captiveorigins/onetaxonomy-core' -import { readChecklistTable, toSnapshot, selectiveImport } from '@captiveorigins/onetaxonomy-import' +import { indexDataset, sealSnapshot, diffSnapshots, referenceImpact, getCoverageReport } from '@extantape/onetaxonomy-core' +import { readChecklistTable, toSnapshot, selectiveImport } from '@extantape/onetaxonomy-import' const csv = readFileSync(new URL('../fixtures/checklist.csv', import.meta.url), 'utf8') const read = readChecklistTable(csv, { diff --git a/fixtures/README.md b/fixtures/README.md index 77776fd..3afecae 100644 --- a/fixtures/README.md +++ b/fixtures/README.md @@ -9,4 +9,4 @@ Small, **illustrative** datasets for tests, examples and documentation. Every fi | `checklist.csv` → `checklist.json` | A parent-pointer checklist in Darwin Core columns with **invented names** (Exemplaria, Alphus …): synonyms, a heterotypic synonym, a provisional species, common names in two languages. Used for adapters, curation, coverage and the selective-import demo. | | `checklist-v2.csv` → `checklist-v2.json` | "Release 2" of the same checklist: a rename, two relocations, a rank change, a split candidate (misapplied name) and a lump (removed species now a synonym). Used by `diffSnapshots`. | -Regenerate the JSON from the CSV files with `pnpm build && pnpm --filter @captiveorigins/onetaxonomy-examples fixtures`; the CSV adapter test checks that the committed JSON still matches. +Regenerate the JSON from the CSV files with `pnpm build && pnpm --filter @extantape/onetaxonomy-examples fixtures`; the CSV adapter test checks that the committed JSON still matches. diff --git a/package.json b/package.json index 21d1a36..4e7e221 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "react", "payload-plugin" ], - "homepage": "https://github.com/CaptiveOrigins/onetaxonomy#readme", + "homepage": "https://github.com/ExtantApe/OneTaxonomy#readme", "bugs": { - "url": "https://github.com/CaptiveOrigins/onetaxonomy/issues" + "url": "https://github.com/ExtantApe/OneTaxonomy/issues" }, "license": "PolyForm-Noncommercial-1.0.0", "type": "module", diff --git a/packages/core/README.md b/packages/core/README.md index 8f4213c..7c361fc 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,9 +1,9 @@ -# @captiveorigins/onetaxonomy-core +# @extantape/onetaxonomy-core The scientific core of OneTaxonomy: taxon concepts, names, classifications and phylogenetic hypotheses, with validation, bounded traversal, MRCA and **tri-state monophyly**. No dependencies; no DOM, Node, React, CMS or database. It runs in browsers, Node, workers and desktop webviews. ```js -import { assessTaxon, findMRCA, indexDataset, isMonophyletic, sealSnapshot, validateDataset } from '@captiveorigins/onetaxonomy-core' +import { assessTaxon, findMRCA, indexDataset, isMonophyletic, sealSnapshot, validateDataset } from '@extantape/onetaxonomy-core' const problems = validateDataset(snapshot) // diagnostics; nothing is repaired silently const sealed = sealSnapshot(snapshot) // checksummed and frozen @@ -27,7 +27,7 @@ assessTaxon(data.classification('traditional'), 'c:reptilia', null) ## Comparing and rooting hypotheses ```js -import { comparePhylogenies, midpointRoot, rerootPhylogeny } from '@captiveorigins/onetaxonomy-core' +import { comparePhylogenies, midpointRoot, rerootPhylogeny } from '@extantape/onetaxonomy-core' comparePhylogenies(treeA, treeB) // { status: 'compared', mode: 'unrooted', rf: 2, normalized: 0.5, shared, onlyInA, onlyInB, inAOnly, inBOnly, notes } @@ -43,7 +43,7 @@ const midpoint = midpointRoot(unrootedTree, { id: 'my-tree-midpoint', version: ' ## Curation, coverage and releases ```js -import { applySelection, diffSnapshots, extractSubset, getCoverageReport, referenceImpact } from '@captiveorigins/onetaxonomy-core' +import { applySelection, diffSnapshots, extractSubset, getCoverageReport, referenceImpact } from '@extantape/onetaxonomy-core' const result = applySelection(data, selection) // curated + ancestor paths, omitted branches, diagnostics const coverage = getCoverageReport(data, { @@ -54,6 +54,6 @@ const subset = extractSubset(snapshot, { id, version, createdAt, classification: const changes = referenceImpact(diffSnapshots(oldRelease, newRelease), mySavedIds) // renamed, relocated, split/merge candidates… ``` -`schema/snapshot.schema.json` (exported as `@captiveorigins/onetaxonomy-core/schema/snapshot.schema.json`) describes the snapshot format for non-TypeScript tools. +`schema/snapshot.schema.json` (exported as `@extantape/onetaxonomy-core/schema/snapshot.schema.json`) describes the snapshot format for non-TypeScript tools. See the [data schema](../../docs/architecture/data-schema.md) and [invariants](../../docs/architecture/scientific-invariants.md). diff --git a/packages/core/package.json b/packages/core/package.json index 05c1c2c..e80f597 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { - "name": "@captiveorigins/onetaxonomy-core", - "version": "0.1.0-alpha.3", + "name": "@extantape/onetaxonomy-core", + "version": "0.1.0-alpha.4", "description": "OneTaxonomy core: taxon concepts, classifications and phylogenetic hypotheses, with validation, bounded traversal, MRCA, tri-state monophyly, curated selections, coverage reports and snapshot diffs. No React, CMS, database or platform dependency.", "keywords": [ "taxonomy", @@ -11,14 +11,14 @@ "classification", "biodiversity" ], - "homepage": "https://github.com/CaptiveOrigins/onetaxonomy#readme", + "homepage": "https://github.com/ExtantApe/OneTaxonomy#readme", "bugs": { - "url": "https://github.com/CaptiveOrigins/onetaxonomy/issues" + "url": "https://github.com/ExtantApe/OneTaxonomy/issues" }, "license": "PolyForm-Noncommercial-1.0.0", "repository": { "type": "git", - "url": "git+https://github.com/CaptiveOrigins/onetaxonomy.git", + "url": "git+https://github.com/ExtantApe/OneTaxonomy.git", "directory": "packages/core" }, "type": "module", diff --git a/packages/core/schema/snapshot.schema.json b/packages/core/schema/snapshot.schema.json index 3bb0f26..b41f75b 100644 --- a/packages/core/schema/snapshot.schema.json +++ b/packages/core/schema/snapshot.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/mgilardi/onetaxonomy/blob/main/packages/core/schema/snapshot.schema.json", "title": "OneTaxonomy dataset snapshot (schema version 1)", - "description": "Structure only. Referential integrity, cycles, units and scientific caveats are checked by validateDataset in @captiveorigins/onetaxonomy-core; see docs/architecture/data-schema.md.", + "description": "Structure only. Referential integrity, cycles, units and scientific caveats are checked by validateDataset in @extantape/onetaxonomy-core; see docs/architecture/data-schema.md.", "type": "object", "required": ["schemaVersion", "id", "version", "createdAt", "sources", "names", "concepts", "classifications", "phylogenies"], "additionalProperties": false, diff --git a/packages/core/src/validate.ts b/packages/core/src/validate.ts index 80d9891..0b358da 100644 --- a/packages/core/src/validate.ts +++ b/packages/core/src/validate.ts @@ -36,7 +36,7 @@ export function validateDataset(s: DatasetSnapshot): Diagnostic[] { const out: Diagnostic[] = [] const add = (severity: Severity, code: DiagnosticCode, path: string, message: string, extra: Partial = {}) => out.push({ severity, code, path, message, ...extra }) - if (s.schemaVersion !== SCHEMA_VERSION) add('error', 'schema-version', 'schemaVersion', `Schema version ${s.schemaVersion} is not supported (expected ${SCHEMA_VERSION}).`, { suggestion: 'Migrate the snapshot with the matching @captiveorigins/onetaxonomy-import release.' }) + if (s.schemaVersion !== SCHEMA_VERSION) add('error', 'schema-version', 'schemaVersion', `Schema version ${s.schemaVersion} is not supported (expected ${SCHEMA_VERSION}).`, { suggestion: 'Migrate the snapshot with the matching @extantape/onetaxonomy-import release.' }) // Identity: ids are unique within each record family. const unique = (family: string, ids: Id[]) => { diff --git a/packages/education/README.md b/packages/education/README.md index 2f53053..f56d315 100644 --- a/packages/education/README.md +++ b/packages/education/README.md @@ -1,10 +1,10 @@ -# @captiveorigins/onetaxonomy-education +# @extantape/onetaxonomy-education Lessons over a pinned OneTaxonomy snapshot: decks of **scenes** (a view, a camera, annotations and step-by-step **reveals**) and **worksheets** whose answers are computed by the core science functions, never stored. Portable JSON, no networking; a live classroom (Phase 2) only syncs scene and step numbers. ```js -import { createView, indexDataset } from '@captiveorigins/onetaxonomy-core' -import { createLesson, sceneAt, sceneFromView, validateLesson, worksheetMarkdown } from '@captiveorigins/onetaxonomy-education' +import { createView, indexDataset } from '@extantape/onetaxonomy-core' +import { createLesson, sceneAt, sceneFromView, validateLesson, worksheetMarkdown } from '@extantape/onetaxonomy-education' const data = indexDataset(snapshot) const deck = createLesson(data, { id: 'reptiles', title: 'Are reptiles a clade?' }) @@ -21,6 +21,6 @@ worksheetMarkdown(deck, 'w1', data, { answers: true }) // printable, with the co - **Questions**: `is-clade` (yes / no / can't tell), `mrca`, `missing` (what to add to make a clade), `open`. `answerQuestion` and `checkResponse` evaluate them with `isMonophyletic` and `findMRCA` over the scene's rooted hypothesis, with the reasons in words (`describeAssessment`). Clade questions on a classification are refused: a classification states no ancestry. - **Pinned data**: a deck records the snapshot id, version and checksum; opening it over another snapshot warns, and answers are recomputed from what is loaded. -- **Players**: `LessonPlayer` in `@captiveorigins/onetaxonomy-react` presents (locked drawing), explores (full explorer from the current step) and authors (studio: scenes from the current view, annotations). +- **Players**: `LessonPlayer` in `@extantape/onetaxonomy-react` presents (locked drawing), explores (full explorer from the current step) and authors (studio: scenes from the current view, annotations). PolyForm Noncommercial 1.0.0. diff --git a/packages/education/package.json b/packages/education/package.json index 2fc0f1c..b45f0f1 100644 --- a/packages/education/package.json +++ b/packages/education/package.json @@ -1,6 +1,6 @@ { - "name": "@captiveorigins/onetaxonomy-education", - "version": "0.1.0-alpha.3", + "name": "@extantape/onetaxonomy-education", + "version": "0.1.0-alpha.4", "description": "OneTaxonomy education: portable lesson decks of scenes over a pinned snapshot (views, camera, annotations, step-by-step reveals) and worksheets whose answers are checked by the core science functions.", "keywords": [ "taxonomy", @@ -11,14 +11,14 @@ "worksheets", "teaching" ], - "homepage": "https://github.com/CaptiveOrigins/onetaxonomy#readme", + "homepage": "https://github.com/ExtantApe/OneTaxonomy#readme", "bugs": { - "url": "https://github.com/CaptiveOrigins/onetaxonomy/issues" + "url": "https://github.com/ExtantApe/OneTaxonomy/issues" }, "license": "PolyForm-Noncommercial-1.0.0", "repository": { "type": "git", - "url": "git+https://github.com/CaptiveOrigins/onetaxonomy.git", + "url": "git+https://github.com/ExtantApe/OneTaxonomy.git", "directory": "packages/education" }, "type": "module", @@ -46,6 +46,6 @@ "prepack": "pnpm run build" }, "dependencies": { - "@captiveorigins/onetaxonomy-core": "workspace:^" + "@extantape/onetaxonomy-core": "workspace:^" } } diff --git a/packages/education/src/index.ts b/packages/education/src/index.ts index 6b5b28a..44877db 100644 --- a/packages/education/src/index.ts +++ b/packages/education/src/index.ts @@ -11,7 +11,7 @@ import { type PhylogenyIndex, type TaxonomicView, type ViewAction, -} from '@captiveorigins/onetaxonomy-core' +} from '@extantape/onetaxonomy-core' /** * Lessons (M5): portable decks of scenes over one pinned snapshot. A scene is a diff --git a/packages/education/test/education.test.ts b/packages/education/test/education.test.ts index 66a9cc8..9f48660 100644 --- a/packages/education/test/education.test.ts +++ b/packages/education/test/education.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { createView, indexDataset, type DatasetSnapshot } from '@captiveorigins/onetaxonomy-core' +import { createView, indexDataset, type DatasetSnapshot } from '@extantape/onetaxonomy-core' import amniotesJson from '../../../fixtures/amniotes.json' import { answerQuestion, checkResponse, createLesson, parseLesson, sceneAt, sceneFromView, serializeLesson, validateLesson, worksheetMarkdown, type LessonDeck } from '../src' diff --git a/packages/import/README.md b/packages/import/README.md index 9733725..d956021 100644 --- a/packages/import/README.md +++ b/packages/import/README.md @@ -1,11 +1,11 @@ -# @captiveorigins/onetaxonomy-import +# @extantape/onetaxonomy-import Source adapters, readers and writers, reconciliation and curated selective imports for OneTaxonomy. It ships no data and picks no sources: you bring yours. Adapters take file contents (text or bytes), not paths, so they run in browsers, Node, workers and desktop webviews. ## Newick ```js -import { parseNewick, toNewick } from '@captiveorigins/onetaxonomy-import' +import { parseNewick, toNewick } from '@extantape/onetaxonomy-import' const tree = parseNewick("((A_b:0.1,'C d':0.2)95:0.05,F:1e-3);", { id: 'my-tree', title: 'My tree', version: '1', sources: ['my-source'], @@ -21,7 +21,7 @@ Polytomies are kept, quoted labels and comments preserved (comments become `anno ## NEXUS and phyloXML ```js -import { nexusAdapter, phyloXmlAdapter } from '@captiveorigins/onetaxonomy-import' +import { nexusAdapter, phyloXmlAdapter } from '@extantape/onetaxonomy-import' const source = { id: 'my-study', title: 'My study', citation: '…', license: 'CC BY 4.0' } const nexus = await nexusAdapter({ source, id: 'my-study', version: '1', lengthUnit: 'substitutions-per-site', mapLabel }).read(text) @@ -40,7 +40,7 @@ Both refuse malformed input with the line (and the element, for phyloXML). Neith Every adapter implements `SourceAdapter