Skip to content

Commit b4215ea

Browse files
peopleworksclaude
andcommitted
Catch the half of the conversation nobody meant to submit
A competitor's pattern set — amanmaqsood/prose-humanizer, MIT, a writing skill rather than a detector — turned out to hold 70 signals our English pack does not fire on. Six of them are worth having and forty-six are not, and the difference is the point. Every other rule here is a judgement about style, and a formal writer can lose to any of them. These six are not that. "I hope this helps" at the foot of an essay is not a register a student chose; it is an assistant's closing line, pasted in with the answer, and it says where the file has been rather than who is talented — the same kind of claim the character scanner makes. That is also why a corpus of research articles could admit them: no amount of pre-2022 academic prose tells us how often a human blogger writes "the possibilities are endless", but none of it will ever contain "as of my last training update". Screened before they were written, not after somebody complained. Each regex was counted against the calibration corpus — 249,455 words of English and 39,712 of Spanish, all published before generative models existed — and all six scored zero in both. Re-running the calibration afterwards moved nothing: same corpus fingerprint, same 0 of 90 at 25/100, same 4.1% upper bound. They cost nothing to carry. Twelve other candidates from the same source were rejected by that same screen, and they are the interesting half. Its tier-1 vocabulary list contains underpin (5 of 65 texts), optimize (6), elucidate (5), paradigm (4), exemplify (3) and illuminate (3) — ordinary research English. They fire too rarely for the human-rate gate to catch them, which needs eight texts, and too often to be called a tell. Adding them would have spent the false-positive budget of every formal writer in the corpus. This is issue #61 in the other language, found before shipping rather than after. Six more were left out for a reason worth writing down: "this essay will explore", "here's the kicker", "the possibilities are endless". These are plausible tells whose population is student essays and blog posts, and the corpus contains neither. Measuring on one population and applying to another is the mistake #59 already exists for. They need their own corpus first. Both languages, because the residue is bilingual too. 306 tests, 23 new — including the ones that check the rules leave a tutor's own "let me know" and a historian's mid-sentence "of course" alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015PEbbiYSNPw7jE3LrPNhyF
1 parent 2be0d2b commit b4215ea

6 files changed

Lines changed: 221 additions & 2 deletions

File tree

Docs/CALIBRATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ It is **not an accuracy figure**. Accuracy needs machine-written text to measure
1111
- **Corpus** `signsofai-human-baseline`, fingerprint `123fa5b9ebca3f29`
1212
- **Texts** 90 (280,221 words)
1313
- **Engine** SignsOfAI.Core 0.4.0
14-
- **Run** 2026-08-05
14+
- **Run** 2026-08-24
1515
- **Target false-positive rate** 5%
1616

