Skip to content

fix(registry): exempt proven first releases from holdback - #3078

Merged
ascorbic merged 3 commits into
mainfrom
codex/1350-first-release-history
Sep 12, 2026
Merged

fix(registry): exempt proven first releases from holdback#3078
ascorbic merged 3 commits into
mainfrom
codex/1350-first-release-history

Conversation

@ascorbic

Copy link
Copy Markdown
Collaborator

What does this PR do?

The optional registry minimum-release-age policy now exempts a package's first release only when the aggregator can prove that it continuously observed the package and retained exactly one release version.

The aggregator records whether a package first arrived through a live Jetstream creation or through an incomplete source such as backfill or an older queue producer. Existing packages start incomplete during migration. A backfilled release or dead-lettered release permanently makes that package's history incomplete. Historical counts include tombstoned releases, so deleting an earlier version cannot turn a later release into a first release.

The package view carries optional history evidence through the lexicon and registry client. The admin uses it to present the policy state, while the install handler enforces the same rule after independently matching the selected release against the publisher's signed PDS records. Missing, malformed, incomplete, or multi-release evidence remains held back. Explicit DID/package exemptions and the opt-in policy default are unchanged.

Part of #1350.

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: n/a — bug fix
  • I have included screenshots below if this PR changes the UI

AI-generated code disclosure

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

Screenshots / test output

Incomplete, backfilled, or unavailable history remains held back:

Registry plugin page with Install disabled and a release-age warning because release history is incomplete

A proven first release remains installable despite the configured holdback:

Registry plugin page with Install enabled for a proven first release

  • Root build and typecheck passed.
  • Full aggregator suite: 297 passed.
  • Registry client: 162 passed.
  • Registry lexicons: 30 passed.
  • Core registry subset: 223 passed.
  • Admin registry browser subset: 54 passed.
  • Documentation build passed.
  • Type-aware lint: 0 diagnostics.
  • Formatting and diff checks passed.

Registry plugin page with Install disabled and a release-age warning because release history is incomplete

Registry plugin page with Install enabled for a proven first release

@changeset-bot

changeset-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e10de24

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

This PR includes changesets to release 24 packages
Name Type
@emdash-cms/registry-lexicons Minor
@emdash-cms/registry-client Minor
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/plugin-cli Patch
@emdash-cms/registry-verification Patch
@emdash-cms/labeler Patch
@emdash-cms/release-action Patch
@emdash-cms/release-service Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/release-verifier Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@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 686 lines across 27 files. Large PRs are harder to review and more likely to be closed without review.

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

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 04f2fd2 Sep 12 2026, 08:14 AM

@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://codex-1350-first-release-history.try.emdashcms.com, https://codex-1350-first-release-history-emdash-playground.emdash-cms.workers.dev (commit e10de24)

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://475aaa20.try.emdashcms.com, https://475aaa20-emdash-playground.emdash-cms.workers.dev e10de24 2026-09-12T09:54:46.780Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://eecea4f4.try.emdashcms.com, https://eecea4f4-emdash-playground.emdash-cms.workers.dev dc8b022 2026-09-12T09:22:00.394Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://6b9a30f2.try.emdashcms.com, https://6b9a30f2-emdash-playground.emdash-cms.workers.dev 04f2fd2 2026-09-12T08:14:45.966Z Visit the dashboard ↗

@pkg-pr-new

pkg-pr-new Bot commented Sep 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

npm i https://pkg.pr.new/emdash@3078

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-moderation

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: e10de24

@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 substantial but well-scoped change: it tracks per-package release-history completeness in the aggregator, exposes the evidence through the registry lexicon/client, and lets the admin UI and core install handler exempt a proven first release from the optional minimum-release-age holdback. The design is fail-closed (backfill, dead-letter, missing or multi-release evidence stays held back; tombstones still count), which matches the intent in #1350 and fits EmDash's registry trust model.

