diff --git a/src/signals/extension-contributor-context.ts b/src/signals/extension-contributor-context.ts index 53e7e3a1fd..be2bbbf51e 100644 --- a/src/signals/extension-contributor-context.ts +++ b/src/signals/extension-contributor-context.ts @@ -21,8 +21,14 @@ export function contributorReadinessBand(total: number): ContributorReadinessBan // The upstream builders are already contributor-facing, but every string is re-checked here and any // forbidden private term (reward/wallet/key material/raw trust score/etc.) is redacted rather than // leaked. Kept local (no import) so this module stays cycle-free and the API never 500s on a stray term. +// The bare `cohort`/`ranking`/`miner-originated`/`human-originated`/`reviewability` alternatives (and the +// `[-_\s]?` separator on the originated pair) mirror src/signals/redaction.ts's canonical PUBLIC_UNSAFE_TERMS +// so this overlay stops leaking economic-identity terms that surface drifted away from (#5840). The compound +// `reviewability` terms stay ordered before the bare word so "reviewability internals"/"private reviewability" +// still match as a whole. Kept hand-synced (no import) so this module stays cycle-free; a drift-guard test +// (extension-contributor-context.test.ts) fails if these diverge from PUBLIC_UNSAFE_TERMS again. const FORBIDDEN_EXTENSION_TERMS = - /\b(?:rewards?|payouts?|farming|wallets?|hotkeys?|coldkeys?|seed[-\s]?phrases?|mnemonics?|private[-\s]?keys?|raw[-\s]?trust(?:[-\s]?scores?)?|trust[-\s]?scores?|score[-\s]?(?:estimate|preview|prediction)s?|estimated[-\s]?scores?|scoreability|private[-\s]?reviewability|reviewability[-\s]?internals?|private[-\s]?rankings?)\b/gi; + /\b(?:rewards?|payouts?|farming|wallets?|hotkeys?|coldkeys?|seed[-\s]?phrases?|mnemonics?|private[-\s]?keys?|raw[-\s]?trust(?:[-\s]?scores?)?|trust[-\s]?scores?|score[-\s]?(?:estimate|preview|prediction)s?|estimated[-\s]?scores?|scoreability|cohort\w*|ranking\w*|miner[-_\s]?originated|human[-_\s]?originated|private[-\s]?reviewability|reviewability[-\s]?internals?|reviewability|private[-\s]?rankings?)\b/gi; export function redactExtensionText(text: string): string { return text.replace(FORBIDDEN_EXTENSION_TERMS, "[redacted]").replace(/\s+/g, " ").trim(); diff --git a/test/unit/extension-contributor-context.test.ts b/test/unit/extension-contributor-context.test.ts index 08b8053f27..df40cc8335 100644 --- a/test/unit/extension-contributor-context.test.ts +++ b/test/unit/extension-contributor-context.test.ts @@ -7,8 +7,10 @@ import { redactExtensionText, } from "../../src/signals/extension-contributor-context"; import type { ContributorOpportunity, PublicReadinessScore } from "../../src/signals/engine"; +import { PUBLIC_UNSAFE_TERMS } from "../../src/signals/redaction"; -const FORBIDDEN_PUBLIC_TERMS = /wallet|hotkey|coldkey|mnemonic|reward|payout|farming|raw trust|trust score|scoreability|reviewability internals|private ranking/i; +const FORBIDDEN_PUBLIC_TERMS = + /wallet|hotkey|coldkey|mnemonic|reward|payout|farming|raw trust|trust score|scoreability|cohort|ranking|miner-originated|human-originated|reviewability/i; function opportunity(over: Partial = {}): ContributorOpportunity { return { @@ -59,6 +61,64 @@ describe("redactExtensionText", () => { it("leaves safe text untouched", () => { expect(redactExtensionText("Maintainer-created issue, good fit.")).toBe("Maintainer-created issue, good fit."); }); + + // #5840: FORBIDDEN_EXTENSION_TERMS had drifted from redaction.ts's canonical PUBLIC_UNSAFE_TERMS and let + // these economic-identity terms through unredacted. + it("redacts bare cohort, previously leaked", () => { + expect(redactExtensionText("Cohort diagnostics flagged this PR")).toBe("[redacted] diagnostics flagged this PR"); + }); + + it("redacts bare ranking, previously leaked", () => { + expect(redactExtensionText("Your ranking dropped this week")).toBe("Your [redacted] dropped this week"); + }); + + it("redacts miner-originated / human-originated, previously leaked", () => { + expect(redactExtensionText("This looks miner-originated, not human-originated")).toBe("This looks [redacted], not [redacted]"); + // the [-_\s]? separator matches underscore and space forms too, matching PUBLIC_UNSAFE_TERMS. + expect(redactExtensionText("miner_originated and human originated")).toBe("[redacted] and [redacted]"); + }); + + it("redacts standalone reviewability while still redacting the compound forms as a whole", () => { + expect(redactExtensionText("Reviewability is limited right now")).toBe("[redacted] is limited right now"); + expect(redactExtensionText("reviewability internals exposed")).toBe("[redacted] exposed"); + expect(redactExtensionText("private reviewability data")).toBe("[redacted] data"); + }); + + it("stays in sync with the canonical PUBLIC_UNSAFE_TERMS vocabulary (drift guard)", () => { + // Every economic-identity term the canonical boundary blocks must also be scrubbed by this overlay, so the + // two vocabularies can't silently diverge again. Bare `score` is the one intentional exception: this + // surface returns readiness as public BANDS, and redaction.ts's own note records sibling surfaces that + // deliberately do not redact a bare `score`. + const canonical = new RegExp(PUBLIC_UNSAFE_TERMS, "i"); + const intentionalExceptions = new Set(["public score"]); + const samples = [ + "reward", + "wallet", + "hotkey", + "coldkey", + "mnemonic", + "payout", + "farming", + "raw trust", + "trust score", + "ranking", + "cohort", + "miner-originated", + "human-originated", + "private reviewability", + "reviewability", + "public score", // intentional exception -- canonical matches it, this overlay deliberately does not + ]; + for (const sample of samples) { + expect(canonical.test(sample)).toBe(true); // sanity: the canonical vocabulary really does flag it + const redacted = redactExtensionText(sample); + if (intentionalExceptions.has(sample)) { + expect(redacted).toBe(sample); // band-gated: intentionally left as-is + } else { + expect(redacted).toBe("[redacted]"); + } + } + }); }); describe("buildExtensionIssueFit", () => {