Skip to content

feat(labeler): publisher-history context stage (W8.4 slice 1)#2033

Merged
ascorbic merged 3 commits into
feat/plugin-registry-labelling-servicefrom
feat/labeler-publisher-history
Jul 15, 2026
Merged

feat(labeler): publisher-history context stage (W8.4 slice 1)#2033
ascorbic merged 3 commits into
feat/plugin-registry-labelling-servicefrom
feat/labeler-publisher-history

Conversation

@ascorbic

@ascorbic ascorbic commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Implements W8.4 slice 1 (publisher-history context) for the labeler assessment pipeline. Adds a history-source stage that produces bounded factual context from the labeler's own D1, and amends the W8.1 finding contract to carry it honestly. Part of the plugin-registry labelling service (umbrella #1909, RFC #694); decisions ratified in the plan (docs(labeler-plan): ratify W8.4 decisions).

What it adds

  • analyzeHistory stage (history-context.ts) producing at most one finding per input, only when non-empty:
    • prior releases from the publishing DID,
    • the same artifact checksum submitted under other DIDs (global, cross-publisher — the sharper republished/stolen-artifact signal per D2),
    • active manual labels on the subject (reusing getActiveLabelState, filtered !automated && active).
  • W8.1 contract amendment (D4): validateFinding branches on sourcesource: "history" findings cite a dedicated HISTORY_FINDING_CATEGORIES set (publisher-history, shared-artifact, active-manual-label), disjoint from the automated-block ∪ warning vocabulary. Non-history validation is unchanged. The two category sets are mutually exclusive per source in both directions.
  • Migration 0006: partial index on assessments.artifact_checksum for the cross-DID lookup.
  • Two store queries (getPriorReleaseUrisForDid, getPublishersSharingChecksum) plus getCurrentSubjectByUri, all parameterized and bounded.

The invariant that matters: history never becomes a label

Structural, not a new guard: the resolver already drops every source: "history" finding before any category→label mapping, and isBlockingFinding returns false for it (untouched here). A new orchestrator test wires the real analyzeHistory as the history stage, first asserts it genuinely produces a history finding (non-vacuous), then asserts the run finalizes passed and no history-category value appears in issued_labels.

Adversary-driven hardening (Opus review before this PR)

  • Best-effort stage (was: could fail the whole run). History runs last and is context-only, so a failure to gather it must never fail the assessment run and discard every other stage's findings (including real blocks). The stage now swallows its own errors and returns [] rather than throwing; a test proves a D1 failure yields no findings instead of erroring the run. The OrchestratorStages.history contract documents the best-effort requirement so a future wiring can't reintroduce the hazard.
  • No sensitive data in public-facing fields. Cross-publisher checksum reuse and active manual-label identities/existence (including redactions) are deanonymization / hidden-moderation signals, so they live only in privateDetail; the title/publicSummary are non-revealing, with regression-guard assertions. The plan records that slice 2's surfacing must additionally render history findings operator-only and exclude them from any public serializer.

Scope / honesty notes

  • Nothing is surfaced yet. The orchestrator does not persist stage findings to the findings table in its run path today (no stage does — a pre-existing boundary; the orchestrator is test-only behind the production boundary until W7/W8). So in slice 1 history findings compute and are proven never to label, but the "recommend operator review" surfacing is slice 2 (console read-time, D5) — this PR delivers the production side of the stage and the contract, not an operator-visible surface.
  • Deferred: the two aggregator-sourced inputs (recent handle/profile changes, verification state) live only in the aggregator's D1, which the labeler has no binding to reach; they are slice 3, gated on a ratified read path (D1).

Type of change

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)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (implementation and review; an Opus adversary pass drove the best-effort and public-face fixes before opening)

Screenshots / test output

Test Files  29 passed (29)
     Tests  559 passed (559)

https://claude.ai/code/session_014rikrGMh25h1rJczUQyTMM


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-publisher-history. Updated automatically when the playground redeploys.

Add a `history`-source assessment stage that produces bounded factual
context from the labeler's own D1: prior releases from the publishing
DID, the same artifact checksum submitted under other DIDs (global,
cross-publisher), and active manual labels on the subject.

History findings are operator-only context, never labels. The resolver
already drops every `source: "history"` finding before any category to
label mapping, and this amends the W8.1 finding contract (D4) so history
findings cite a dedicated `HISTORY_FINDING_CATEGORIES` set, disjoint from
the automated-block union warning vocabulary; the non-history validation
path is unchanged. An orchestrator test proves a history finding flows
through validation and resolution but never becomes an issued label.

