Skip to content

feat(plugins): add sandboxed taxonomy writes - #3169

Merged
ascorbic merged 10 commits into
mainfrom
codex/taxonomy-writes
Sep 19, 2026
Merged

ascorbic merged 10 commits into
mainfrom
codex/taxonomy-writes

Conversation

@ascorbic

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds the taxonomies:write sandboxed-plugin capability as a complete vertical slice. Plugins can create taxonomy terms and apply idempotent add/remove assignment deltas through ctx.taxonomies; write implies read, uses term row or translation-group IDs, validates collection attachment, entry and term ownership, configured locales, translation identity, and hierarchy, and invalidates taxonomy caches after successful mutations.

The mutation path is owned by core and is shared by native execution, Cloudflare Worker Loader, and Node/workerd. Registry metadata, manifest reconciliation, marketplace validation, installation/update consent, generated CLI artifacts, plugin-test fixtures and inspectors, public docs, both creating-plugins skills, and package changesets carry the same authority.

A forward migration makes (translation_group, locale) unique for taxonomy terms. Existing invalid duplicate rows are split into independent groups after their assignments are copied, and the migration is restart-safe.

Taxonomy-definition management, attachment changes, assignment replacement, term updates, and term deletion remain out of scope. This adds no logged-out route or query.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: No matching public Discussion covers this term/assignment capability; implementation was requested directly as PR 9 of the maintainer-authored sandboxed capability program.
  • I have included screenshots below if this PR changes the UI — not applicable; this adds localized capability consent copy to the existing dialog without changing its rendered structure or interaction.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: GPT-5 Codex

Screenshots / test output

Screenshots are not applicable. The existing consent dialog renders one new Lingui-wrapped capability label; there is no component, layout, or interaction change.

Validated at 725d983bb7a25e9448f085f6898ebc2901e1dc64:

  • pnpm build
  • pnpm typecheck
  • pnpm lint:json | jq '.diagnostics | length' (0)
  • pnpm format:check
  • pnpm --dir docs build
  • native taxonomy locale, hierarchy, translation race, assignment delta, migration upgrade/retry, manifest reconciliation, consent/update, registry lexicon, plugin CLI, Cloudflare bridge, workerd bridge, runtime-backed Worker Loader, and real Node/workerd isolate tests
  • independent adversarial review and material-fix re-check: passed with no remaining findings

@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 48cbcc2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
@emdash-cms/admin Minor
@emdash-cms/cloudflare Minor
emdash Minor
@emdash-cms/plugin-cli Minor
@emdash-cms/plugin-test Minor
@emdash-cms/plugin-types Minor
@emdash-cms/registry-lexicons Minor
@emdash-cms/sandbox-workerd Minor
@emdash-cms/registry-verification Patch
@emdash-cms/registry-client Patch
@emdash-cms/registry-loader Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
create-emdash Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,302 lines across 49 files. Large PRs are harder to review and more likely to be closed without review.
This PR spans 4 different areas (area/core, area/admin, area/docs, area/cloudflare). Consider breaking it into smaller, focused PRs.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs c9656c3 Sep 19 2026, 03:02 PM

@pkg-pr-new

pkg-pr-new Bot commented Sep 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/admin@3169

@emdash-cms/auth

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth@3169

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth-atproto@3169

@emdash-cms/blocks

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/blocks@3169

@emdash-cms/cloudflare

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/cloudflare@3169

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/contentful-to-portable-text@3169

emdash

npm i https://pkg.pr.new/emdash-cms/emdash@3169

create-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/create-emdash@3169

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/gutenberg-to-portable-text@3169

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-cli@3169

@emdash-cms/plugin-test

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-test@3169

@emdash-cms/plugin-types

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-types@3169

@emdash-cms/registry-client

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-client@3169

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-lexicons@3169

@emdash-cms/registry-loader

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-loader@3169

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-moderation@3169

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@3169

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/sandbox-workerd@3169