What I checked:

  • SQL safety in apps/aggregator/src/routes/xrpc/listing-query.ts — the new columns are a constant string, not interpolated with user input.
  • Data integrity/transaction boundaries in records-consumer.ts — history writes are batched with package/release writes, D1's transactional batch keeps them consistent; ON CONFLICT DO NOTHING and the explicit UPDATE preserve existing incomplete history.
  • Lexicon and client validation — new fields are optional in the schema and validated by atcute; isProvenFirstRelease rejects malformed evidence.
  • Admin and core install gates — both consume the evidence and fail closed when it's missing/malformed.
  • Tests and changeset — broad coverage across aggregator, registry-client, admin, and core; changeset is accurate and user-facing.

Headline conclusion: the implementation is mostly solid, but there is one real bug in the admin UI. RegistryPluginDetail was updated to pass the full pkg view to policyOk and to the release-picker badge, but the defaultVersion picker still evaluates releasePassesPolicy with a partial { did, slug } object that lacks historicalReleaseCount/releaseHistoryComplete. For a package whose only installable release is a proven first release but whose newest listed release is still inside the holdback, the picker defaults to the too-new release (disabling Install) instead of defaulting to the exempt first release. This undermines the feature's UX for multi-release packages and should be fixed before merge, together with a regression test.


Findings

  • [needs fixing] packages/admin/src/components/RegistryPluginDetail.tsx:204

    The defaultVersion picker still passes { did: pkg.did, slug } to releasePassesPolicy, but the function now needs the full historicalReleaseCount/releaseHistoryComplete evidence to recognize a proven first-release exemption. The other two call sites in this file (policyOk and the per-release (too new) badge) were already updated to pass pkg; this one was missed.

    For a package with a proven first release and a newer release still inside the holdback window, find() will not see the first release as passing and will fall back to releases[0] (the newest, too-new release). The user then lands on a disabled Install button and a "too new" banner even though an installable first release exists. This is the same mis-evaluation the PR fixes for the picker badge and the install button.

    Fix by passing the full pkg view here too, then add a component test with two releases (a newer too-new release and an older proven first release) asserting that the picker defaults to the first release and the Install button is enabled.

    		const passes = releases.find((r) =>
    			releasePassesPolicy(r, pkg, config.policy),
    		);
    

@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 12, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Thanks — I traced this through the component and the history invariant, and I don't think the suggested change is safe.

A proven first release requires releaseHistoryComplete === true and historicalReleaseCount === 1. In that valid state there can only be one release: defaultVersion falls back to that same sole release, then policyOk evaluates it with the full package evidence and enables Install.

With two releases, historicalReleaseCount must be at least 2, so the package is established and neither release receives the first-release exemption. Passing the full package into defaultVersion under an inconsistent historicalReleaseCount: 1 plus two-release response would instead exempt both releases and select the newer too-new release, weakening the fail-closed behavior.

A two-release regression that is red before and green after the suggested one-line change therefore cannot represent a valid registry state. I have left the code unchanged and would appreciate reconsideration of this finding.

~ 🤖 Codex

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 12, 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 the first-release holdback exemption. The aggregator, lexicon, registry-client, and core install-gate changes are still consistent with the fail-closed design described in #1350. However, the one bug reported in the previous review is still present in the PR head: RegistryPluginDetail was updated to pass the full pkg view to policyOk and to the per-release picker badge, but the defaultVersion picker still evaluates releasePassesPolicy with a partial { did: pkg.did, slug } object. Because that object lacks historicalReleaseCount and releaseHistoryComplete, a package whose newest release is inside the holdback window but whose older proven first release is exempt will still default to the too-new release — disabling Install and showing the “too new” banner even though an installable first release exists. The newly added component tests only exercise single-release packages, so they cannot catch this regression.

