Skip to content

Commit 415c641

Browse files
committed
fix(scorer): require hard signals for likely-test
1 parent de68107 commit 415c641

10 files changed

Lines changed: 288 additions & 117 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ This fork monitors both `app.certified.actor.profile` and `app.certified.actor.o
88

99
| Label | Score | Meaning |
1010
|-------|-------|---------|
11-
| ⚠ Likely Test | 0-39 | Placeholder, junk, or obvious test data |
12-
| ● Standard | 40-69 | A valid organization record with the basics filled in |
13-
| ✦ High Quality | 70-100 | A complete organization record with several strong details |
11+
| ⚠ Likely Test | signal-based | Explicit test evidence, such as a configured test PDS or obvious placeholder identity data |
12+
| ● Standard | 0-69 without test signals | A non-test organization record that does not meet the high-quality threshold |
13+
| ✦ High Quality | 70+ without test signals | A complete organization record with several strong details |
1414

1515
## Quick Start
1616

@@ -93,7 +93,7 @@ Scores `app.certified.actor.organization` records on 13 completeness signals (10
9393

9494
Trusted PDS scoring uses the actor's resolved PDS host, not the profile website or organization URLs. Actor PDS lookup runs through the durable recompute queue; the first record from an uncached actor may be labeled by content score first, then corrected once the actor DID document is resolved.
9595

96-
Test detection: authenticity checks catch common placeholder strings (`test`, `asdf`, `lorem ipsum`, etc.) and override the score to force ⚠ Likely Test. Operators can also set `TEST_PDS_HOSTS` to force actors from known development PDS hosts into ⚠ Likely Test.
96+
Test detection is intentionally conservative: hard `testSignals` such as configured `TEST_PDS_HOSTS`, obvious placeholder display names, placeholder domains, or `lorem ipsum` descriptions force ⚠ Likely Test. Softer data-quality issues become `validationNotes` for the dashboard but do not change the tier. Generic words like `test`, `testing`, or `tested` are allowed in profile descriptions, but still show as validation notes in short metadata fields such as organization type or URL labels.
9797

9898
### URL enrichment
9999

src/app/docs/page.tsx

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const SCORING_CRITERIA = [
1717
{
1818
label: 'Description',
1919
points: COMPLETENESS_WEIGHTS.description,
20-
description: 'Awards points when the profile description is present and not obvious test data.',
20+
description: 'Awards points when the profile description is present; generic words like test, testing, or tested are allowed in descriptions.',
2121
},
2222
{
2323
label: 'Organization type',
@@ -166,9 +166,9 @@ export default function DocsPage() {
166166
<li className='flex gap-3'>
167167
<span className='font-[family-name:var(--font-syne)] font-bold text-foreground shrink-0'>3.</span>
168168
<span>
169-
The scoring engine checks an authenticity gate first. Records with authenticity failures are labeled
170-
<ScoreBadge tier='likely-test' /> before completeness scoring begins. Validation notes are shown
171-
separately and do not affect tiering. Passing records then receive the 100-point completeness score.
169+
The scoring engine separates hard test signals from softer validation notes. Records with test signals
170+
are labeled <ScoreBadge tier='likely-test' />. Records without test signals receive the 100-point
171+
completeness score and are labeled standard or high-quality from that score.
172172
</span>
173173
</li>
174174
<li className='flex gap-3'>
@@ -186,11 +186,11 @@ export default function DocsPage() {
186186
Scoring criteria
187187
</h2>
188188
<p className='text-sm text-muted-foreground'>
189-
Each passing organization record is evaluated on 13 completeness criteria for a maximum of 100 completeness
190-
points, plus a configurable actor-PDS trust bonus. The rubric now gives less credit to basic identity fields
189+
Each organization record is evaluated on 13 completeness criteria for a maximum of 100 completeness
190+
points, plus a configurable actor-PDS trust bonus. The rubric gives less credit to basic identity fields
191191
and more weight to harder-to-fake credibility signals like website resolution, location, and visual/profile
192-
completeness. The authenticity gate runs first, validation notes are informational only, and labels stay
193-
attached to the organization record URI.
192+
completeness. Hard test signals override the score to likely-test; validation notes are informational only,
193+
and labels stay attached to the organization record URI.
194194
</p>
195195
<div className='border border-border rounded-lg bg-card overflow-hidden'>
196196
<table className='w-full text-sm'>
@@ -232,27 +232,29 @@ export default function DocsPage() {
232232
</div>
233233

234234
<h3 className='font-[family-name:var(--font-syne)] text-sm font-bold mt-4'>
235-
Authenticity gate
235+
Test signals and validation notes
236236
</h3>
237237
<p className='text-sm text-muted-foreground'>
238-
Matching any gate failure forces <ScoreBadge tier='likely-test' />. These are authenticity failures, not
239-
validation notes, and there are no separate numeric deductions.
238+
Matching a hard test signal forces <ScoreBadge tier='likely-test' />. Validation notes are softer data-quality
239+
issues shown to operators, but they do not change the tier.
240240
</p>
241241
<div className='border border-border rounded-lg bg-card p-4 text-xs text-muted-foreground space-y-1'>
242-
<p className='font-medium text-foreground text-xs'>Authenticity gate failure patterns</p>
242+
<p className='font-medium text-foreground text-xs'>Hard test signal patterns</p>
243243
<ul className='space-y-1 pl-3'>
244-
<li>Common junk values: <span className='font-mono bg-secondary rounded px-1'>test</span>, <span className='font-mono bg-secondary rounded px-1'>asdf</span>, <span className='font-mono bg-secondary rounded px-1'>lorem ipsum</span>, <span className='font-mono bg-secondary rounded px-1'>placeholder</span>, <span className='font-mono bg-secondary rounded px-1'>delete me</span>, <span className='font-mono bg-secondary rounded px-1'>ignore</span>, <span className='font-mono bg-secondary rounded px-1'>todo</span>, <span className='font-mono bg-secondary rounded px-1'>foo</span>, <span className='font-mono bg-secondary rounded px-1'>bar</span>, <span className='font-mono bg-secondary rounded px-1'>abc</span>, <span className='font-mono bg-secondary rounded px-1'>wip</span>, <span className='font-mono bg-secondary rounded px-1'>sample</span>, and <span className='font-mono bg-secondary rounded px-1'>example</span>.</li>
245-
<li>• Empty-style values: <span className='font-mono bg-secondary rounded px-1'>n/a</span>, <span className='font-mono bg-secondary rounded px-1'>none</span>, <span className='font-mono bg-secondary rounded px-1'>null</span>, <span className='font-mono bg-secondary rounded px-1'>undefined</span>, <span className='font-mono bg-secondary rounded px-1'>blank</span>, <span className='font-mono bg-secondary rounded px-1'>draft</span>, <span className='font-mono bg-secondary rounded px-1'>temp</span>, and <span className='font-mono bg-secondary rounded px-1'>tmp</span>.</li>
246-
<li>• Display-name workflow/test terms such as <span className='font-mono bg-secondary rounded px-1'>demo</span>, <span className='font-mono bg-secondary rounded px-1'>dev</span>, <span className='font-mono bg-secondary rounded px-1'>staging</span>, <span className='font-mono bg-secondary rounded px-1'>qa</span>, <span className='font-mono bg-secondary rounded px-1'>e2e</span>, <span className='font-mono bg-secondary rounded px-1'>sandbox</span>, <span className='font-mono bg-secondary rounded px-1'>fixture</span>, <span className='font-mono bg-secondary rounded px-1'>seed data</span>, obvious glued test fixtures like <span className='font-mono bg-secondary rounded px-1'>tobytest</span>, generic names like <span className='font-mono bg-secondary rounded px-1'>org</span>/<span className='font-mono bg-secondary rounded px-1'>org 1</span>/<span className='font-mono bg-secondary rounded px-1'>first org</span>/<span className='font-mono bg-secondary rounded px-1'>new org</span>, <span className='font-mono bg-secondary rounded px-1'>new db</span>, <span className='font-mono bg-secondary rounded px-1'>published</span>, <span className='font-mono bg-secondary rounded px-1'>unpublished</span>, and <span className='font-mono bg-secondary rounded px-1'>changes requested</span>.</li>
247-
<li>• Placeholder domains such as <span className='font-mono bg-secondary rounded px-1'>example.com</span>, <span className='font-mono bg-secondary rounded px-1'>example.org</span>, <span className='font-mono bg-secondary rounded px-1'>example.net</span>, and reserved <span className='font-mono bg-secondary rounded px-1'>.test</span>/<span className='font-mono bg-secondary rounded px-1'>.example</span>/<span className='font-mono bg-secondary rounded px-1'>.invalid</span> hostnames fail URL authenticity checks.</li>
248-
<li>Single-word greetings, repeated characters, repeated-character runs in display names, and numeric-only values are also treated as test data.</li>
244+
<li>Actors hosted on configured <span className='font-mono bg-secondary rounded px-1'>TEST_PDS_HOSTS</span>.</li>
245+
<li>• Placeholder display names such as <span className='font-mono bg-secondary rounded px-1'>test org</span>, <span className='font-mono bg-secondary rounded px-1'>demo</span>, <span className='font-mono bg-secondary rounded px-1'>dev</span>, <span className='font-mono bg-secondary rounded px-1'>staging</span>, <span className='font-mono bg-secondary rounded px-1'>qa</span>, <span className='font-mono bg-secondary rounded px-1'>e2e</span>, <span className='font-mono bg-secondary rounded px-1'>sandbox</span>, <span className='font-mono bg-secondary rounded px-1'>fixture</span>, obvious glued test fixtures like <span className='font-mono bg-secondary rounded px-1'>tobytest</span>, generic names like <span className='font-mono bg-secondary rounded px-1'>org</span>/<span className='font-mono bg-secondary rounded px-1'>org 1</span>/<span className='font-mono bg-secondary rounded px-1'>first org</span>/<span className='font-mono bg-secondary rounded px-1'>new org</span>, and workflow states like <span className='font-mono bg-secondary rounded px-1'>published</span>, <span className='font-mono bg-secondary rounded px-1'>unpublished</span>, or <span className='font-mono bg-secondary rounded px-1'>changes requested</span>.</li>
246+
<li>• Obvious junk descriptions such as <span className='font-mono bg-secondary rounded px-1'>lorem ipsum</span> or a description that is only placeholder text. Generic words like <span className='font-mono bg-secondary rounded px-1'>test</span>, <span className='font-mono bg-secondary rounded px-1'>testing</span>, or <span className='font-mono bg-secondary rounded px-1'>tested</span> are allowed in descriptions.</li>
247+
<li>• Placeholder domains such as <span className='font-mono bg-secondary rounded px-1'>example.com</span>, <span className='font-mono bg-secondary rounded px-1'>example.org</span>, <span className='font-mono bg-secondary rounded px-1'>example.net</span>, and reserved <span className='font-mono bg-secondary rounded px-1'>.test</span>/<span className='font-mono bg-secondary rounded px-1'>.example</span>/<span className='font-mono bg-secondary rounded px-1'>.invalid</span> hostnames.</li>
248+
<li>Repeated-character runs in display names.</li>
249249
</ul>
250250
</div>
251251
<div className='border border-border rounded-lg bg-card p-4 text-xs text-muted-foreground space-y-1'>
252252
<p className='font-medium text-foreground text-xs'>Validation notes</p>
253253
<p>
254-
These are informational only. They can appear when fallback handling keeps usable profile fields and
255-
drops malformed optional fields, and they do not imply suspicious activity or affect tiering.
254+
These are informational only. They can appear when fallback handling keeps usable profile fields,
255+
optional fields are malformed, short metadata fields such as organization type or URL label contain
256+
placeholder/test words, or the record has very little metadata. They do not imply suspicious activity
257+
or affect tiering.
256258
</p>
257259
</div>
258260
</section>
@@ -262,8 +264,8 @@ export default function DocsPage() {
262264
Quality tiers
263265
</h2>
264266
<p className='text-sm text-muted-foreground'>
265-
Scores map to three tiers. Authenticity gate failures override the numeric score and always produce a
266-
<ScoreBadge tier='likely-test' /> label. Validation notes do not change the tier.
267+
Scores map to standard or high-quality unless a hard test signal is present. Test signals override the
268+
numeric score and produce a <ScoreBadge tier='likely-test' /> label. Validation notes do not change the tier.
267269
</p>
268270
<div className='grid gap-3 sm:grid-cols-2'>
269271
{[
@@ -274,13 +276,13 @@ export default function DocsPage() {
274276
},
275277
{
276278
tier: 'standard' as const,
277-
range: '40 – 69',
278-
detail: 'Decent merged record with some useful metadata, but not full rubric coverage.',
279+
range: '0 – 69 without test signals',
280+
detail: 'Non-test merged record with anything below the high-quality threshold.',
279281
},
280282
{
281283
tier: 'likely-test' as const,
282-
range: '0 – 39 or gate failures',
283-
detail: 'Contains authenticity gate failures, or falls below the standard threshold.',
284+
range: 'test signals only',
285+
detail: 'Contains hard test evidence, such as a configured test PDS, placeholder display name, or placeholder domain.',
284286
},
285287
].map(({ tier, range, detail }) => (
286288
<div key={tier} className='border border-border rounded-lg bg-card p-4 space-y-2'>

src/components/ScoreBreakdown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function ScoreBreakdown({ breakdown, validationNotes = [], testSignals }:
4747
return (
4848
<div>
4949
<p className='mb-2 text-[11px] text-muted-foreground'>
50-
Authenticity gate failures are checked before completeness scoring.
50+
Hard test signals override tiering; validation notes are informational only.
5151
</p>
5252

5353
{CRITERIA.map(({ label, field, max }) => {
@@ -137,7 +137,7 @@ export function ScoreBreakdown({ breakdown, validationNotes = [], testSignals }:
137137
<circle cx='6' cy='8.5' r='0.5' fill='currentColor' className='text-rose-700 dark:text-rose-400' />
138138
</svg>
139139
<span className='text-[11px] font-medium text-rose-700 dark:text-rose-400'>
140-
Authenticity gate failed
140+
Hard test signal
141141
</span>
142142
</div>
143143
{testSignals.map((signal, i) => (

src/lib/constants.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { LabelDefinition } from './types'
22

33
type RuntimeQualityTier = 'likely-test' | 'standard' | 'high-quality'
4-
5-
export const AUTHENTICITY_FAILURE_TIER: RuntimeQualityTier = 'likely-test'
4+
type ScoreBasedQualityTier = Exclude<RuntimeQualityTier, 'likely-test'>
65

76
// Shared time unit for score boundaries.
87
export const MS_PER_DAY = 24 * 60 * 60 * 1000
@@ -39,11 +38,12 @@ export const COMPLETENESS_WEIGHTS = {
3938
banner: 10,
4039
} as const
4140

42-
// Final score bands are intentionally coarse. High-quality is open-ended
43-
// because configured bonuses can raise the final score above 100 completeness points.
44-
export const SCORE_THRESHOLDS: Record<RuntimeQualityTier, { min: number; max: number }> = {
45-
'likely-test': { min: 0, max: 39 },
46-
standard: { min: 40, max: 69 },
41+
// Final score bands are intentionally coarse. Likely-test is signal-based:
42+
// records without hard test evidence default to standard until they meet the
43+
// high-quality threshold. High-quality is open-ended because configured bonuses
44+
// can raise the final score above 100 completeness points.
45+
export const SCORE_THRESHOLDS: Record<ScoreBasedQualityTier, { min: number; max: number }> = {
46+
standard: { min: 0, max: 69 },
4747
'high-quality': { min: 70, max: Number.POSITIVE_INFINITY },
4848
}
4949

@@ -65,10 +65,16 @@ export const DEFAULT_TRUSTED_PDS_HOSTS = ['certified.one', 'gainforest.id'] as c
6565
/** Default score points added when an actor is hosted on a trusted PDS. */
6666
export const DEFAULT_TRUSTED_PDS_BONUS = 10
6767

68-
export const TEST_PATTERNS: RegExp[] = [
69-
// Word-boundary "test" — catches "Another Test", "Test Contributors", "This is testing", "test 123"
68+
/**
69+
* Test-word patterns are hard evidence in short identity fields like display
70+
* names, but too common for free-form descriptions.
71+
*/
72+
export const TEST_WORD_PATTERNS: RegExp[] = [
7073
/\btest(ing|ed|er|s)?\b/i,
74+
]
7175

76+
/** Obvious placeholder strings that can be checked outside descriptions. */
77+
export const TEST_PATTERNS: RegExp[] = [
7278
// Common junk prefixes and phrases.
7379
/^asdf/i, /\blorem ipsum\b/i, /^placeholder/i, /^delete me/i, /^ignore/i, /^zzz/i,
7480

@@ -102,11 +108,37 @@ export const AUTHENTICITY_TEXT_PATTERNS: RegExp[] = [
102108
/^unlisted$/i,
103109
]
104110

111+
/** Placeholder patterns for short metadata fields, where test words are meaningful signals. */
112+
export const SHORT_FIELD_AUTHENTICITY_TEXT_PATTERNS: RegExp[] = [
113+
...TEST_WORD_PATTERNS,
114+
...AUTHENTICITY_TEXT_PATTERNS,
115+
]
116+
117+
/**
118+
* Placeholder patterns for descriptions. Generic test words are intentionally
119+
* excluded because real organization descriptions often mention tests, testing,
120+
* or tested methods.
121+
*/
122+
export const DESCRIPTION_AUTHENTICITY_TEXT_PATTERNS: RegExp[] = [
123+
/\blorem ipsum\b/i,
124+
/^asdf/i,
125+
/^placeholder$/i,
126+
/^delete me$/i,
127+
/^ignore$/i,
128+
/^todo$/i,
129+
/^n\/a$/i,
130+
/^none$/i,
131+
/^null$/i,
132+
/^undefined$/i,
133+
/^blank$/i,
134+
]
135+
105136
/**
106137
* Extra authenticity patterns for display names, where workflow/test labels are
107138
* much stronger evidence than the same words appearing in longer descriptions.
108139
*/
109140
export const DISPLAY_NAME_AUTHENTICITY_TEXT_PATTERNS: RegExp[] = [
141+
...TEST_WORD_PATTERNS,
110142
...AUTHENTICITY_TEXT_PATTERNS,
111143
/(?:^|[^\p{Letter}\p{Number}])(?:demo|dev|staging|qa|e2e|sandbox|fixture)(?:$|[^\p{Letter}\p{Number}])/iu,
112144
/^tobytest\d*$/i,
@@ -122,7 +154,7 @@ export const DISPLAY_NAME_AUTHENTICITY_TEXT_PATTERNS: RegExp[] = [
122154
/** Reserved example domains that should never count as organization evidence. */
123155
export const PLACEHOLDER_DOMAINS = ['example.com', 'example.net', 'example.org'] as const
124156

125-
/** Reserved non-production TLDs that should fail the authenticity gate. */
157+
/** Reserved non-production TLDs that should produce a hard test signal. */
126158
export const PLACEHOLDER_TLDS = ['example', 'invalid', 'test'] as const
127159

128160
export const LABEL_LIMIT = 1

src/lib/hf-classifier.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { HfInference } from '@huggingface/inference'
22
import * as config from './config'
33
import { HF_POSITIVE_LABEL, updateActivityHfFields, getActivityByDidRkey, getHfClassifiedNonFlagged } from './db'
44
import { updateActivity } from './db'
5+
import { tierForScore } from './scorer'
56

67
export interface ContentClassification {
78
label: string
@@ -94,12 +95,20 @@ function reclassifyWithHfSignal(did: string, rkey: string, classification: Conte
9495
? [...existingSignals, signal]
9596
: existingSignals
9697

98+
const tier = tierForScore(row.score, updatedSignals)
99+
97100
updateActivity(did, rkey, {
98101
score: row.score,
99-
tier: row.tier,
102+
tier,
100103
breakdown: row.breakdown,
101104
testSignals: JSON.stringify(updatedSignals),
102105
})
106+
107+
if (tier !== row.tier && _onReclassify) {
108+
void _onReclassify(row.uri, tier).catch(err => {
109+
console.warn('[hf-classifier] label update failed:', err instanceof Error ? err.message : err)
110+
})
111+
}
103112
}
104113

105114
let hf: HfInference | null = null

0 commit comments

Comments
 (0)