@emdash-cms/x402

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/x402@3169

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-moderation@3169

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-atproto@3169

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-audit-log@3169

@emdash-cms/plugin-color

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-color@3169

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-embeds@3169

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-field-kit@3169

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-forms@3169

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-webhook-notifier@3169

commit: 48cbcc2

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://codex-taxonomy-writes.try.emdashcms.com, https://codex-taxonomy-writes-emdash-playground.emdash-cms.workers.dev (commit 48cbcc2)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://5e55b907.try.emdashcms.com, https://5e55b907-emdash-playground.emdash-cms.workers.dev 48cbcc2 2026-09-19T15:20:26.194Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://d0d4f498.try.emdashcms.com, https://d0d4f498-emdash-playground.emdash-cms.workers.dev c9656c3 2026-09-19T15:03:51.526Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://b2d45193.try.emdashcms.com, https://b2d45193-emdash-playground.emdash-cms.workers.dev 50c47ee 2026-09-19T08:07:17.397Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://ea21022f.try.emdashcms.com, https://ea21022f-emdash-playground.emdash-cms.workers.dev 99de35d 2026-09-19T06:56:35.930Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://2de43d67.try.emdashcms.com, https://2de43d67-emdash-playground.emdash-cms.workers.dev 03c0aa5 2026-09-17T16:04:54.188Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://a2a297bb.try.emdashcms.com, https://a2a297bb-emdash-playground.emdash-cms.workers.dev 1adaa84 2026-09-17T13:37:29.871Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://26b94de2.try.emdashcms.com, https://26b94de2-emdash-playground.emdash-cms.workers.dev 2511a90 2026-09-17T12:12:52.227Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://c7f342c7.try.emdashcms.com, https://c7f342c7-emdash-playground.emdash-cms.workers.dev 4858a7e 2026-09-17T11:40:11.294Z Visit the dashboard ↗
  • Build: In progress 🔵

View logs ↗
7ea8cdf 2026-09-17T11:31:25.805Z View logs ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://b2a0ecb3.try.emdashcms.com, https://b2a0ecb3-emdash-playground.emdash-cms.workers.dev 725d983 2026-09-17T11:18:05.201Z Visit the dashboard ↗

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a well-scoped vertical slice for the new taxonomies:write sandboxed capability. The approach fits EmDash’s existing capability/bridge model: core owns the mutation logic, both sandbox runners proxy the same surface, and manifest/consent/registry/docs are kept in sync. The PR does not add any logged-out route and correctly gates writes behind the capability.

I checked the core mutation helpers, the DB migration, the Cloudflare and workerd bridges, the marketplace/admin consent wiring, the test fixtures, and the documentation/changeset. The implementation is mostly solid, but I have two small findings:

  1. In packages/core/src/api/handlers/taxonomies.ts the unique-constraint catch block reports the locale from input.locale instead of the resolved locale, so a case-differing or normalized locale can produce a misleading error message. The fix is to reuse the in-scope locale variable.
  2. The changeset is technically accurate but buries the database migration and a core API behavior change (rejecting parentId on flat taxonomies) inside descriptive validation prose. Per .changeset/README.md, release notes should call out the required migration and any user-facing behavior change so operators know whether and how to upgrade.

The PR description notes the feature-level Discussion checkbox is unchecked; it explains this as part of the maintainer-authored sandboxed capability program. That is a process note, not a code defect, and it does not by itself block a technical review.

Overall: close to ready, pending the two items above.