The stage is best-effort: because it can never affect labels, a failure
to gather context must never fail the run and discard the other stages'
findings, so it swallows its own errors and returns no findings. Sensitive
specifics (cross-publisher checksum reuse, active manual-label identities)
stay in `privateDetail`, never in the public-facing title/summary.

Adds migration 0006 (partial index on `assessments.artifact_checksum`)
for the cross-DID checksum lookup. The two aggregator-sourced inputs
(handle/profile changes, verification state) are deferred to a later
slice gated on a ratified read path.

Claude-Session: https://claude.ai/code/session_014rikrGMh25h1rJczUQyTMM
@github-actions github-actions Bot added size/XL review/needs-review No maintainer or bot review yet labels Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 650 lines across 9 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.

@changeset-bot

changeset-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 3f2fe11

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 14, 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 3f2fe11 Jul 15 2026, 06:47 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 14, 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 3f2fe11 Jul 15 2026, 06:47 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 14, 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 3f2fe11 Jul 15 2026, 06:47 AM

@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.

Approach

This is W8.4 slice 1 of the plugin-registry labelling service: a history-source context stage that produces bounded, factual findings from the labeler's own D1, plus the W8.1 finding-contract amendment that lets those findings cite a dedicated category vocabulary. The approach is the right one and fits EmDash's architecture: history is context, never a label, and that invariant is enforced structurally (not by a new guard) — the resolver already drops source: "history" before any category→label mapping and isBlockingFinding returns false for it. The contract amendment (validateFinding branching on source) is a clean, narrowly-scoped change that keeps the two category sets mutually exclusive in both directions.

What I verified

  • "History never becomes a label" is structural. policy-resolver.ts:100 does if (finding.source === "history") continue; before any mapping, and isBlockingFinding (line 52) returns false for history. Confirmed untouched by this PR and correctly relied upon. The new orchestrator test wires the real analyzeHistory and asserts the run finalizes passed with no history category in issued_labels.
  • validateFinding amendment is correct and bidirectional. History findings must cite HISTORY_FINDING_CATEGORIES; non-history findings must still cite block∪warn. Tests cover all four directions (accept history category, reject history→block/warn, reject non-history→history category, non-history unchanged). The history category strings are disjoint from the policy vocabulary (asserted in the test).
  • Best-effort stage. analyzeHistory wraps all D1 work in try/catch and returns [] on any error, so it can never abort the run and discard other stages' decision-relevant findings. Tested with a broken db stub. The OrchestratorStages.history contract documents the requirement.
  • Privacy. Cross-publisher checksum reuse and manual-label identity/existence live only in privateDetail; title/publicSummary are non-revealing for shared-artifact and active-manual-label, with regression-guard assertions. priorReleasesFinding reveals only an aggregate count about the subject's own publisher (not a cross-publisher correlation), which is appropriate. Nothing is persisted/surfaced in slice 1 (the orchestrator doesn't call recordFinding for any stage — a pre-existing boundary the PR is honest about), so there's no public leak today; slice 2's operator-only surfacing is correctly deferred.
  • Migration 0006. Partial index idx_assessments_artifact_checksum (WHERE artifact_checksum IS NOT NULL), naming follows idx_{table}_{column}, auto-discovered via readD1Migrations and applied by wrangler's migrations_dir. Correct.
  • SQL safety. Both new store queries (getPriorReleaseUrisForDid, getPublishersSharingChecksum) plus the reuse of getCurrentSubjectByUri/getActiveLabelState are fully parameterized (? binds, including LIMIT ?). No interpolation.
  • No regressions to existing source: "history" usages. The code-ai-adapter/image-ai-adapter tests supply a model-forged source: "history" that the adapter overrides to the real source, so it never reaches validateFinding as a history finding; the policy-resolver test passes a history finding straight to resolvePolicyOutcome (bypasses validateFinding). The findings.test.ts "passes a valid finding for each FindingSource" loop was correctly updated in lockstep to use publisher-history for history.
  • OrchestratorStages.history required-field addition. Every existing construction in the test suite spreads ...stubStages, and stubStages was updated to include history, so no literal breaks. Additive in practice.
  • Conventions. @emdash-cms/labeler is "private": true → changeset n/a. No admin-UI strings → i18n n/a. No content-table queries → no locale-filter concern.

Conclusion

Headline: this is a clean, well-scoped slice. The implementation matches the stated goal, the structural invariants hold, the adversary-driven hardening (best-effort + private-only fields) is real and tested, and I found no logic, security, or data-integrity bugs. The only thing worth surfacing is a test-robustness nit (below).