1717
Every text here was published before generative models could have written it. That is the whole basis for calling it human, and it is a stronger guarantee than any classifier offers about anything. The manifest names each source, its licence and its year, so the claim can be traced rather than trusted.

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,9 @@ The optional server is a normal ASP.NET Core app (`dotnet publish` the `SignsOfA
384384

385385
Created by **Pedro Hernández — PeopleWorks**, [Microsoft MVP for .NET](https://mvp.microsoft.com/en-US/mvp/profile/24060a02-dbc6-44ec-bca5-c213ff9835c5). Detection markers are grounded in
386386
linguistics research on AI stylometry — see `Docs/GoogleResearch.md`.
387+
388+
The `chat.*` rules — the assistant's own turn, left in the document — were adapted from the pattern
389+
set of [amanmaqsood/prose-humanizer](https://github.com/amanmaqsood/prose-humanizer) (MIT), a writing
390+
skill rather than a detector. They entered the packs the way everything here does: screened against
391+
the calibration corpus first, where all six scored zero. Twelve other candidates from the same source
392+
did not enter, because they fire on writing published before generative models existed.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"corpusId":"signsofai-human-baseline","texts":90,"measuredOn":"2026-08-05","engine":"0.4.0","recommendedThreshold":25,"flaggedAtThreshold":0,"rateLow":0,"rateHigh":0.04093562618368095,"noisiestRules":[{"ruleId":"stat.burstiness","textShare":0.2777777777777778},{"ruleId":"rhet.in-terms-of","textShare":0.1},{"ruleId":"rhet.not-only-but","textShare":0.08888888888888889},{"ruleId":"rhet.in-order-to","textShare":0.07777777777777778},{"ruleId":"lex.furthermore","textShare":0.07777777777777778},{"ruleId":"lex.robust","textShare":0.07777777777777778},{"ruleId":"lex.just","textShare":0.07777777777777778},{"ruleId":"lex.simply","textShare":0.07777777777777778}],"languages":[{"language":"en","texts":65,"recommendedThreshold":null,"bestBound":0.05580153215404492},{"language":"es","texts":25,"recommendedThreshold":null,"bestBound":0.13319225276039096}]}
1+
{"corpusId":"signsofai-human-baseline","texts":90,"measuredOn":"2026-08-24","engine":"0.4.0","recommendedThreshold":25,"flaggedAtThreshold":0,"rateLow":0,"rateHigh":0.04093562618368095,"noisiestRules":[{"ruleId":"stat.burstiness","textShare":0.2777777777777778},{"ruleId":"rhet.in-terms-of","textShare":0.1},{"ruleId":"rhet.not-only-but","textShare":0.08888888888888889},{"ruleId":"rhet.in-order-to","textShare":0.07777777777777778},{"ruleId":"lex.furthermore","textShare":0.07777777777777778},{"ruleId":"lex.robust","textShare":0.07777777777777778},{"ruleId":"lex.just","textShare":0.07777777777777778},{"ruleId":"lex.simply","textShare":0.07777777777777778}],"languages":[{"language":"en","texts":65,"recommendedThreshold":null,"bestBound":0.05580153215404492},{"language":"es","texts":25,"recommendedThreshold":null,"bestBound":0.13319225276039096}]}

src/SignsOfAI.Core/Rules/Packs/rules.en.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,66 @@
11141114
"severity": "Info",
11151115
"message": "Mid-sentence bold — formatting slop.",
11161116
"suggestion": "Emphasis rarely helps prose; cut the bold."
1117+
},
1118+
{
1119+
"id": "chat.model-self-reference",
1120+
"category": "Rhetorical",
1121+
"regex": "\\bas an? (AI|artificial intelligence)( language)?( model| assistant)?\\b|\\bas a large language model\\b|\\bI(’|')?m an AI\\b",
1122+
"weight": 6.0,
1123+
"severity": "High",
1124+
"message": "A sentence the assistant says about itself, not a sentence in a document.",
1125+
"suggestion": "Delete it. If the text was drafted with a tool, say so in the acknowledgements instead.",
1126+
"evidence": "0 occurrences in the 249,455-word en half of the human calibration corpus (pre-2022, 65 texts)"
1127+
},
1128+
{
1129+
"id": "chat.training-cutoff",
1130+
"category": "Rhetorical",
1131+
"regex": "\\bas of my (last )?(training|knowledge)[- ](update|cutoff|cut-off|data)\\b|\\bmy training data (only )?(goes|extends|runs) up to\\b|\\bas of my last update\\b",
1132+
"weight": 6.0,
1133+
"severity": "High",
1134+
"message": "A disclaimer about a model’s training data, left in the text.",
1135+
"suggestion": "Delete it, and check the claim it was hedging.",
1136+
"evidence": "0 occurrences in the 249,455-word en half of the human calibration corpus (pre-2022, 65 texts)"
1137+
},
1138+
{
1139+
"id": "chat.signoff",
1140+
"category": "Rhetorical",
1141+
"regex": "\\bI hope th(is|at) helps\\b|\\bhope th(is|at) helps\\b|\\bwould you like me to\\b|\\blet me know if you(’|')?(d| would) like\\b|\\bis there anything else (I can|you(’|')?d like)\\b",
1142+
"weight": 6.0,
1143+
"severity": "High",
1144+
"message": "The closing line of an assistant’s reply, inside the document.",
1145+
"suggestion": "Delete it. The document has no reader to offer further help to.",
1146+
"evidence": "0 occurrences in the 249,455-word en half of the human calibration corpus (pre-2022, 65 texts)"
1147+
},
1148+
{
1149+
"id": "chat.capability-disclaimer",
1150+
"category": "Rhetorical",
1151+
"regex": "\\bI (can(’|')?t|cannot|do not|don(’|')?t) (browse|access) (the )?(internet|web|real-?time)\\b|\\bI (do not|don(’|')?t) have (access to )?real-?time (data|information|access)\\b|\\bI(’|')?m unable to browse\\b",
1152+
"weight": 5.0,
1153+
"severity": "High",
1154+
"message": "A statement about what the writing tool cannot do.",
1155+
"suggestion": "Delete it, and supply the source it was standing in for.",
1156+
"evidence": "0 occurrences in the 249,455-word en half of the human calibration corpus (pre-2022, 65 texts)"
1157+
},
1158+
{
1159+
"id": "chat.answer-preamble",
1160+
"category": "Rhetorical",
1161+
"regex": "\\bhere(’|')?s (the|a|your) (rewritten|revised|edited|updated|improved|polished) (version|text|draft|paragraph|essay|article)\\b|\\bhere is (the|a|your) (rewritten|revised|edited|updated|improved|polished) (version|text|draft|paragraph|essay|article)\\b",
1162+
"weight": 5.0,
1163+
"severity": "High",
1164+
"message": "The line a tool prints before handing back its answer.",
1165+
"suggestion": "Delete it. The document should start with the document.",
1166+
"evidence": "0 occurrences in the 249,455-word en half of the human calibration corpus (pre-2022, 65 texts)"
1167+
},
1168+
{
1169+
"id": "chat.eager-opener",
1170+
"category": "Rhetorical",
1171+
"regex": "(?m)(?:^|(?<=[.!?])\\s)(Certainly|Of course|Absolutely|Sure thing|Great question)[!,]",
1172+
"weight": 3.5,
1173+
"severity": "Medium",
1174+
"message": "An assistant’s eager opener (“Certainly!”, “Great question!”).",
1175+
"suggestion": "Delete it. Nobody asked the document a question.",
1176+
"evidence": "0 occurrences in the 249,455-word en half of the human calibration corpus (pre-2022, 65 texts)"
11171177
}
11181178
],
11191179
"functionWords": [

src/SignsOfAI.Core/Rules/Packs/rules.es.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,66 @@
916916
"severity": "Info",
917917
"message": "Negrita a media frase — ruido de formato.",
918918
"suggestion": "El énfasis rara vez ayuda a la prosa; quita la negrita."
919+
},
920+
{
921+
"id": "chat.model-self-reference",
922+
"category": "Rhetorical",
923+
"regex": "\\bcomo (una? )?(modelo de lenguaje|IA|inteligencia artificial|asistente de (IA|inteligencia artificial))\\b|\\bsoy una (IA|inteligencia artificial)\\b",
924+
"weight": 6.0,
925+
"severity": "High",
926+
"message": "Una frase que el asistente dice sobre sí mismo, no una frase del documento.",
927+
"suggestion": "Bórrala. Si el texto se redactó con una herramienta, dilo en los agradecimientos.",
928+
"evidence": "0 occurrences in the 39,712-word es half of the human calibration corpus (pre-2022, 25 texts)"
929+
},
930+
{
931+
"id": "chat.training-cutoff",
932+
"category": "Rhetorical",
933+
"regex": "\\bhasta mi última actualización\\b|\\bmis datos de entrenamiento\\b|\\bhasta donde (llega|alcanza) mi entrenamiento\\b|\\bmi conocimiento (llega|alcanza) hasta\\b",
934+
"weight": 6.0,
935+
"severity": "High",
936+
"message": "Una advertencia sobre los datos de entrenamiento de un modelo, olvidada en el texto.",
937+
"suggestion": "Bórrala, y comprueba la afirmación que estaba matizando.",
938+
"evidence": "0 occurrences in the 39,712-word es half of the human calibration corpus (pre-2022, 25 texts)"
939+
},
940+
{
941+
"id": "chat.signoff",
942+
"category": "Rhetorical",
943+
"regex": "\\bespero que (esto |te |le )?(te |le )?(ayude|sirva|haya servido)\\b|¿(quieres|deseas|te gustaría) que (te )?(lo |la |los |las )?(reescriba|amplíe|resuma|ayude|prepare)\\b|\\bavísame si (quieres|necesitas|te)\\b|¿(hay )?algo más en lo que pueda ayudar",
944+
"weight": 6.0,
945+
"severity": "High",
946+
"message": "La línea de cierre de una respuesta de asistente, dentro del documento.",
947+
"suggestion": "Bórrala. El documento no tiene a quién ofrecerle más ayuda.",
948+
"evidence": "0 occurrences in the 39,712-word es half of the human calibration corpus (pre-2022, 25 texts)"
949+
},
950+
{
951+
"id": "chat.capability-disclaimer",
952+
"category": "Rhetorical",
953+
"regex": "\\bno (tengo|puedo) (acceso a |navegar por |consultar )?(internet|la web|información en tiempo real)\\b|\\bno tengo acceso en tiempo real\\b",
954+
"weight": 5.0,
955+
"severity": "High",
956+
"message": "Una declaración sobre lo que la herramienta de escritura no puede hacer.",
957+
"suggestion": "Bórrala, y pon la fuente que estaba sustituyendo.",
958+
"evidence": "0 occurrences in the 39,712-word es half of the human calibration corpus (pre-2022, 25 texts)"
959+
},
960+
{
961+
"id": "chat.answer-preamble",
962+
"category": "Rhetorical",
963+
"regex": "\\baquí (tienes|está|te dejo) (el|la|una|tu) (versión|texto|párrafo|redacción|artículo) (reescrit[ao]|revisad[ao]|corregid[ao]|mejorad[ao]|actualizad[ao])\\b|\\baquí (tienes|está) (el|la) (versión|texto) (reescrit[ao]|revisad[ao])\\b",
964+
"weight": 5.0,
965+
"severity": "High",
966+
"message": "La línea que imprime una herramienta antes de entregar su respuesta.",
967+
"suggestion": "Bórrala. El documento debería empezar por el documento.",
968+
"evidence": "0 occurrences in the 39,712-word es half of the human calibration corpus (pre-2022, 25 texts)"
969+
},
970+
{
971+
"id": "chat.eager-opener",
972+
"category": "Rhetorical",
973+
"regex": "(?m)(?:^|(?<=[.!?¡])\\s)¡?(Por supuesto|Claro|Excelente pregunta|Desde luego)[!,]",
974+
"weight": 3.5,
975+
"severity": "Medium",
976+
"message": "Apertura entusiasta de asistente («¡Por supuesto!», «¡Excelente pregunta!»).",
977+
"suggestion": "Bórrala. Nadie le ha hecho una pregunta al documento.",
978+
"evidence": "0 occurrences in the 39,712-word es half of the human calibration corpus (pre-2022, 25 texts)"
919979
}
920980
],
921981
"functionWords": [
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System.Linq;
2+
using SignsOfAI.Core;
3+
using Xunit;
4+
5+
namespace SignsOfAI.Core.Tests;
6+
7+
/// <summary>
8+
/// The half of the conversation that was never meant to be in the document.
9+
///
10+
/// Every other rule in this project is a judgement about style, and a formal writer can lose to any
11+
/// of them. These are not that. "I hope this helps" at the foot of an essay is not a register the
12+
/// student chose; it is an assistant's closing line, pasted in with the answer — the same kind of
13+
/// claim the character scanner makes, about where the file has been rather than about who is
14+
/// talented.
15+
///
16+
/// That is also why this batch was admissible when most of the pattern set it came from was not.
17+
/// The rules were screened against the calibration corpus first — 249,455 words of English and
18+
/// 39,712 of Spanish, all published before generative models existed — and every one of them scored
19+
/// zero. Re-running the calibration afterwards left the published false-positive rate untouched,
20+
/// which is the point: they cost nothing to carry.
21+
///
22+
/// The mine they came from is amanmaqsood/prose-humanizer (MIT). Twelve of its other candidates
23+
/// were rejected here by the same screen: underpin, optimize, elucidate, paradigm, exemplify and
24+
/// illuminate are ordinary research English, and each appears in three to six of those ninety texts.
25+
/// </summary>
26+
public class ChatResidueTests
27+
{
28+
private readonly AiWritingAnalyzer _a = new();
29+
30+
private bool Has(string text, string lang, string ruleId) =>
31+
_a.Analyze(text, lang).Findings.Any(f => f.RuleId == ruleId);
32+
33+
[Theory]
34+
[InlineData("As an AI language model, I should note that the figures are indicative.", "chat.model-self-reference")]
35+
[InlineData("As of my last training update, the tidal survey had not been repeated.", "chat.training-cutoff")]
36+
[InlineData("The wall was built in 1971. I hope this helps.", "chat.signoff")]
37+
[InlineData("Would you like me to expand the section on sediment transport?", "chat.signoff")]
38+
[InlineData("I cannot browse the internet, so the citation below is from memory.", "chat.capability-disclaimer")]
39+
[InlineData("Here is the revised version of your essay on coastal erosion.", "chat.answer-preamble")]
40+
[InlineData("Certainly! The coastline retreated by nine metres.", "chat.eager-opener")]
41+
public void Flags_the_assistants_own_turn_en(string text, string ruleId) =>
42+
Assert.True(Has(text, "en", ruleId), ruleId);
43+
44+
[Theory]
45+
[InlineData("Como modelo de lenguaje, debo señalar que las cifras son indicativas.", "chat.model-self-reference")]
46+
[InlineData("Hasta mi última actualización, el estudio no se había repetido.", "chat.training-cutoff")]
47+
[InlineData("El muro se construyó en 1971. Espero que esto te ayude.", "chat.signoff")]
48+
[InlineData("¿Quieres que amplíe el apartado sobre el transporte de sedimentos?", "chat.signoff")]
49+
[InlineData("No tengo acceso a información en tiempo real sobre las mareas.", "chat.capability-disclaimer")]
50+
[InlineData("Aquí tienes la versión reescrita de tu ensayo sobre la erosión costera.", "chat.answer-preamble")]
51+
[InlineData("¡Por supuesto! La costa retrocedió nueve metros.", "chat.eager-opener")]
52+
public void Flags_the_assistants_own_turn_es(string text, string ruleId) =>
53+
Assert.True(Has(text, "es", ruleId), ruleId);
54+
55+
/// <summary>
56+
/// The regexes have to leave ordinary writing alone, and two of them are close to sentences a
57+
/// person really writes. A tutor's own feedback says "let me know" and a historian writes "of
58+
/// course" mid-sentence; neither is an assistant handing back an answer.
59+
/// </summary>
60+
[Theory]
61+
[InlineData("Let me know when the survey is finished and I will read it.", "chat.signoff")]
62+
[InlineData("The wall was, of course, built long before the survey began.", "chat.eager-opener")]
63+
[InlineData("I hope the council publishes the survey before the winter.", "chat.signoff")]
64+
[InlineData("Here is the revised timetable the committee agreed on Tuesday.", "chat.answer-preamble")]
65+
public void Leaves_a_person_writing_to_a_person_alone_en(string text, string ruleId) =>
66+
Assert.False(Has(text, "en", ruleId), ruleId);
67+
68+
[Theory]
69+
[InlineData("Avísame cuando termine el estudio y lo leo.", "chat.signoff")]
70+
[InlineData("El muro, por supuesto, se construyó mucho antes del estudio.", "chat.eager-opener")]
71+
[InlineData("Aquí tienes el calendario que acordó la comisión el martes.", "chat.answer-preamble")]
72+
public void Leaves_a_person_writing_to_a_person_alone_es(string text, string ruleId) =>
73+
Assert.False(Has(text, "es", ruleId), ruleId);
74+
75+
/// <summary>
76+
/// A rule that fires on nothing measured is a rule with no measured human rate, and the pack
77+
/// must keep saying so rather than inventing one. See <c>PatternRule.HumanRatePer1000</c>: an
78+
/// absent rate means "never observed", which is what these are, and a rate of zero would be a
79+
/// different and much stronger claim.
80+
/// </summary>
81+
[Theory]
82+
[InlineData("en")]
83+
[InlineData("es")]
84+
public void The_new_rules_claim_no_human_rate(string lang)
85+
{
86+
var pack = SignsOfAI.Core.Rules.RulePackLoader.Load(lang);
87+
var chat = pack.Patterns.Where(p => p.Id.StartsWith("chat.")).ToList();
88+
89+
Assert.Equal(6, chat.Count);
90+
Assert.All(chat, rule => Assert.Null(rule.HumanRatePer1000));
91+
Assert.All(chat, rule => Assert.False(string.IsNullOrWhiteSpace(rule.Evidence), rule.Id));
92+
}
93+
}

0 commit comments

Comments
 (0)