Findings

  • [suggestion] packages/core/src/api/handlers/taxonomies.ts:1045

    The catch block for a (translation_group, locale) unique violation reports the locale from input.locale ?? getI18nConfig()?.defaultLocale ?? "en". handleTermCreate already resolved the actual effective locale earlier in the function as const locale = resolveConfiguredLocale(input.locale ?? getI18nConfig()?.defaultLocale ?? "en"); that is the locale the database actually attempted to insert. Reporting input.locale can give the caller the wrong locale when the input is omitted or differs in casing/normalization.

    					message: `Term translation already exists for locale '${locale}'`,
    
  • [needs fixing] .changeset/tidy-taxonomy-deltas.md:12-17

    This entry describes the new sandbox capability but buries two user-facing concerns in validation prose: (1) the database migration that adds idx_taxonomies_translation_group_locale_unique, and (2) the core term-creation behavior change that now rejects parentId on flat taxonomies. Since this changeset is copied into package CHANGELOGs, readers need to know they must run migrations and that the core API surface has tightened.

    ---
    "@emdash-cms/admin": minor
    "@emdash-cms/cloudflare": minor
    "emdash": minor
    "@emdash-cms/plugin-cli": minor
    "@emdash-cms/plugin-test": minor
    "@emdash-cms/plugin-types": minor
    "@emdash-cms/registry-lexicons": minor
    "@emdash-cms/sandbox-workerd": minor
    ---
    
    Adds the `taxonomies:write` sandboxed-plugin capability for creating taxonomy terms and applying idempotent assignment deltas through `ctx.taxonomies`.
    
    #### What changed
    
    - `ctx.taxonomies.createTerm()` creates terms and translations; parent terms are allowed only in hierarchical taxonomies.
    - `ctx.taxonomies.addEntryTerms()` and `ctx.taxonomies.removeEntryTerms()` accept term row IDs or translation-group IDs and apply set deltas, so concurrent additions do not replace existing assignments.
    - `taxonomies:write` implies `taxonomies:read`. The runtime validates taxonomy attachment, entry existence, term/taxonomy match, translation identity, and hierarchy before mutating.
    
    #### Migration required
    
    This release ships migration `080_taxonomy_translation_locale_unique`, which adds a unique partial index on `(taxonomies.translation_group, locale)`. Run `pnpm exec emdash migrate` before enabling plugins that use `taxonomies:write`; existing duplicate rows are automatically split into independent translation groups.
    
    Taxonomy definition management, full assignment replacement, term updates, and term deletion remain unavailable to sandboxed plugins.
    

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Sep 17, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Sep 17, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a well-scoped vertical slice for taxonomies:write. The approach fits EmDash’s existing capability/bridge model: core owns the mutation helpers, both sandbox runners proxy the same surface, and manifest/consent/registry/docs are kept in sync. The PR introduces no logged-out routes and gates writes behind the capability.

The two items from the prior review are addressed:

  1. packages/core/src/api/handlers/taxonomies.ts now reports the resolved effectiveLocale in the translation-locale conflict error message, so case-differing or normalized inputs are reflected correctly.
  2. .changeset/tidy-taxonomy-deltas.md now names migration 080_taxonomy_translation_locale_unique explicitly, explains restart safety, and calls out the need for renewed consent.

I re-checked the core mutation helpers, the DB migration, the Cloudflare and workerd bridges, the marketplace/admin consent wiring, the test fixtures, and the docs/changeset. I found one remaining issue: the hierarchy validation is incomplete.

validateParentTerm checks parent existence, taxonomy match, self-parenting, and cycles, but neither handleTermCreate nor handleTermUpdate verifies that the taxonomy is hierarchical before accepting a parentId. This allows a plugin with taxonomies:write to create or update parented terms in a flat taxonomy, which contradicts the taxonomy definition model and the PR’s statement that hierarchy is validated. The fix is to gate parentId on lookup.def.hierarchical in both handlers (or to thread a hierarchical flag through the helper) and to add a reproducing test for the rejection. The changeset/docs should then call out the concrete behavior.

No other blockers. The migration is restart-safe, cache invalidation covers the write paths, and the bridges enforce capability parity.