// — the invariant below is non-vacuous.
const assessment = await getAssessment(testEnv.DB, run.id);
const produced = await analyzeHistory(testEnv.DB, assessment!, { src: LABELER_DID });
expect(produced.some((f) => HISTORY_FINDING_CATEGORIES.has(f.category))).toBe(true);

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 non-vacuousness assertion expect(produced.some(...)).toBe(true) relies on cross-test shared state: pendingRun creates exactly one subject under PUBLISHER_DID (the run's own, which getPriorReleaseUrisForDid excludes via excludeUri), so analyzeHistory only returns a publisher-history finding because earlier it blocks in the same file have already seeded other subjects under the shared PUBLISHER_DID. The comment acknowledges this, but it means the test fails if run in isolation (e.g. vitest -t "history stage never auto-labels") — there are no prior releases and no checksum/labels, so produced is [].

Seeding an explicit prior subject for this DID inside the test would make the non-vacuousness self-contained:

Suggested change
expect(produced.some((f) => HISTORY_FINDING_CATEGORIES.has(f.category))).toBe(true);
// Seed an explicit prior release under the same DID so the stage genuinely
// produces a publisher-history finding without relying on other tests' state.
await createSubject(testEnv.DB, {
uri: releaseUri("history-invariant-prior"),
cid: await cid("history-invariant-prior"),
did: PUBLISHER_DID,
collection: "com.emdashcms.experimental.package.release",
rkey: "history-invariant-prior:1.0.0",
});
const assessment = await getAssessment(testEnv.DB, run.id);
const produced = await analyzeHistory(testEnv.DB, assessment!, { src: LABELER_DID });
expect(produced.some((f) => HISTORY_FINDING_CATEGORIES.has(f.category))).toBe(true);

@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 Jul 14, 2026
The non-vacuousness assertion relied on prior releases seeded by earlier
tests under the shared DID, so it was vacuous when run in isolation. Seed
an explicit prior release inside the test.

Claude-Session: https://claude.ai/code/session_014rikrGMh25h1rJczUQyTMM
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 14, 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 Jul 14, 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.

Approach

Re-review of W8.4 slice 1 (publisher-history context stage). The approach remains the right one and fits EmDash's architecture: history is context, never a label, enforced structurally — policy-resolver.ts does if (finding.source === "history") continue; before any category→label mapping and isBlockingFinding returns false for history (both untouched by this PR). The validateFinding amendment that branches allowedCategories on source === "history" is a clean, bidirectional change keeping the two category vocabularies mutually exclusive.

What I re-verified

  • "History never becomes a label" is structural. Confirmed policy-resolver.ts:100 drops history before mapping; isBlockingFinding returns false for history. The orchestrator test seeds an explicit prior release under PUBLISHER_DID so the stage is genuinely non-vacuous, runs the real analyzeHistory as the history stage, and asserts the run finalizes passed with no history-category value in issued_labels.
  • validateFinding amendment. History findings validate against HISTORY_FINDING_CATEGORIES exclusively; non-history against allowedFindingCategories (block∪warn). The findings.test.ts block covers all four directions; the per-source loop was updated in lockstep (publisher-history for history, obfuscated-code otherwise). Confirmed the history category strings are disjoint from the policy vocabulary.
  • Best-effort stage. analyzeHistory wraps all D1 work in try/catch and returns [] on any error; the broken-db test proves a D1 failure yields no findings rather than erroring the run. Since history runs last, this prevents a history failure from discarding other stages' decision-relevant findings. The OrchestratorStages.history contract documents the requirement.
  • SQL safety / self-exclusion. Both new store queries plus the reused getCurrentSubjectByUri/getActiveLabelState are fully parameterized (? binds, including LIMIT ?). I traced the cross-publisher getPublishersSharingChecksum JOIN (subjects on uri+cid, the assessment PK side) and the excludeDid filter: the current assessment's own subject has did == excludeDid and is filtered, as are same-DID re-releases under other URIs — so no self/false-positive flag; only genuinely cross-DID reuse is reported. The partial index idx_assessments_artifact_checksum (WHERE artifact_checksum IS NOT NULL) covers the artifact_checksum = ? predicate; migration 0006 is the next free number and follows idx_{table}_{column} naming.
  • Privacy. shared-artifact and active-manual-label keep specifics (other DIDs, label values) only in privateDetail; title/publicSummary are non-revealing, with regression-guard assertions. priorReleasesFinding reveals only an aggregate count about the subject's own publisher — public ATProto records anyway, so not a deanonymization signal. Nothing is persisted/surfaced in slice 1 (the orchestrator doesn't recordFinding for any stage — a pre-existing boundary the PR is honest about), so no public leak path exists today.
  • No regression to existing source: "history" usages. The code/image adapter tests supply a model-forged source: "history" that the adapter overrides to the real source (code-ai-adapter.ts:154 sets source: "model"), so it never reaches validateFinding as a history finding. The policy-resolver test bypasses validateFinding entirely.
  • OrchestratorStages.history required-field addition. Every test construction spreads ...stubStages, which now includes history; no literal breaks. Additive in practice.
  • Conventions. @emdash-cms/labeler is "private": true → changeset n/a. No admin-UI strings → i18n n/a. These are the labeler's own D1 tables, not EmDash content tables → no locale-filter concern.

