feat(score): actionable recommendation sentence in the verdict band (#42) - #160
Merged
Conversation
) 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>
s-annam
approved these changes
Jun 24, 2026
s-annam
left a comment
Contributor
There was a problem hiding this comment.
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/maxmath 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 typecheckcleannpm 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", |
Contributor
There was a problem hiding this comment.
[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
approved these changes
Jun 24, 2026
s-annam
left a comment
Contributor
There was a problem hiding this comment.
Re-approve after branch update to base (no code change). Gates were green at review: typecheck clean, 730 tests passing.
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  , hex –, ’, …) survived the strip and leaked raw &#…; fragments into the JD-match passes — the skill regex missed "you’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 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 (�, , , …) 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 / back as raw &#…; fragments, the exact failure this PR fixes. Stripping (not preserving) is the correct treatment for genuine garbage control points. Tests: drop �/, and / 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  , hex –, ’, …) survived the strip and leaked raw &#…; fragments into the JD-match passes — the skill regex missed "you’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 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 (�, , , …) 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 / back as raw &#…; fragments, the exact failure this PR fixes. Stripping (not preserving) is the correct treatment for genuine garbage control points. Tests: drop �/, and / 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
AnonymousAtsScorewe 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:layout.scanned→ "export a text-based PDF" hard blocker.layout.multiplier < 1→ names the actuallayout.triggers(e.g. "multi-column layout") and citespreLayoutOverall— the dominant drag even when content scores well.getScoreTier) + the next step for the weakest gradable dimension (same lowest-score/maxmath the old gap line used), withcompleteness.missing/redactedDatesdriving the completeness copy.VerdictHeadernow 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 typecheckcleannpm run testgreen (730 passed; +12 newrecommendation.test.tscovering every branch, both bands, plural/singular missing fields, redacted dates, and determinism)eslintclean on changed files (tokens only, no hardcoded colors)npm run dev— sentence renders under the band and varies across strong / two-column / scanned fixtures