Findings

  • [needs fixing] packages/core/src/api/handlers/taxonomies.ts:975

    handleTermCreate already resolved the taxonomy definition as lookup.def, but it never checks lookup.def.hierarchical before allowing parentId. handleTermUpdate has the same gap at line 1214. The helper validateParentTerm checks existence, taxonomy match, self-parenting, and cycles, yet a non-hierarchical taxonomy can still receive a parentId, so the PR does not fully deliver its stated hierarchy validation. This lets sandboxed plugins create nested terms in flat taxonomies and leaves the admin term-creation path inconsistent with the taxonomy model.

    Add a flat-taxonomy guard before each validateParentTerm call. In handleTermCreate you can return early right after the definition lookup:

    // Reject parent terms on flat taxonomies before validating the parent reference.
    if (parentId !== undefined && !lookup.def.hierarchical) {
    	return {
    		success: false,
    		error: {
    			code: "VALIDATION_ERROR",
    			message: `Taxonomy '${taxonomyName}' is not hierarchical and cannot have parent terms`,
    		},
    	};
    }
    

    handleTermUpdate currently does not look up the definition, so either look it up once near the top of the handler or pass hierarchical into validateParentTerm. Also add a test in packages/core/tests/integration/plugins/capabilities.test.ts (or a new taxonomy-handler unit test) that asserts a VALIDATION_ERROR when createTerm or handleTermCreate is called with parentId on a taxonomy whose hierarchical flag is 0.

    Once the guard is in place, update .changeset/tidy-taxonomy-deltas.md to call out the concrete behavior: parentId is rejected for non-hierarchical taxonomies.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Sep 17, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Thanks for the re-review. I’m not applying the suggested flat-taxonomy guard because it breaks the existing taxonomy contract rather than completing this capability.

I implemented that exact guard on the earlier head. CI then reproduced four regressions in tests/integration/mcp/taxonomy.test.ts: the established parent/child, maximum-depth, cycle, and child-protected-delete journeys intentionally exercise the built-in flat tags taxonomy. Removing only the blanket hierarchical check restored all 140 MCP and plugin-capability tests.

The write capability still validates every structural invariant in validateParentTerm(): parent existence, same-taxonomy ownership, translation-group self-parenting, ancestor cycles, and the 100-level depth bound. hierarchical remains the existing presentation/tree flag; this PR does not redefine it or change trusted/MCP behavior. The docs and changeset therefore promise hierarchy validation, not a new prohibition on parent IDs for flat definitions.

The independent adversarial re-check also reviewed this exact compatibility decision at 2511a906c and found no remaining issue. All current-head CI checks are green.

~ 🤖 Codex

@ascorbic

Copy link
Copy Markdown
Collaborator Author

Cross-PR blocker: three open PRs all add migration 080

origin/main is at 079_datetime_normalization, and three open PRs each add a different migration numbered 080:

PR migration file sorts
#3182 080_content_translation_locale_unique.ts 1st
#3184 080_redirect_write_guards.ts 2nd
#3169 080_taxonomy_translation_locale_unique.ts 3rd

Individually each is fine. Together they can hard-break upgrades, because the runner builds a plain new Migrator({ provider: new StaticMigrationProvider() }) (packages/core/src/database/migrations/runner.ts:227-229) with no allowUnorderedMigrations, so Kysely enforces strict alphabetical ordering:

throw new Error(`corrupted migrations: expected previously executed migration ${executedMigrations[i]} to be at index ${i} but ${migrations[i].name} was found in its place. New migrations must always have a name that comes alphabetically after the last executed migration.`);

kysely@0.29.2/dist/migration/migrator.js:503, #ensureMigrationsInOrder

Failure scenario: #3184 merges and ships, and a site upgrades and runs 080_redirect_write_guards. #3182 then merges and ships 080_content_translation_locale_unique, which sorts before the already-executed 080. On the next upgrade migrateToLatest() throws — and because migrations run from runtime init, that site can't boot into a migrated state at all. Recovery means hand-editing the migration table on production.