What I checked:

  • packages/admin/src/components/RegistryPluginDetail.tsx — all three releasePassesPolicy call sites; two were fixed, one was not.
  • packages/admin/tests/components/RegistryPluginDetail.test.tsx — added minimum-release-age tests cover only single-release fixtures.
  • packages/admin/src/lib/api/registry.tsreleasePassesPolicy correctly consumes the history evidence via isProvenFirstRelease.
  • apps/aggregator/src/routes/xrpc/listing-query.ts — scalar subqueries in an existing SELECT; no extra round-trip and no user input interpolation.
  • apps/aggregator/src/records-consumer.ts and migration — fail-closed history tracking is unchanged and transactionally batched.
  • packages/core/src/api/handlers/registry.ts — install gate independently applies the same first-release exemption.
  • Changeset and docs — still accurate and user-facing.

Headline conclusion: the implementation is solid except for the unresolved admin picker default. Passing the full pkg view at line 204 and adding a multi-release regression test would finish the PR.


Findings

  • [needs fixing] packages/admin/src/components/RegistryPluginDetail.tsx:204

    The defaultVersion picker still passes a partial { did: pkg.did, slug } package view to releasePassesPolicy. Without historicalReleaseCount and releaseHistoryComplete, isProvenFirstRelease always fails closed here, so the picker behaves as if no release is exempt.

    For a package with a newer release still inside the holdback and an older proven first release, find() will not see the first release as passing and falls back to releases[0] (the newest, too-new release). The user lands on a disabled Install button and a “too new” banner even though a first release is installable. The other two call sites in this file (policyOk at line 360 and the (too new) badge at line 580) already pass the full pkg; this one was missed.

    The newly added component tests use a single release each, so they cannot exercise this regression. Add a test with two releases — a newer too-new release and an older proven first release — asserting that the picker defaults to the first release and the Install button is enabled.

    			releasePassesPolicy(r, pkg, config.policy),
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 12, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 12, 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 12, 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.

This is the right fix for #1350 and the prior review's admin picker bug is resolved in the current head: RegistryPluginDetail's defaultVersion picker now passes the full pkg view to releasePassesPolicy, matching the other two call sites.

