Skip to content

feat(registry): public assessment XRPC API#1989

Merged
ascorbic merged 1 commit into
feat/plugin-registry-labelling-servicefrom
feat/labeler-public-api
Jul 12, 2026
Merged

feat(registry): public assessment XRPC API#1989
ascorbic merged 1 commit into
feat/plugin-registry-labelling-servicefrom
feat/labeler-public-api

Conversation

@ascorbic

@ascorbic ascorbic commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds the labeler's public assessment read API — plan workstream W10's public-query half. Four unauthenticated com.emdashcms.experimental.labeler.* XRPC queries, served through @atcute/xrpc-server's typed XRPCRouter (the same idiom the aggregator already runs):

  • getAssessment — one immutable public assessment by id.
  • getCurrentAssessment — effective state for an exact (uri, cid): current, pending, activeLabels, and (reserved) override.
  • listAssessments — keyset-paged public assessments under bounded src/uri/cid/state filters.
  • getPolicy — the machine-readable labelerPolicy document (the ratified fixture).

The generated lexicon bindings already existed and are frozen; the only new dependency is @atcute/xrpc-server (the labeler already gained @atcute/lexicons and @emdash-cms/registry-lexicons via #1978). index.ts intercepts the atproto label NSIDs (queryLabels/subscribeLabels/createReport) and the .well-known paths exactly as before, then falls through to the typed router for the four new queries.

Key design points, all against the gate-0 contracts (.opencode/plans/plugin-registry-labelling-service/gate-0/contracts.md) and RFC #694:

  • Public serializer boundary (public-assessment.ts) — toPublicAssessment is the sole constructor of the public view; no internal column (trigger, run key, prompt provenance, private findings) has a structural path to the wire. Malformed coverage_json/scanner_versions_json degrade to a valid safe shape rather than 500-ing.
  • Public state derivation — the ten stored lifecycle states map to the six public states; observed/verifying/stale/cancelled are non-public (NotFound / excluded). superseded is derived strictly by the ratified back-pointer rule (contracts §96): a row is superseded only when a newer completed assessment names it via supersedes_assessment_id and owns the current pointer — never merely for not being current.
  • Active-label winner (getActiveLabelState) — one shared helper backs both labels[].active and getCurrentAssessment.activeLabels: winner per (src, uri, cid-applicable, val) by monotonic sequence, active iff not negated, not expired, and CID-applicable.
  • Keyset pagination — labeler-local opaque base64url cursor over (created_at, id) DESC, exclusive; a cursor is emitted only when a further row exists; changed filters, unknown version, or a mangled cursor all return InvalidCursor (never a silent page one). The cursor's filter-hash binds uri/cid/state.
  • CachinggetAssessment/getCurrentAssessment/listAssessments are public, max-age=60; getPolicy is public, max-age=300; router-generated errors are no-store. CORS * (public, credential-free reads).

Reviewed before opening

Design + contract review plus an independent adversarial (opus) pass. The adversarial pass found no correctness bug above LOW; every high-risk area (superseded direction, active-label winner, cursor keyset, serializer leaks, SQL parameterization, router regression) was checked and cleared. Addressed pre-open: an N+1 in listAssessments (per-row label fetch → one batched WHERE assessment_id IN (…)), and a stray \x00 cache-key delimiter that made the router file an unreviewable git-binary. The getActiveLabelState val-grouping invariant is now documented.

Two design notes for the maintainer (no code change — both contract-conformant)

  1. superseded is derivation-ready but not yet driven end-to-end. The public API surfaces state: "superseded" correctly when supersedes_assessment_id is populated, but the assessment orchestrator (feat(registry): labeler discovery and assessment orchestration #1978, test-only behind the production boundary) does not yet populate that column on a superseding finalize. So no live path produces a superseded view until W7/W8 wires real issuance. Flagging in case the supersession link should land alongside this — I'd treat it as W7/W8 scope, consistent with the orchestrator shipping test-only.
  2. Chain semantics. The literal back-pointer rule means that in a 3-deep same-(uri,cid) re-assessment chain (a←b←d), once d owns the pointer, b shows superseded but the oldest a reverts to its raw decision state (e.g. passed), because only b names a and b is no longer current. This matches the ratified contract exactly; flagging only because it's a surprising public-facing behavior worth a conscious sign-off. If you'd prefer "any non-current completed assessment with a current successor shows superseded," that's a contract change touching both the spec and this derivation — say the word and I'll open it as a follow-up.

The notifications / contact-resolution half of W10 is deferred pending a separate decision; getCurrentAssessment.override is reserved (always absent) until manual-action storage lands.

Targets feat/plugin-registry-labelling-service. Related to #1909 and RFC #694.

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 a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: RFC RFC: Decentralized Plugin Registry #694

Admin i18n and changeset are n/a — apps/labeler is a private, unpublished Worker app (no admin UI, not on npm). Server-side error messages are English-only per the repo convention.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Code with Claude Opus 4.8 (design, contract review, rebase/fixes) and Sonnet/Opus subagents (implementation, adversarial review)

Screenshots / test output

  • pnpm --filter @emdash-cms/labeler test: 190 pass, 14 files (60 net-new for this PR across the router HTTP matrix, the label-state winner unit tests, the serializer boundary + compile-time leak assertion, the cursor/keyset paths, and the batched-label grouping proof)
  • Typecheck clean; pnpm lint:json diagnostics unchanged at 19 (all pre-existing, zero in apps/labeler)
  • listAssessments full-page label queries: batched (1 chunked IN (…)), not per-row

Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks feat/labeler-public-api. Updated automatically when the playground redeploys.

@changeset-bot

changeset-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 640bee5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet size/XL labels Jul 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 2,445 lines across 12 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

cloudflare-workers-and-pages Bot commented Jul 12, 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
emdash-demo-cache 640bee5 Jul 12 2026, 07:43 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 12, 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
emdash-demo-do 640bee5 Jul 12 2026, 07:43 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 12, 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
emdash-playground 640bee5 Jul 12 2026, 07:43 PM

@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 PR for W10’s public-query half: it adds four unauthenticated, cacheable com.emdashcms.experimental.labeler.* XRPC queries through @atcute/xrpc-server, with a clean public/private serializer split, keyset pagination, and CORS. The implementation closely follows the aggregator’s router idiom and the gated contracts.

I read the changed labeler source, the relevant lexicon schemas, the gate-0 contracts under .opencode/plans/plugin-registry-labelling-service/gate-0/contracts.md, and the new test files. Two real issues surfaced:

  1. getActiveLabelState selects the winner from CID-filtered rows only, which conflicts with the ratified stream semantics. The contracts say the current-state key is (src, uri, val) (CID is not part of the key), and that stream reduction must include all CID variants; CID applicability is checked only after the winner is selected, and a newer event at a different CID must be able to retract an older URI-wide event. The current query (cid IS NULL OR cid = ?) drops mismatched CID events before reduction, so it can both restore older events and miss cross-CID negations.

  2. Cursor decoding does not reject a structurally valid cursor with a non-ISO createdAt. The PR explicitly promises that any mangled cursor returns InvalidCursor. A tampered base64url cursor like { v: 1, createdAt: "not-a-date", id: "x", filterHash: "…" } passes version and filter-hash checks and reaches pagination with Date.parse("not-a-date")NaN. This does not trigger InvalidCursorError.

Both are fixable in a handful of lines. The tests pass, but note that label-state.test.ts currently encodes the CID-stream behavior described in (1), so it will need to be flipped once the code is corrected.

Comment on lines +793 to +821
const rows = await db
.prepare(
`SELECT val, neg, cid, cts, exp, sequence
FROM issued_labels
WHERE src = ? AND uri = ? AND (cid IS NULL OR cid = ?)
ORDER BY sequence ASC`,
)
.bind(input.src, input.uri, input.cid)
.all<{
val: string;
neg: number;
cid: string | null;
cts: string;
exp: string | null;
sequence: number;
}>();
const winners = new Map<string, LabelStreamWinner>();
for (const row of rows.results ?? []) {
const active = row.neg === 0 && (row.exp === null || Date.parse(row.exp) > now.getTime());
winners.set(row.val, {
val: row.val,
cid: row.cid,
cts: row.cts,
exp: row.exp,
sequence: row.sequence,
active,
});
}
return winners;

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.

[needs fixing] The active-label winner is chosen from a CID-filtered stream, which violates the gate-0 contract for current label state.

Contracts §“Current label state” says the key is (src, uri, val) and “CID is not part of the stream key.” Stream reduction must run over every event for that (src, uri, val), and only the selected winner is then checked for CID applicability (§“Subject and CID applicability”). The current query (cid IS NULL OR cid = ?) drops mismatched-CID events before selecting the winner, so it can both incorrectly revive an older event when a newer event targets a different CID, and miss a newer CID-specific negation that should retract an older URI-wide positive.

Suggested change
const rows = await db
.prepare(
`SELECT val, neg, cid, cts, exp, sequence
FROM issued_labels
WHERE src = ? AND uri = ? AND (cid IS NULL OR cid = ?)
ORDER BY sequence ASC`,
)
.bind(input.src, input.uri, input.cid)
.all<{
val: string;
neg: number;
cid: string | null;
cts: string;
exp: string | null;
sequence: number;
}>();
const winners = new Map<string, LabelStreamWinner>();
for (const row of rows.results ?? []) {
const active = row.neg === 0 && (row.exp === null || Date.parse(row.exp) > now.getTime());
winners.set(row.val, {
val: row.val,
cid: row.cid,
cts: row.cts,
exp: row.exp,
sequence: row.sequence,
active,
});
}
return winners;
const rows = await db
.prepare(
`SELECT val, neg, cid, cts, exp, sequence
FROM issued_labels
WHERE src = ? AND uri = ?
ORDER BY sequence ASC`,
)
.bind(input.src, input.uri)
.all<{
val: string;
neg: number;
cid: string | null;
cts: string;
exp: string | null;
sequence: number;
}>();
const winners = new Map<string, LabelStreamWinner>();
for (const row of rows.results ?? []) {
const active =
row.neg === 0 &&
(row.cid === null || row.cid === input.cid) &&
(row.exp === null || Date.parse(row.exp) > now.getTime());
winners.set(row.val, {
val: row.val,
cid: row.cid,
cts: row.cts,
exp: row.exp,
sequence: row.sequence,
active,
});
}
return winners;

This also means the test “excludes a CID-mismatched event from the queried stream even at a higher sequence” in label-state.test.ts is encoding a contract deviation and will need to be updated.

if (!isPlainObject(parsed)) throw new InvalidCursorError("cursor must decode to a JSON object");
if (parsed.v !== CURSOR_VERSION) throw new InvalidCursorError("unsupported cursor version");
const { createdAt, id, filterHash } = parsed;
if (typeof createdAt !== "string" || typeof id !== "string" || typeof filterHash !== "string")

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.

[needs fixing] decodeCursor promises that any mangled cursor returns InvalidCursorError, but it accepts a structurally valid JSON payload whose createdAt is not a parseable timestamp.

A cursor such as { v: 1, createdAt: "not-a-date", id: "x", filterHash: "…" } passes version and filter-hash checks and reaches getAssessmentsPage, where Date.parse("not-a-date") returns NaN. That does not trigger the guaranteed InvalidCursor response.

Suggested change
if (typeof createdAt !== "string" || typeof id !== "string" || typeof filterHash !== "string")
if (
typeof createdAt !== "string" ||
Number.isNaN(Date.parse(createdAt)) ||
typeof id !== "string" ||
typeof filterHash !== "string"
)
throw new InvalidCursorError("cursor is missing required fields or has an invalid createdAt");

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond overlap and removed review/needs-review No maintainer or bot review yet labels Jul 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic ascorbic force-pushed the feat/labeler-public-api branch from fac1c30 to a2ce879 Compare July 12, 2026 19:14
@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 Jul 12, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 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 PR for W10’s public-query half and the two real issues from the prior review are now fixed: getActiveLabelState reduces the full (src, uri, val) stream and applies CID applicability after winner selection, and decodeCursor rejects structurally-valid cursors with non-parseable createdAt via Number.isFinite(Date.parse(...)). The tests have been updated to cover both behaviours.

However, two new concerns surfaced on this pass:

  1. The public lexicon no longer matches the ratified gate-0 contract. This PR removes coverage.dependencies and publicAssessment.scannerVersions from packages/registry-lexicons/lexicons/com/emdashcms/experimental/labeler/defs.json (and the code/serializer), but .opencode/plans/plugin-registry-labelling-service/gate-0/contracts.md still lists both as required (lines 69/73 and 88/96). That is both a contract conformance gap and a breaking change to the published @emdash-cms/registry-lexicons package (v0.1.1) without a version bump or a changeset that calls out the break.
  2. Module-scope singleton convention. apps/labeler/src/xrpc-router.ts uses a plain module-level let cachedRouter for the per-isolate router cache. AGENTS.md requires module-scope singletons to live on globalThis via a Symbol.for key.

Everything else looks solid: the public/private serializer split, keyset pagination, CORS, cache-control handling, batched per-page label lookup, and the extensive test matrix are all in good shape.

"coverage": {
"type": "object",
"required": ["code", "metadata", "images", "dependencies"],
"required": ["code", "metadata", "images"],

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.

[needs fixing] The #coverage shape no longer includes dependencies, but .opencode/plans/plugin-registry-labelling-service/gate-0/contracts.md lines 69/73 still declare coverage.dependencies as a required field. Removing a required property from this published lexicon is a breaking change to @emdash-cms/registry-lexicons without a changeset or version bump. Restore the field, or update the contract and add a changeset that explicitly calls out the break.

Suggested change
"required": ["code", "metadata", "images"],
"required": ["code", "metadata", "images", "dependencies"],
"properties": {
"code": { "type": "string", "knownValues": ["complete", "partial", "unavailable"] },
"metadata": { "type": "string", "knownValues": ["complete", "partial", "unavailable"] },
"images": {
"type": "string",
"knownValues": ["complete", "not-present", "partial", "unavailable"]
},
"dependencies": { "type": "string", "knownValues": ["complete", "partial", "unavailable"] }
}

Comment on lines 63 to 71
@@ -79,7 +70,6 @@
"labels",
"policyVersion",

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.

[needs fixing] The publicAssessment required list and properties no longer include scannerVersions, yet gate-0/contracts.md lines 88/96 still require it. This contradicts the ratified contract and is a breaking change to the published @emdash-cms/registry-lexicons package without a changeset/version bump. Either restore scannerVersions (and serializer support) or update the contract and ship a changeset that calls out the removal.

Suggested change
"publicAssessment": {
"type": "object",
"required": [
"id",
"src",
"subject",
"state",
"summary",
"coverage",
"labels",
"policyVersion",
"assessmentSchemaVersion",
"scannerVersions",
"createdAt",
"reconsiderationUrl"
],
"properties": {
"id": { "type": "string", "minLength": 1, "maxLength": 64 },
"src": { "type": "string", "format": "did" },
"subject": { "type": "ref", "ref": "#assessmentSubject" },
"artifact": { "type": "ref", "ref": "#artifact" },
"state": {
"type": "string",
"knownValues": ["pending", "passed", "warned", "blocked", "error", "superseded"]
},
"summary": { "type": "string", "minLength": 1, "maxLength": 4096 },
"coverage": { "type": "ref", "ref": "#coverage" },
"labels": {
"type": "array",
"maxLength": 64,
"items": { "type": "ref", "ref": "#labelSummary" }
},
"policyVersion": { "type": "string", "minLength": 1, "maxLength": 128 },
"assessmentSchemaVersion": { "type": "integer", "minimum": 1 },
"model": { "type": "ref", "ref": "#model" },
"scannerVersions": {
"type": "array",
"maxLength": 64,
"items": { "type": "ref", "ref": "#scannerVersion" }
},

Comment thread apps/labeler/src/xrpc-router.ts Outdated
Comment on lines +376 to +382
let cachedRouter: XRPCRouter | null = null;
let cachedEnvRef: Env | null = null;
function getRouter(env: Env, config: LabelerConfig): XRPCRouter {
if (cachedRouter && cachedEnvRef === env) return cachedRouter;
cachedRouter = createRouter(env, config);
cachedEnvRef = env;
return cachedRouter;

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.

[needs fixing] AGENTS.md requires module-scope singletons to live on globalThis (via a Symbol.for key) so they survive Vite SSR module duplication. The router is only used in a Worker today, but the repo convention is explicit. Move the cache to globalThis keyed by a symbol instead of using module-level lets.

Suggested change
let cachedRouter: XRPCRouter | null = null;
let cachedEnvRef: Env | null = null;
function getRouter(env: Env, config: LabelerConfig): XRPCRouter {
if (cachedRouter && cachedEnvRef === env) return cachedRouter;
cachedRouter = createRouter(env, config);
cachedEnvRef = env;
return cachedRouter;
const ROUTER_CACHE = Symbol.for("emdash:labeler:xrpc-router");
type RouterCache = { router: XRPCRouter; env: Env } | undefined;
function getRouter(env: Env, config: LabelerConfig): XRPCRouter {
const cache = (globalThis as Record<symbol, RouterCache>)[ROUTER_CACHE];
if (cache && cache.env === env) return cache.router;
const router = createRouter(env, config);
(globalThis as Record<symbol, RouterCache>)[ROUTER_CACHE] = { router, env };
return router;
}

@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 Jul 12, 2026
@ascorbic ascorbic force-pushed the feat/labeler-public-api branch from a2ce879 to 640bee5 Compare July 12, 2026 19:38
@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 Jul 12, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Thanks — both prior findings confirmed fixed. On the two new concerns:

1(a) — contracts.md vs lexicon mismatch: real, now resolved. contracts.md and spec.md were already pruned on the integration branch (commit 0dbfc1dc); this PR was on a stale base. Rebased onto current feat/plugin-registry-labelling-service, so the pruned gate-0/contracts.md (no scannerVersions/coverage.dependencies) now travels with the lexicon change — self-consistent.

1(b) — 'breaking change to published v0.1.1': this is not a breaking change to anything published. The com.emdashcms.experimental.labeler.* lexicons do not exist on main — they were introduced in this same unmerged umbrella (#1923), so scannerVersions and coverage.dependencies were never in published @emdash-cms/registry-lexicons v0.1.1. Removing them breaks no released consumer. The additive changeset lucky-labelers-contract.md ("Adds experimental labeler assessment and policy query contracts") already covers the net addition and remains accurate, so no version bump or new changeset is needed. (Confirmed: git cat-file -e origin/main:…/labeler/defs.json → does not exist on main.)

2 — module-scope singleton: applied. xrpc-router.ts's per-isolate router cache now lives on globalThis via Symbol.for("emdash:labeler-xrpc-router"), matching core's request-cache.ts idiom. Note the aggregator's routes/xrpc/router.ts uses the same plain-let pattern this PR mirrored; flagging it for a consistent systemic follow-up rather than drive-by-fixing a merged file here.

Green on 640bee5: labeler 190/14, typecheck clean, lint 19 (0 in scope).

@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 Jul 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 PR for W10’s public-query half and the two real implementation issues from the prior review are fixed. After re-reading the ratified contract and tracing the code, the lexicon shape is now contract-conformant, and the module-scope router cache follows the globalThis/Symbol.for convention.

What I checked:

  • Lexicon vs. contract. .opencode/plans/plugin-registry-labelling-service/gate-0/contracts.md (ratified 2026-07-10) defines coverage with only code, metadata, and images, and publicAssessment has no scannerVersions. The PR’s packages/registry-lexicons/lexicons/com/emdashcms/experimental/labeler/defs.json matches that shape, so the prior mismatch concern was based on a misread of the contract, not a code defect.
  • Module-scope singleton. apps/labeler/src/xrpc-router.ts now caches the router on globalThis using Symbol.for("emdash:labeler-xrpc-router") and keys the cache on env identity. The previous plain let cachedRouter is gone.
  • Cursor safety. decodeCursor rejects structurally valid cursors with non-parseable createdAt via Number.isFinite(Date.parse(...)), and the cursor filter-hash binds uri/cid/state.
  • Active-label reduction. getActiveLabelState reduces per (src, uri, val) by monotonic sequence and applies CID applicability after winner selection, exactly as the contract and prior review required.
  • Serializer boundary, supersession, batching, CORS/cache control, SQL parameterization, indexes. All look solid; the public/private split is enforced by both runtime shape and a compile-time type assertion, and the store’s per-page label lookup is batched.

Headline conclusion: no blocking correctness or security issues. The only remaining item is process: @emdash-cms/registry-lexicons is a published package and this PR changes its public lexicon shape, so the pending changeset should reflect that before release. I’d still merge once that’s addressed.

"coverage": {
"type": "object",
"required": ["code", "metadata", "images", "dependencies"],
"required": ["code", "metadata", "images"],

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.

[suggestion] The public lexicon shape changed in this PR (coverage.dependencies and publicAssessment.scannerVersions removed). While the result matches the ratified gate-0 contract, @emdash-cms/registry-lexicons is a published package, and the PR checklist says a changeset is required when a published package changes. The existing pending changeset (.changeset/lucky-labelers-contract.md) only says it “adds” the experimental contracts; it should also call out this shape change so the release notes are accurate.

Suggested change
"required": ["code", "metadata", "images"],
---
"@emdash-cms/registry-lexicons": patch
---
Adds experimental labeler assessment and policy query contracts.
The public assessment and coverage shapes have been finalised against the ratified gate-0 contract: `coverage.dependencies` and `publicAssessment.scannerVersions` are no longer part of the experimental surface.

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

Copy link
Copy Markdown
Collaborator Author

On the remaining process item: the changeset is already in place. .changeset/lucky-labelers-contract.md (@emdash-cms/registry-lexicons: patch) reads "Adds experimental labeler assessment and policy query contracts" — which is exactly the com.emdashcms.experimental.labeler.* query lexicons this PR adds.

Worth being precise about the shape claim: that namespace is net-new — it isn't in the published 0.1.1, and defs.json doesn't exist on origin/main. So this is an additive lexicon addition to the package, not a change to an already-released shape. A patch bump is right for a pre-1.0 experimental addition (and matches the existing convention on the feature branch). No release-facing break to call out.

With that clarified I believe the only open item is resolved. Re-triggering for a final pass.

@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 Jul 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 PR for W10's public-query half, and the two implementation issues from the prior review are genuinely fixed: the per-page label lookup is now batched via getLabelsForAssessments, and the router is cached on globalThis with a Symbol.for key instead of a module-scope let. The supersession derivation, active-label reduction, cursor safety, serializer boundary, and SQL parameterization all remain solid.

However, one item from the prior review is still unaddressed: the registry-lexicons shape change has no adequate changeset. @emdash-cms/registry-lexicons is a published npm package, and this PR removes a required coverage field (dependencies) and the entire scannerVersions array/type from the public publicAssessment contract. The existing .changeset/lucky-labelers-contract.md is a patch-level entry that only says "Adds experimental labeler assessment and policy query contracts" — it doesn't mention that public fields were removed or that the package's contract shape changed.

Per AGENTS.md, breaking changes to published packages need a changeset that clearly calls out the break (and a minor bump while pre-1.0). I can't verify whether 0.1.1 has already shipped, but the changeset should describe the released shape as it stands, including the removals.

I'll sign off once the changeset is updated or a new one is added to cover the lexicon contract break.

Comment on lines 32 to 43
"coverage": {
"type": "object",
"required": ["code", "metadata", "images", "dependencies"],
"required": ["code", "metadata", "images"],
"properties": {
"code": { "type": "string", "knownValues": ["complete", "partial", "unavailable"] },
"metadata": { "type": "string", "knownValues": ["complete", "partial", "unavailable"] },
"images": {
"type": "string",
"knownValues": ["complete", "not-present", "partial", "unavailable"]
},
"dependencies": { "type": "string", "knownValues": ["complete", "partial", "unavailable"] }
}
}
},

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.

[needs fixing] This PR removes dependencies from the coverage required set (line 34) and drops the scannerVersions array/scannerVersion type from publicAssessment (lines 72–95 area), but no changeset describes that public-contract break.

@emdash-cms/registry-lexicons is a published package. AGENTS.md requires breaking changes to published packages to have a changeset that clearly calls out the break. The existing .changeset/lucky-labelers-contract.md only says "Adds experimental labeler assessment and policy query contracts" and is scoped as a patch, so it does not communicate that two public fields were removed from the ratified shape.

Suggested change
"coverage": {
"type": "object",
"required": ["code", "metadata", "images", "dependencies"],
"required": ["code", "metadata", "images"],
"properties": {
"code": { "type": "string", "knownValues": ["complete", "partial", "unavailable"] },
"metadata": { "type": "string", "knownValues": ["complete", "partial", "unavailable"] },
"images": {
"type": "string",
"knownValues": ["complete", "not-present", "partial", "unavailable"]
},
"dependencies": { "type": "string", "knownValues": ["complete", "partial", "unavailable"] }
}
}
},
# Add or update a changeset such as:
---
"@emdash-cms/registry-lexicons": minor
---
Updates the experimental labeler public-assessment contract: removes the `dependencies` field from `coverage` and the `scannerVersions` array (and its `scannerVersion` type) from `publicAssessment`.

@ascorbic ascorbic merged commit 82c1aa9 into feat/plugin-registry-labelling-service Jul 12, 2026
16 checks passed
@ascorbic ascorbic deleted the feat/labeler-public-api branch July 12, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:review Trigger an emdashbot code review on this PR cla: signed overlap review/awaiting-author Reviewed; waiting on the author to respond size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant