Skip to content

Commit d2442d8

Browse files
authored
Merge pull request #48 from peopleworks/verdict-that-one-source-decides
Reconcile the verdict with the measurement (#32)
2 parents 2717949 + 6a0c89e commit d2442d8

13 files changed

Lines changed: 134 additions & 60 deletions

File tree

Docs/CALIBRATION.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ Across these groups the median score runs from 7.2 (**es-wikipedia**) down to 4.
7373
| 95 | 0 / 90 | 0% | 0% – 4.1% |
7474
| 100 | 0 / 90 | 0% | 0% – 4.1% |
7575

76+
## What the product does with this number
77+
78+
The tool speaks at **25/100** and nowhere else, taking the boundary from the table above rather than from anybody's judgement. Below it a document gets its score and the reason it gets nothing more: a low score is not evidence that a person wrote something, since a detector that detects nothing also returns a low score, and this project has deliberately never measured how much machine writing it catches. The boundary moves when this page moves — including upward if a larger corpus turns out to be less flattering.
79+
80+
Above it there is **one** verdict, not a scale of them. This corpus can place a boundary and can say nothing whatever about how much further past it a score has travelled: no text known to be human came close to the upper reaches, and grading "moderate" against "strong" would need machine-written text, which the opening of this page argues against collecting. Interfaces do shade a high score more urgently than a low one, and those shades are a display convention — they are not on this page because nothing measured them.
81+
82+
A language absent from the corpus entirely gets no verdict at all, whatever it scores. A language present but too thin to bound its own rate borrows this boundary and carries its own figure beside it, so the reader weighs the rate that was measured for the writing in front of them rather than the pooled one.
83+
7684
## Which rules misfire
7785

7886
Every rule below fired on text no machine wrote, so each hit is a false positive by construction — there is no judgement call to make. This is the most immediately useful thing the exercise produces: it turns "our rules probably have false positives somewhere" into a ranked list.

src/SignsOfAI.Core/Model/AnalysisResult.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,7 @@ public sealed record AnalysisResult
9393
/// person goes through the interface's localiser or the report's own resources, both of which
9494
/// take their boundary from <see cref="VerdictBands"/> exactly as this does.
9595
/// </summary>
96-
public string Verdict => VerdictBands.Emphasis(OverallScore) switch
97-
{
98-
VerdictEmphasis.High => "Strong signs of AI writing",
99-
VerdictEmphasis.Elevated => "Moderate signs of AI writing",
100-
VerdictEmphasis.Present => "Light signs of AI writing",
101-
_ => "Reads mostly human",
102-
};
96+
public string Verdict => VerdictBands.Holds(OverallScore)
97+
? "Signs of AI writing"
98+
: "No signs above the measured boundary";
10399
}

src/SignsOfAI.Core/Reporting/EvidenceReport.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -499,10 +499,8 @@ private static void HowOftenWrong(StringBuilder sb, ReportText text, string? lan
499499

500500
private static string Verdict(ReportText text, double score) => text.Get(score switch
501501
{
502-
_ when VerdictBands.Emphasis(score) is VerdictEmphasis.High => ReportMessages.VerdictStrong,
503-
_ when VerdictBands.Emphasis(score) is VerdictEmphasis.Elevated => ReportMessages.VerdictModerate,
504-
_ when VerdictBands.Emphasis(score) is VerdictEmphasis.Present => ReportMessages.VerdictLight,
505-
_ => ReportMessages.VerdictMinimal,
502+
_ when VerdictBands.Holds(score) => ReportMessages.VerdictSigns,
503+
_ => ReportMessages.VerdictNone,
506504
}).Text;
507505

508506
private static string LanguageName(ReportText text, string language) => text.Get(

src/SignsOfAI.Core/Reporting/ReportMessages.cs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,27 @@ public static class ReportMessages
2929
public const string SectionObservations = "section.observations";
3030
public const string SectionErrorRate = "section.error-rate";
3131
public const string SectionUnreadable = "section.unreadable";
32-
public const string VerdictStrong = "verdict.strong";
33-
public const string VerdictModerate = "verdict.moderate";
34-
public const string VerdictLight = "verdict.light";
35-
public const string VerdictMinimal = "verdict.minimal";
32+
/// <summary>
33+
/// Above the measured boundary. One wording, not three.
34+
///
35+
/// "Strong", "Moderate" and "Light" read as three measured degrees, and they never were: the
36+
/// corpus locates the boundary and says nothing whatever about 45 or 70, since no text known to
37+
/// be human came within twenty points of either. Separating degrees would need machine-written
38+
/// text, and Docs/CALIBRATION.md argues against ever collecting it. Printing the three words
39+
/// with a footnote admitting they are unmeasured does not fix that — the footnote is read once
40+
/// and the heading is read every time.
41+
/// </summary>
42+
public const string VerdictSigns = "verdict.signs";
43+
44+
/// <summary>
45+
/// Below it. A statement about this tool, never about the writer.
46+
///
47+
/// The report used to say "Reads mostly human" and the interface "Minimal signs of AI writing" —
48+
/// the same state, two claims, and the first one is not ours to make. A detector that detects
49+
/// nothing also returns a low score, and this project has deliberately never measured how much
50+
/// machine writing it catches, so a low score is evidence about the boundary and nothing else.
51+
/// </summary>
52+
public const string VerdictNone = "verdict.none";
3653
public const string AnalysisScoreWithVerdict = "analysis.score.with-verdict";
3754
public const string AnalysisScoreWithoutVerdict = "analysis.score.without-verdict";
3855
public const string AnalysisNoVerdict = "analysis.no-verdict";
@@ -107,10 +124,8 @@ public static class ReportMessages
107124
[SectionObservations] = 0,
108125
[SectionErrorRate] = 0,
109126
[SectionUnreadable] = 0,
110-
[VerdictStrong] = 0,
111-
[VerdictModerate] = 0,
112-
[VerdictLight] = 0,
113-
[VerdictMinimal] = 0,
127+
[VerdictSigns] = 0,
128+
[VerdictNone] = 0,
114129
[AnalysisScoreWithVerdict] = 2,
115130
[AnalysisScoreWithoutVerdict] = 1,
116131
[AnalysisNoVerdict] = 0,
@@ -195,10 +210,8 @@ public static class ReportMessages
195210
[SectionObservations] = "Found, but at a rate people write at",
196211
[SectionErrorRate] = "How often this is wrong",
197212
[SectionUnreadable] = "Could not be read",
198-
[VerdictStrong] = "Strong signs of AI writing",
199-
[VerdictModerate] = "Moderate signs of AI writing",
200-
[VerdictLight] = "Light signs of AI writing",
201-
[VerdictMinimal] = "Reads mostly human",
213+
[VerdictSigns] = "Signs of AI writing",
214+
[VerdictNone] = "No signs above the measured boundary",
202215
[AnalysisScoreWithVerdict] = "**{0}/100 — {1}**",
203216
[AnalysisScoreWithoutVerdict] = "**{0}/100**",
204217
[AnalysisNoVerdict] = "*Below the threshold this build can support, so no verdict is given. A low score is not evidence that a person wrote this.*",
@@ -264,7 +277,7 @@ public static class ReportMessages
264277
FallbackMarker, FallbackSummary, FallbackLanguage, DefaultTitle,
265278
SectionAnalysis, SectionCheckable, SectionCharacters, SectionCitations, SectionSignals,
266279
SectionObservations, SectionErrorRate, SectionUnreadable,
267-
VerdictStrong, VerdictModerate, VerdictLight, VerdictMinimal,
280+
VerdictSigns, VerdictNone,
268281
AnalysisNoVerdict,
269282
LanguageEnglish, LanguageSpanish, LanguageOther,
270283
CaveatUncalibrated, CaveatAggregateNoThreshold, CaveatLanguageUnmeasured,

src/SignsOfAI.Core/Reporting/report.en.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@
1818
"section.observations": { "text": "Found, but at a rate people write at" },
1919
"section.error-rate": { "text": "How often this is wrong" },
2020
"section.unreadable": { "text": "Could not be read" },
21-
"verdict.strong": { "text": "Strong signs of AI writing" },
22-
"verdict.moderate": { "text": "Moderate signs of AI writing" },
23-
"verdict.light": { "text": "Light signs of AI writing" },
24-
"verdict.minimal": { "text": "Reads mostly human" },
21+
"verdict.signs": { "text": "Signs of AI writing" },
22+
"verdict.none": { "text": "No signs above the measured boundary" },
2523
"analysis.score.with-verdict": { "text": "**{0}/100 — {1}**" },
2624
"analysis.score.without-verdict": { "text": "**{0}/100**" },
2725
"analysis.no-verdict": { "text": "*Below the threshold this build can support, so no verdict is given. A low score is not evidence that a person wrote this.*" },

src/SignsOfAI.Core/Reporting/report.es.json

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,13 @@
5454
"text": "No se pudieron leer",
5555
"sourceHash": "51a02791dcb61eb8846a00d3f0263a9da516b30b6a92a3b08e34d65af4cef259"
5656
},
57-
"verdict.strong": {
58-
"text": "Señales fuertes de escritura con IA",
59-
"sourceHash": "ee17bc5f7ea18e88a5a4a535d8a7d8f86fbaf3559c0b1927c71eb299ca138eef"
57+
"verdict.signs": {
58+
"text": "Señales de escritura con IA",
59+
"sourceHash": "a03ec88d694c5dee62e6b04c9b46b89cdcb6e5d48b2971dba539fe7a5fbef0fc"
6060
},
61-
"verdict.moderate": {
62-
"text": "Señales moderadas de escritura con IA",
63-
"sourceHash": "78304806a705ef996e3115ba84e0256f7838debf03afccb023e91d299d628a5b"
64-
},
65-
"verdict.light": {
66-
"text": "Señales leves de escritura con IA",
67-
"sourceHash": "99844fce9fa8df4247389342a41e53d0d231b950cc700f2458207dfef79d5d2e"
68-
},
69-
"verdict.minimal": {
70-
"text": "Parece escrito mayormente por una persona",
71-
"sourceHash": "4dd5c224203bf31595b61831cbd46ec9f2da352ce359b92ce46262e86cda64ce"
61+
"verdict.none": {
62+
"text": "Sin señales por encima del umbral medido",
63+
"sourceHash": "dfdd3c06acb001454e7ec45ae2ffce857964a2531bb41686fc9f61c6abc16003"
7264
},
7365
"analysis.no-verdict": {
7466
"text": "*Por debajo del umbral que esta compilación puede respaldar, no se emite ningún veredicto. Una puntuación baja no demuestra que una persona haya escrito este texto.*",

src/SignsOfAI.UI/Services/Loc.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,10 @@ private async Task LogAsync(string message)
249249
/// The one-line verdict for an overall score, in the interface's language. The boundary comes
250250
/// from <see cref="VerdictBands"/>; this only chooses the words for it.
251251
/// </summary>
252-
public string Verdict(double score) => VerdictBands.Emphasis(score) switch
252+
public string Verdict(double score) => VerdictBands.Holds(score) switch
253253
{
254-
VerdictEmphasis.High => this["verdict.strong"],
255-
VerdictEmphasis.Elevated => this["verdict.moderate"],
256-
VerdictEmphasis.Present => this["verdict.light"],
257-
_ => this["verdict.minimal"],
254+
true => this["verdict.signs"],
255+
false => this["verdict.none"],
258256
};
259257

260258
/// <summary>How to name the language the analyzer settled on. The engine only knows EN and ES.</summary>

src/SignsOfAI.UI/wwwroot/i18n/en.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@
4646
"sev.low": "Low",
4747
"sev.medium": "Medium",
4848
"sev.high": "High",
49-
"verdict.strong": "Strong signs of AI writing",
50-
"verdict.moderate": "Moderate signs of AI writing",
51-
"verdict.light": "Light signs of AI writing",
52-
"verdict.minimal": "Minimal signs of AI writing",
49+
"verdict.signs": "Signs of AI writing",
50+
"verdict.none": "No signs above the measured boundary",
5351
"home.pagetitle": "Signs of AI Writing — detect & de-AI-ify your text",
5452
"home.tagline": "Paste, upload, or just start typing. We flag the tells of AI writing — overused words, rhetorical crutches, robotic rhythm — <strong>live, as you write</strong>, and show you how to fix each one.",
5553
"home.privacy": "Analysis runs entirely in your browser. Your text never leaves your device.",

src/SignsOfAI.UI/wwwroot/i18n/es.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@
4646
"sev.low": "Baja",
4747
"sev.medium": "Media",
4848
"sev.high": "Alta",
49-
"verdict.strong": "Señales fuertes de escritura IA",
50-
"verdict.moderate": "Señales moderadas de escritura IA",
51-
"verdict.light": "Señales leves de escritura IA",
52-
"verdict.minimal": "Señales mínimas de escritura IA",
49+
"verdict.signs": "Señales de escritura con IA",
50+
"verdict.none": "Sin señales por encima del umbral medido",
5351
"home.pagetitle": "Señales de escritura IA — detecta y humaniza tu texto",
5452
"home.tagline": "Pega, sube un archivo o simplemente empieza a escribir. Marcamos las señales de escritura IA —palabras sobreusadas, muletillas retóricas, ritmo robótico— <strong>en vivo, mientras escribes</strong>, y te mostramos cómo corregir cada una.",
5553
"home.privacy": "El análisis se ejecuta por completo en tu navegador. Tu texto nunca sale de tu dispositivo.",

tests/SignsOfAI.Core.Tests/EvidenceReportTests.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ public void An_unmeasured_language_gets_no_aggregate_threshold_or_verdict()
272272

273273
Assert.Contains("never been measured for language code pt", report);
274274
Assert.Contains("aggregate result from other languages is not a substitute", report);
275-
Assert.DoesNotContain("Strong signs of AI writing", report);
275+
Assert.DoesNotContain("Signs of AI writing", report);
276276

277277
if (PublishedCalibration.Current is { } calibration)
278278
{
@@ -328,11 +328,13 @@ public void A_report_language_without_the_mandatory_core_is_rejected()
328328
[Fact]
329329
public void Withholds_the_verdict_below_the_threshold_it_can_support()
330330
{
331-
// "Reads mostly human" printed above "treat the score as saying nothing" is a page arguing
332-
// with itself, and the reader keeps whichever half suits them.
331+
// A verdict printed above "treat the score as saying nothing" is a page arguing with itself,
332+
// and the reader keeps whichever half suits them. Below the boundary the report prints the
333+
// score and the reason, and no verdict line of either kind.
333334
var report = Report();
334335

335-
Assert.DoesNotContain("Reads mostly human", report);
336+
Assert.DoesNotContain("No signs above the measured boundary", report);
337+
Assert.DoesNotContain("Signs of AI writing", report);
336338
Assert.Contains("A low score is not evidence that a person wrote this", report);
337339
}
338340

0 commit comments

Comments
 (0)