What I checked:

  • packages/admin/src/components/RegistryPluginDetail.tsx — all three releasePassesPolicy call sites now use the full package view with historicalReleaseCount/releaseHistoryComplete.
  • packages/admin/src/lib/api/registry.tsreleasePassesPolicy correctly applies isProvenFirstRelease after the explicit exemption and before the age check.
  • packages/core/src/api/handlers/registry.ts — install gate independently enforces the same fail-closed exemption via isProvenFirstRelease(packageView); update flow is unchanged (it didn't apply the holdback before this PR either).
  • apps/aggregator/src/records-consumer.ts and 0006_release_history.sql — history tracking is inserted on every profile ingest, marked incomplete on any non-jetstream release or dead-letter, and never upgraded from incomplete.
  • apps/aggregator/src/routes/xrpc/listing-query.ts — history evidence is carried as correlated scalar subqueries in the existing getPackage/resolvePackage selects, adding no extra round-trip.
  • Lexicon, registry-client discovery, docs, and changeset changes are consistent with the design.

Headline conclusion: the implementation is sound and the previously reported picker bug is fixed. I have two non-blocking suggestions: update a stale docstring in releasePassesPolicy and add a multi-release fixture to the admin component tests so the picker-default behavior is covered when the package is no longer a single-release first release.


Findings

  • [suggestion] packages/admin/src/lib/api/registry.ts:236-245

    The releasePassesPolicy docstring no longer matches the implementation. It now also exempts a proven first release via isProvenFirstRelease, not just the minimum-release-age window and minimumReleaseAgeExclude.

    /**
     * Returns whether a release should be considered installable given the
     * configured policy. Implements (in order):
     *
     *   1. The `minimumReleaseAgeExclude` allowlist.
     *   2. A fail-closed first-release exemption when the aggregator proves
     *      exactly one complete observed release.
     *   3. The minimum-release-age holdback described in RFC 0001's
     *      "Pre-label gap and launch tempo" section.
     *
     * Returns `false` (release blocked) when the policy is configured but
     * the release is missing a valid `indexedAt` -- we fail closed rather
     * than silently letting unbounded-age releases through.
     */
    
  • [suggestion] packages/admin/tests/components/RegistryPluginDetail.test.tsx:324-368

    The new minimum-release-age component tests only exercise single-release packages. Add a multi-release fixture to cover the picker-default path when the package is no longer a proven first release (e.g., a newer too-new release alongside an older release). This guards the default-version logic against accidentally regressing back to the newest blocked release when history evidence says the package has more than one retained release.

    	it("defaults to an older age-qualified release when the newest release is too new", async () => {
    		setup(makePackage({ historicalReleaseCount: 2, releaseHistoryComplete: true }), [
    			makeRelease({ version: "2.0.0", indexedAt: new Date().toISOString() }),
    			makeRelease({ version: "1.0.0", indexedAt: "2025-03-01T00:00:00Z" }),
    		]);
    		const screen = await render(
    			<Wrapper>
    				<RegistryPluginDetail
    					pluginId="acme.dev/myplugin"
    					config={{
    						...CONFIG,
    						policy: { minimumReleaseAgeSeconds: 48 * 60 * 60 },
    					}}
    				/>
    			</Wrapper>,
    		);
    
    		await expect.element(screen.getByRole("button", { name: "Install" })).toBeEnabled();
    		await expect.element(screen.getByText("1.0.0")).toBeInTheDocument();
    		expect(screen.getByText("Release is too new to install").query()).toBeNull();
    	});
    

    (Assumes makeRelease accepts a version override; the test can be placed inside the existing describe("RegistryPluginDetail minimum release age" block.)

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 12, 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 12, 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 12, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 12, 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.

This is the right fix for #1350 and the re-review resolves both findings from the prior pass.

What changed since the last review:

  • packages/admin/src/lib/api/registry.ts — the releasePassesPolicy docstring now correctly lists the three gates in order: minimumReleaseAgeExclude allowlist, proven-first-release exemption, then the age holdback.
  • packages/admin/tests/components/RegistryPluginDetail.test.tsx — a multi-release fixture now covers the default-version picker path, proving the detail page falls back to an older age-qualified release when history evidence rules out a single-release exemption.

What I checked:

  • RegistryPluginDetail now passes the full pkg view (including historicalReleaseCount / releaseHistoryComplete) to all three releasePassesPolicy call sites.
  • releasePassesPolicy and the core install gate both apply isProvenFirstRelease fail-closed after explicit exemptions and before the age check; isProvenFirstRelease requires both releaseHistoryComplete === true and historicalReleaseCount === 1.
  • The aggregator migration creates package_release_history, seeds existing packages as incomplete/unknown, and is idempotent via INSERT OR IGNORE.
  • ingestPackageProfile marks live profile creations as complete and never upgrades incomplete history (ON CONFLICT DO NOTHING); ingestPackageRelease and dead-letter handling mark history incomplete for any non-Jetstream or dead-lettered release.
  • The read API carries history evidence as two correlated scalar subqueries in the existing getPackage/resolvePackage selects, adding no extra round trip.
  • The lexicon, generated types, registry-client validation, admin tests, core conformance tests, aggregator consumer tests, and docs all line up with the fail-closed design.
  • No new user-facing strings, no raw SQL interpolation, no extra logged-out queries, and the changeset is proportional and accurate.

Headline: all previously raised suggestions are addressed and the implementation is sound. LGTM.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 12, 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 12, 2026
@ascorbic
ascorbic merged commit befce6d into main Sep 12, 2026
57 checks passed
@ascorbic
ascorbic deleted the codex/1350-first-release-history branch September 12, 2026 10:45
@emdashbot emdashbot Bot mentioned this pull request Sep 12, 2026
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