This only bites when the 080s land in different releases. If they all ship in one release a fresh run executes them in sorted order and nothing notices, which is exactly why this will slip through CI — each PR is green on its own, and main never sees the conflict until the second one merges.

Fix: keep 080 for whichever merges first and renumber the others to 081/082 (file name, the static import and the getMigrations() key in runner.ts) before they merge. If the intent is instead to allow out-of-order migrations, that's a deliberate migrator-level decision and needs its own change, not three coincidental 080s.

For this PR: 080_taxonomy_translation_locale_unique sorts last of the three, so it is the safest to renumber — but it must be renumbered to 081/082 if either #3182 or #3184 merges first, which on current ordering is the likely outcome.

Otherwise this PR reviewed clean: capability gating in both bridges, no plugin-supplied identifier reaching SQL, bounded hierarchy walk with no cycle on create, per-locale slug scoping, and runtime-backed tests including a real workerd isolate.

~ 🤖 Codex

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Sep 19, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Confirmed and fixed at 99de35d. I reproduced the strict-ordering failure with Kysely 0.29.2: after applying 080_content_translation_locale_unique and 081_redirect_write_guards, introducing the old 080 taxonomy key fails before pending migrations run with the corrupted-prefix error.

This PR now uses 082_taxonomy_translation_locale_unique everywhere: filename, static import, provider key, migration test, and changeset. The runner integration suite also enforces alphabetically ordered registration with unique, increasing three-digit sequence numbers, so combining two 080 migrations or appending a lower-number registration fails CI.

The coordinated landing order is #3182 as 080, #3184 as 081, then this PR as 082. #3169 must not merge or deploy before those dependencies. Once both land, I will update this branch from main and advance the taxonomy upgrade test baseline from 079 to 081 while keeping the exact assertion that only 082 applies.

Validation passed: focused migration tests (27/27), root build, full package typecheck, type-aware lint (0 diagnostics), docs build, and an independent adversarial review of origin/main...99de35d with no findings.

~ 🤖 Codex (gpt-5.6-sol)

@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Sep 19, 2026
@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Sep 19, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of PR #3169 (codex/taxonomy-writes).

The single remaining finding from the previous review has been addressed: packages/workerd/src/sandbox/bridge-handler.ts no longer labels the taxonomy helper section as read-only, and the taxonomy dispatch cases now accurately cover both reads and the new write paths.

I re-checked the full diff against EmDash's conventions and found no new blocking issues:

  • Capability flow is correct. taxonomies:write implies taxonomies:read through definePlugin normalization, plugin-types round-tripping, the manifest schemas, and the Cloudflare/workerd bridge capability checks.
  • Validation lives in the right layers. Core handleTermCreate/handleTermUpdate now reject parentId for non-hierarchical taxonomies; resolveTaxonomyDelta validates taxonomy-collection attachment, entry existence, term/taxonomy membership, and translation identity; and the bridges enforce the capability gate before invoking mutations.
  • Cache invalidation is covered. Taxonomy repository writes call invalidateTaxonomyObjectCache(), and createTaxonomyAccessWithWrite delegates to the same handlers.
  • The migration is restart-safe. 082_taxonomy_translation_locale_unique processes duplicate rows sequentially, copies assignments before re-pointing a duplicate's translation group, and uses CREATE UNIQUE INDEX IF NOT EXISTS.
  • No logged-out routes or queries are added. All new write paths are plugin/runtime-internal or authenticated admin/mutation surfaces.
  • Docs, changeset, and CLI/skill templates are consistent with the new capability and match the implementation.

No line-anchored findings remain. Approving.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Sep 19, 2026
@ascorbic
ascorbic enabled auto-merge (squash) September 19, 2026 15:28
@ascorbic
ascorbic merged commit 8ad06e9 into main Sep 19, 2026
54 checks passed
@ascorbic
ascorbic deleted the codex/taxonomy-writes branch September 19, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant