Skip to content

feat(score): actionable recommendation sentence in the verdict band (#42) - #160

Merged
s-annam merged 2 commits into
mainfrom
feat/verdict-recommendation-42
Jun 24, 2026
Merged

feat(score): actionable recommendation sentence in the verdict band (#42)#160
s-annam merged 2 commits into
mainfrom
feat/verdict-recommendation-42

Conversation

@rohithgollapalli

Copy link
Copy Markdown
Collaborator

Summary

The verdict band showed a band label ("Strong" / "Getting There" / "Needs Work") and a "Biggest gap: {dimension} — {hint}" diagnostic, but never told the user what to do next. This replaces the gap line with a single actionable recommendation sentence derived deterministically from the same AnonymousAtsScore we already compute — no LLM, no new deps.

New pure getScoreRecommendation() (src/lib/score/recommendation.ts) maps the score to one sentence via a priority chain, each branch keyed to a real field:

  1. layout.scanned → "export a text-based PDF" hard blocker.
  2. layout.multiplier < 1 → names the actual layout.triggers (e.g. "multi-column layout") and cites preLayoutOverall — the dominant drag even when content scores well.
  3. else → band opener (getScoreTier) + the next step for the weakest gradable dimension (same lowest-score/max math the old gap line used), with completeness.missing / redactedDates driving the completeness copy.

VerdictHeader now renders the band label + this sentence; the per-dimension counts (e.g. 4/10 bullets carry a metric) still live on the dimension cards. Net change is a simplification (−54/+11 in the two components).

Closes #42

Test plan

  • npm run typecheck clean
  • npm run test green (730 passed; +12 new recommendation.test.ts covering every branch, both bands, plural/singular missing fields, redacted dates, and determinism)
  • eslint clean on changed files (tokens only, no hardcoded colors)
  • Manually verified in npm run dev — sentence renders under the band and varies across strong / two-column / scanned fixtures

)

Replace the verdict band's "biggest gap" diagnostic with a single actionable
next-step sentence derived from the same AnonymousAtsScore breakdown. New pure
getScoreRecommendation() maps band + weakest gradable dimension (or layout
trigger / scanned) to copy that round-trips to real fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rohithgollapalli
rohithgollapalli requested a review from s-annam June 24, 2026 15:32

@s-annam s-annam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: feat(score): actionable recommendation sentence in the verdict band (#42)

Summary

Clean, well-scoped. Pure deterministic getScoreRecommendation() keyed to real AnonymousAtsScore fields, net simplification (−54/+11 in the two components). No LLM, no new deps.

Highlights

  • Priority chain (scanned → layout penalty → weakest gradable dimension) reuses the same lowest-score/max math the old "biggest gap" line used, so the pick stays consistent.
  • Graceful trigger-phrase fallback (t.replace(/_/g, " ")) for any future trigger.
  • Thorough tests: every branch, both bands, plural/singular missing fields, redacted dates, determinism.

Verification

  • npm run typecheck clean
  • npm run test — 730 passed
  • Semantic tokens only; no hardcoded colors

Verdict

Action: APPROVE
Rationale: No blocking items. One non-blocking nit below.

* inline use — the full explanation lives in `LayoutFlagsList`. */
const TRIGGER_PHRASE: Record<string, string> = {
two_column: "multi-column layout",
fonts_unmappable: "font encoding the parser can't read",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit]: describeTriggers filters out "scanned", but branch 1 in getScoreRecommendation already short-circuits whenever layout.scanned is true — so this filter never actually drops anything in practice. Harmless defensive guard; fine to keep.

@s-annam s-annam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approve after branch update to base (no code change). Gates were green at review: typecheck clean, 730 tests passing.

@s-annam
s-annam merged commit ffe1f09 into main Jun 24, 2026
2 checks passed
@s-annam
s-annam deleted the feat/verdict-recommendation-42 branch June 24, 2026 17:29
s-annam added a commit that referenced this pull request Jun 25, 2026
…xt (#102)

* fix(jd-match): decode numeric HTML entity references in htmlToPlaintext

htmlToPlaintext only decoded a fixed set of named entities, so numeric
character references (decimal &#160;, hex &#x2013;, &#8217;, …) survived
the strip and leaked raw &#…; fragments into the JD-match passes — the
skill regex missed "you&#8217;ll" and the noun-pass acronym regex began
matching `#` fragments.

Add a decimal + hex numeric-reference decode pass after the named-entity
loop, routed through decodeCodePoint, which:
  - leaves out-of-range / lone-surrogate values as the original &#…; text
    rather than throwing or emitting U+FFFD,
  - folds 0xA0 (nbsp) to a regular space so numeric forms match the named
    &nbsp; path.

Malformed refs like &#x; never match (the digit group requires 1+) and
pass through unchanged.

Closes #93

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(jd-match): strip control-char numeric entity refs in htmlToPlaintext

Follow-up hardening on the numeric-entity decode: non-whitespace C0
control characters and DEL (&#0;, &#7;, &#8;, …) are now dropped instead
of decoded into invisible control bytes in the matched plaintext. Tab /
LF / CR stay as legitimate whitespace and are normalized downstream by
the line-collapse pass.

This closes the one gap a "return original" guard would have left —
leaking &#13; / &#10; back as raw &#…; fragments, the exact failure this
PR fixes. Stripping (not preserving) is the correct treatment for
genuine garbage control points.

Tests: drop &#0;/&#7;, and &#13;/&#10; decode without leaking.

Refs #93

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Srinivas Annam <annam@annam.org>
s-annam added a commit that referenced this pull request Jun 25, 2026
) (#160)

Replace the verdict band's "biggest gap" diagnostic with a single actionable
next-step sentence derived from the same AnonymousAtsScore breakdown. New pure
getScoreRecommendation() maps band + weakest gradable dimension (or layout
trigger / scanned) to copy that round-trips to real fields.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Srinivas Annam <annam@annam.org>
s-annam added a commit that referenced this pull request Jun 28, 2026
…xt (#102)

* fix(jd-match): decode numeric HTML entity references in htmlToPlaintext

htmlToPlaintext only decoded a fixed set of named entities, so numeric
character references (decimal &#160;, hex &#x2013;, &#8217;, …) survived
the strip and leaked raw &#…; fragments into the JD-match passes — the
skill regex missed "you&#8217;ll" and the noun-pass acronym regex began
matching `#` fragments.

Add a decimal + hex numeric-reference decode pass after the named-entity
loop, routed through decodeCodePoint, which:
  - leaves out-of-range / lone-surrogate values as the original &#…; text
    rather than throwing or emitting U+FFFD,
  - folds 0xA0 (nbsp) to a regular space so numeric forms match the named
    &nbsp; path.

Malformed refs like &#x; never match (the digit group requires 1+) and
pass through unchanged.

Closes #93

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(jd-match): strip control-char numeric entity refs in htmlToPlaintext

Follow-up hardening on the numeric-entity decode: non-whitespace C0
control characters and DEL (&#0;, &#7;, &#8;, …) are now dropped instead
of decoded into invisible control bytes in the matched plaintext. Tab /
LF / CR stay as legitimate whitespace and are normalized downstream by
the line-collapse pass.

This closes the one gap a "return original" guard would have left —
leaking &#13; / &#10; back as raw &#…; fragments, the exact failure this
PR fixes. Stripping (not preserving) is the correct treatment for
genuine garbage control points.

Tests: drop &#0;/&#7;, and &#13;/&#10; decode without leaking.

Refs #93

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Srinivas Annam <annam@annam.org>
s-annam added a commit that referenced this pull request Jun 28, 2026
) (#160)

Replace the verdict band's "biggest gap" diagnostic with a single actionable
next-step sentence derived from the same AnonymousAtsScore breakdown. New pure
getScoreRecommendation() maps band + weakest gradable dimension (or layout
trigger / scanned) to copy that round-trips to real fields.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Srinivas Annam <annam@annam.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an actionable recommendation sentence to the verdict band

2 participants