Conclusion

Headline: the re-review confirms the prior conclusion — this is a clean, well-scoped slice with no logic, security, or data-integrity bugs; the structural invariants hold and the adversary-driven hardening is real and tested. I found nothing new. The only thing worth surfacing is the same minor test-robustness nit (below): the orchestrator test's non-vacuousness assertion checks for any history category rather than the publisher-history finding its own comment says it seeds, so a future change that broke the prior-release path but left another history finding firing would still pass.

});
const assessment = await getAssessment(testEnv.DB, run.id);
const produced = await analyzeHistory(testEnv.DB, assessment!, { src: LABELER_DID });
expect(produced.some((f) => HISTORY_FINDING_CATEGORIES.has(f.category))).toBe(true);

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 comment above this call says the seeded prior release makes the stage "genuinely produces a publisher-history finding without relying on other tests' state," but the assertion only checks that some history-category finding was produced (HISTORY_FINDING_CATEGORIES.has(f.category)), not specifically publisher-history. If a future change broke the prior-release path but left shared-artifact or active-manual-label firing (e.g. from leftover state or a different code path), this non-vacuousness guard would still pass while proving nothing about the path the test claims to exercise. Assert the specific category the seeded data is meant to trigger.

Suggested change
expect(produced.some((f) => HISTORY_FINDING_CATEGORIES.has(f.category))).toBe(true);
expect(produced.some((f) => f.category === "publisher-history")).toBe(true);

@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 14, 2026
…nvariant test

Check for the `publisher-history` category the seeded prior release
produces rather than any history category, so a regression in the
prior-release path can't pass on an unrelated history finding.

Claude-Session: https://claude.ai/code/session_014rikrGMh25h1rJczUQyTMM
@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 15, 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 Jul 15, 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 W8.4 slice 1 (publisher-history context stage).

The approach remains sound and well-scoped: it adds a best-effort history stage using the labeler's own D1, amends the finding contract with a dedicated history-only category set, and relies on the pre-existing structural invariant that policy-resolver.ts drops source: "history" findings before any category→label mapping. I re-verified:

  • Structural "history never labels" invariant still holds; isBlockingFinding returns false for history.
  • validateFinding source branch correctly uses HISTORY_FINDING_CATEGORIES for history and allowedFindingCategories for everything else, with mutual exclusion covered by tests.
  • SQL safety: all new store queries and reused helpers use parameterized binds (?). The partial index idx_assessments_artifact_checksum follows the idx_{table}_{column} convention and matches the lookup predicate.
  • Privacy: sensitive cross-publisher DIDs and manual-label values stay in privateDetail; public-facing fields are non-revealing.
  • Best-effort stage: analyzeHistory catches its own errors and returns []; the broken-DB test proves it doesn't fail the run.
  • Conventions: no admin UI strings (i18n n/a), labeler package is "private": true (changeset n/a), queries are against labeler-owned D1 tables (no content-table locale filter concern).

Prior review nit fixed: the orchestrator history-invariant test now asserts produced.some((f) => f.category === "publisher-history") instead of "any history category", closing the vacuous-pass hole identified in the previous review.

I found no new logic, security, data-integrity, or convention issues. LGTM.

@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 Jul 15, 2026
@ascorbic ascorbic merged commit e741ce4 into feat/plugin-registry-labelling-service Jul 15, 2026
11 checks passed
@ascorbic ascorbic deleted the feat/labeler-publisher-history branch July 15, 2026 07:05
@ascorbic ascorbic mentioned this pull request Jul 15, 2026
81 tasks
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 review/approved Approved; no new commits since size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant