feat(labeler): publisher-history context stage (W8.4 slice 1)#2033
Conversation
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
Scope checkThis 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. |
|
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 3f2fe11 | Jul 15 2026, 06:47 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | 3f2fe11 | Jul 15 2026, 06:47 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 3f2fe11 | Jul 15 2026, 06:47 AM |
There was a problem hiding this comment.
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:100doesif (finding.source === "history") continue;before any mapping, andisBlockingFinding(line 52) returnsfalsefor history. Confirmed untouched by this PR and correctly relied upon. The new orchestrator test wires the realanalyzeHistoryand asserts the run finalizespassedwith no history category inissued_labels. validateFindingamendment is correct and bidirectional. History findings must citeHISTORY_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.
analyzeHistorywraps 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 brokendbstub. TheOrchestratorStages.historycontract documents the requirement. - Privacy. Cross-publisher checksum reuse and manual-label identity/existence live only in
privateDetail;title/publicSummaryare non-revealing forshared-artifactandactive-manual-label, with regression-guard assertions.priorReleasesFindingreveals 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 callrecordFindingfor 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 followsidx_{table}_{column}, auto-discovered viareadD1Migrationsand applied by wrangler'smigrations_dir. Correct. - SQL safety. Both new store queries (
getPriorReleaseUrisForDid,getPublishersSharingChecksum) plus the reuse ofgetCurrentSubjectByUri/getActiveLabelStateare fully parameterized (?binds, includingLIMIT ?). No interpolation. - No regressions to existing
source: "history"usages. Thecode-ai-adapter/image-ai-adaptertests supply a model-forgedsource: "history"that the adapter overrides to the real source, so it never reachesvalidateFindingas a history finding; thepolicy-resolvertest passes a history finding straight toresolvePolicyOutcome(bypassesvalidateFinding). Thefindings.test.ts"passes a valid finding for each FindingSource" loop was correctly updated in lockstep to usepublisher-historyfor history. OrchestratorStages.historyrequired-field addition. Every existing construction in the test suite spreads...stubStages, andstubStageswas updated to includehistory, so no literal breaks. Additive in practice.- Conventions.
@emdash-cms/labeleris"private": true→ changeset n/a. No admin-UI strings → i18n n/a. No content-table queries → nolocale-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); |
There was a problem hiding this comment.
[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:
| 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); |
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
There was a problem hiding this comment.
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:100drops history before mapping;isBlockingFindingreturnsfalsefor history. The orchestrator test seeds an explicit prior release underPUBLISHER_DIDso the stage is genuinely non-vacuous, runs the realanalyzeHistoryas thehistorystage, and asserts the run finalizespassedwith no history-category value inissued_labels. validateFindingamendment. History findings validate againstHISTORY_FINDING_CATEGORIESexclusively; non-history againstallowedFindingCategories(block∪warn). Thefindings.test.tsblock covers all four directions; the per-source loop was updated in lockstep (publisher-historyfor history,obfuscated-codeotherwise). Confirmed the history category strings are disjoint from the policy vocabulary.- Best-effort stage.
analyzeHistorywraps all D1 work in try/catch and returns[]on any error; the broken-dbtest 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. TheOrchestratorStages.historycontract documents the requirement. - SQL safety / self-exclusion. Both new store queries plus the reused
getCurrentSubjectByUri/getActiveLabelStateare fully parameterized (?binds, includingLIMIT ?). I traced the cross-publishergetPublishersSharingChecksumJOIN (subjectsonuri+cid, the assessment PK side) and theexcludeDidfilter: the current assessment's own subject hasdid == excludeDidand 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 indexidx_assessments_artifact_checksum(WHERE artifact_checksum IS NOT NULL) covers theartifact_checksum = ?predicate; migration 0006 is the next free number and followsidx_{table}_{column}naming. - Privacy.
shared-artifactandactive-manual-labelkeep specifics (other DIDs, label values) only inprivateDetail;title/publicSummaryare non-revealing, with regression-guard assertions.priorReleasesFindingreveals 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'trecordFindingfor 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-forgedsource: "history"that the adapter overrides to the real source (code-ai-adapter.ts:154setssource: "model"), so it never reachesvalidateFindingas a history finding. Thepolicy-resolvertest bypassesvalidateFindingentirely. OrchestratorStages.historyrequired-field addition. Every test construction spreads...stubStages, which now includeshistory; no literal breaks. Additive in practice.- Conventions.
@emdash-cms/labeleris"private": true→ changeset n/a. No admin-UI strings → i18n n/a. These are the labeler's own D1 tables, not EmDash content tables → nolocale-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); |
There was a problem hiding this comment.
[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.
| expect(produced.some((f) => HISTORY_FINDING_CATEGORIES.has(f.category))).toBe(true); | |
| expect(produced.some((f) => f.category === "publisher-history")).toBe(true); |
…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
There was a problem hiding this comment.
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;
isBlockingFindingreturnsfalsefor history. validateFindingsource branch correctly usesHISTORY_FINDING_CATEGORIESfor history andallowedFindingCategoriesfor everything else, with mutual exclusion covered by tests.- SQL safety: all new store queries and reused helpers use parameterized binds (
?). The partial indexidx_assessments_artifact_checksumfollows theidx_{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:
analyzeHistorycatches 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-tablelocalefilter 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.
e741ce4
into
feat/plugin-registry-labelling-service
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
analyzeHistorystage (history-context.ts) producing at most one finding per input, only when non-empty:getActiveLabelState, filtered!automated && active).validateFindingbranches onsource—source: "history"findings cite a dedicatedHISTORY_FINDING_CATEGORIESset (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.assessments.artifact_checksumfor the cross-DID lookup.getPriorReleaseUrisForDid,getPublishersSharingChecksum) plusgetCurrentSubjectByUri, 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, andisBlockingFindingreturns false for it (untouched here). A new orchestrator test wires the realanalyzeHistoryas thehistorystage, first asserts it genuinely produces a history finding (non-vacuous), then asserts the run finalizespassedand no history-category value appears inissued_labels.Adversary-driven hardening (Opus review before this PR)
[]rather than throwing; a test proves a D1 failure yields no findings instead of erroring the run. TheOrchestratorStages.historycontract documents the best-effort requirement so a future wiring can't reintroduce the hazard.privateDetail; thetitle/publicSummaryare 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
findingstable 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.Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
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.