feat: leave an AI finding's field empty when the model labelled neither half and nothing in the finding could be anchored in the copy, instead of defaulting it to the body and rendering a badge that sends the writer to the wrong box on the one panel whose whole purpose is saying which box to open

This commit is contained in:
Matthew Meszaros
2026-09-10 09:28:00 -07:00
parent 3a9b1747dc
commit ade18d1650
5 changed files with 42 additions and 7 deletions
@@ -810,7 +810,7 @@ At least one of the three must carry something, and the three together are cappe
|-------|-------------|
| `score` | Overall deliverability score, 0 to 100, higher is safer. Grounded on `rules`, never argued against it. |
| `verdict` | One sentence on how the email will land. |
| `findings` | Located problems, most severe first. `severity` is `high`, `warn` or `info`; `field` is `subject` or `body`; `category` is one of `trigger_word`, `tone`, `formatting`, `links`, `structure`, `authenticity`. |
| `findings` | Located problems, most severe first. `severity` is `high`, `warn` or `info`; `field` is `subject` or `body`, absent only when the model labelled neither and nothing in the finding could be anchored in the copy; `category` is one of `trigger_word`, `tone`, `formatting`, `links`, `structure`, `authenticity`. |
| `findings[].text` | The fragment quoted from your copy. Absent when the finding is about the email as a whole. |
| `suggested_subject` | A rewritten subject, or absent when the current one is fine. |
| `improvements` | Copy-level advice with no single fragment to quote. |
+1 -1
View File
@@ -29089,7 +29089,6 @@
"description": "One located problem the AI analysis found. `text` is verified against the submitted copy before it is returned, so it is never a fragment the caller did not send.",
"required": [
"severity",
"field",
"issue"
],
"properties": {
@@ -29103,6 +29102,7 @@
},
"field": {
"type": "string",
"description": "Absent when the model labelled neither half and nothing in the finding could be anchored in the copy.",
"enum": [
"subject",
"body"
+13 -4
View File
@@ -52,8 +52,9 @@ type Input struct {
type Finding struct {
// Severity is "high", "warn" or "info".
Severity string `json:"severity"`
// Field is "subject" or "body".
Field string `json:"field"`
// Field is "subject" or "body", empty when the model labelled neither and
// nothing in the finding could be anchored in the copy.
Field string `json:"field,omitempty"`
// Text is the exact fragment quoted from the copy, empty when the finding
// is about the email as a whole.
Text string `json:"text,omitempty"`
@@ -407,11 +408,19 @@ func severity(s string) string {
}
}
// field reads the half the model named. Anything it did not clearly label comes
// back empty rather than guessed: verify fills it in from the quote when there
// is one, and a badge naming the wrong box on a finding nothing anchored is the
// exact error this whole panel exists to avoid.
func field(s string) string {
if strings.Contains(strings.ToLower(strings.TrimSpace(s)), "subject") {
switch v := strings.ToLower(strings.TrimSpace(s)); {
case strings.Contains(v, "subject"):
return warmlint.FieldSubject
case strings.Contains(v, "body"):
return warmlint.FieldBody
default:
return ""
}
return warmlint.FieldBody
}
func clamp(n int) int {
+23
View File
@@ -317,3 +317,26 @@ func TestAnalyzeAcceptsAZeroScore(t *testing.T) {
t.Errorf("score = %d, want the model's own 0", res.Score)
}
}
// A finding the model left unlabelled must not be given a half at random. With
// a quote, verify anchors it; without one, it carries no field rather than a
// badge sending the writer to the wrong box.
func TestVerifyDoesNotGuessAnUnlabelledField(t *testing.T) {
res := analyzed(t, `{
"score": 60,
"findings": [
{"severity":"warn","text":"limited time offer","issue":"Anchored, so the half is known."},
{"severity":"warn","issue":"Nothing to anchor and no label."}
]
}`, "Quick question", testBody)
if len(res.Findings) != 2 {
t.Fatalf("got %d findings: %+v", len(res.Findings), res.Findings)
}
if res.Findings[0].Field != "body" {
t.Errorf("anchored finding field = %q, want body", res.Findings[0].Field)
}
if res.Findings[1].Field != "" {
t.Errorf("unanchored, unlabelled finding field = %q, want empty", res.Findings[1].Field)
}
}
@@ -42,7 +42,10 @@ export interface ScoreTemplateRequest {
// One located problem the AI analysis found in the copy.
export interface SpamFinding {
severity: "high" | "warn" | "info";
field: TemplateField;
/** Absent when the model labelled neither half and nothing in the finding
* could be anchored in the copy, so no badge is shown rather than one
* naming the wrong box. */
field?: TemplateField;
/** The exact fragment quoted from the copy, absent when the finding is
* about the email as a whole. Verified server-side against the template,
* so it is never a sentence the writer did not write. */