Compare commits
2 Commits
f2d1963c61
...
3143568df0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3143568df0 | |||
| da1aa8bb2a |
@@ -10,6 +10,84 @@ public sealed class JobCvMatchServiceTests
|
||||
private static Dictionary<string, string> Sections(params (string Name, string Text)[] items)
|
||||
=> items.ToDictionary(i => i.Name, i => i.Text, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static IEnumerable<object[]> QualityFixtures()
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
"Norwegian",
|
||||
"Senior systemutvikler",
|
||||
"Vi ser etter deg som har erfaring med C# og ASP.NET Core. Du vil designe skalerbare distribuerte systemer. Gode samarbeidsevner er en fordel.",
|
||||
new[] { "C#", "ASP.NET Core", "designe skalerbare distribuerte systemer" },
|
||||
new[] { "med", "til", "for", "som", "erfaring", "ser" },
|
||||
};
|
||||
yield return new object[]
|
||||
{
|
||||
"English",
|
||||
"Cloud platform engineer",
|
||||
"We need a candidate with experience in AWS and Terraform. You will lead incident response and operate distributed systems.",
|
||||
new[] { "AWS", "Terraform", "incident response", "operate distributed systems" },
|
||||
new[] { "the", "with", "experience", "candidate", "will" },
|
||||
};
|
||||
yield return new object[]
|
||||
{
|
||||
"Mixed Norwegian and English",
|
||||
"DevOps-utvikler",
|
||||
"Du vil jobbe med Node.js og CI/CD. Work closely with Azure DevOps and cross-functional product teams.",
|
||||
new[] { "Node.js", "CI/CD", "Azure DevOps", "Collaboration" },
|
||||
new[] { "og", "med", "with", "teams" },
|
||||
};
|
||||
yield return new object[]
|
||||
{
|
||||
"Short",
|
||||
"Data developer",
|
||||
"Python and SQL.",
|
||||
new[] { "Python", "SQL" },
|
||||
new[] { "and" },
|
||||
};
|
||||
yield return new object[]
|
||||
{
|
||||
"Noisy HTML",
|
||||
"Frontend developer",
|
||||
"<nav>Home Jobs Login</nav><script>trackingCookie('React')</script><main>Build accessible web applications with React and TypeScript.</main><footer>Cookie settings Privacy Terms</footer>",
|
||||
new[] { "React", "TypeScript", "Build accessible web applications" },
|
||||
new[] { "home", "jobs", "login", "cookie", "settings", "privacy", "terms", "trackingcookie" },
|
||||
};
|
||||
yield return new object[]
|
||||
{
|
||||
"Technology heavy",
|
||||
"Platform developer",
|
||||
"C++, C#, .NET, ASP.NET Core, Node.js, CI/CD, Azure DevOps and Kubernetes.",
|
||||
new[] { "C++", "C#", ".NET", "ASP.NET Core", "Node.js", "CI/CD", "Azure DevOps", "Kubernetes" },
|
||||
new[] { "and" },
|
||||
};
|
||||
yield return new object[]
|
||||
{
|
||||
"Repeated recruitment filler",
|
||||
"Software engineer",
|
||||
"Exciting opportunity for a passionate candidate. Great opportunity, strong experience required. We offer an exciting dynamic environment. Apply now. Build services using domain-driven design and Docker.",
|
||||
new[] { "domain-driven design", "Docker" },
|
||||
new[] { "exciting", "opportunity", "passionate", "candidate", "experience", "environment", "apply" },
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(QualityFixtures))]
|
||||
public void Quality_fixtures_keep_useful_terms_and_suppress_noise(
|
||||
string name,
|
||||
string title,
|
||||
string description,
|
||||
string[] expected,
|
||||
string[] excluded)
|
||||
{
|
||||
var result = _service.Evaluate(title, description, Sections(("Skills", "synthetic profile text")));
|
||||
var terms = result.MatchedKeywords.Concat(result.MissingKeywords).ToList();
|
||||
|
||||
foreach (var term in expected)
|
||||
Assert.True(terms.Contains(term, StringComparer.OrdinalIgnoreCase), $"{name}: expected '{term}' in [{string.Join(", ", terms)}]");
|
||||
foreach (var term in excluded)
|
||||
Assert.False(terms.Contains(term, StringComparer.OrdinalIgnoreCase), $"{name}: did not expect '{term}' in [{string.Join(", ", terms)}]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Strong_overlap_scores_high_and_lists_matched_keywords()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using JobTrackerApi.Services.JobImport;
|
||||
@@ -40,11 +41,15 @@ namespace JobTrackerApi.Services
|
||||
private const int TitleBonus = 2;
|
||||
private const int MaxKeywords = 28;
|
||||
|
||||
private static readonly Regex TokenPattern = new(@"[a-z0-9][a-z0-9+.#-]*", RegexOptions.Compiled);
|
||||
private static readonly Regex TokenPattern = new(@"[\p{L}\p{N}][\p{L}\p{N}+.#/-]*", RegexOptions.Compiled);
|
||||
private static readonly Regex HtmlBlockPattern = new(@"<(script|style|nav|header|footer)[^>]*>.*?</\1>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
|
||||
private static readonly Regex HtmlTagPattern = new(@"<[^>]+>", RegexOptions.Compiled);
|
||||
private static readonly Regex SegmentPattern = new(@"[\r\n,;:!?\u2022]+|(?<=[.!?])\s+", RegexOptions.Compiled);
|
||||
|
||||
private static readonly HashSet<string> StopWords = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"the", "and", "for", "with", "you", "your", "our", "are", "will", "have", "has", "that",
|
||||
"the", "a", "an", "and", "or", "of", "to", "in", "on", "as", "is", "be", "if", "it",
|
||||
"we", "us", "for", "with", "you", "your", "our", "are", "will", "have", "has", "that",
|
||||
"this", "from", "not", "but", "all", "can", "who", "how", "why", "what", "when", "who",
|
||||
"job", "role", "work", "working", "team", "teams", "company", "years", "year", "experience",
|
||||
"experienced", "skills", "skill", "ability", "able", "strong", "good", "great", "excellent",
|
||||
@@ -64,6 +69,22 @@ namespace JobTrackerApi.Services
|
||||
"senior", "junior", "lead", "principal", "mid", "staff", "engineer", "engineers",
|
||||
"engineering", "manager", "specialist", "analyst", "consultant", "administrator",
|
||||
"coordinator", "associate", "intern", "officer", "director", "professional",
|
||||
// Norwegian function words and generic recruitment language. These are deliberately
|
||||
// language-wide categories rather than the handful of examples that exposed the bug.
|
||||
"og", "i", "det", "at", "en", "et", "den", "til", "er", "som", "på", "de", "med",
|
||||
"av", "ikke", "der", "så", "var", "seg", "men", "har", "om", "vi", "ha", "hadde",
|
||||
"hun", "han", "nå", "da", "ved", "fra", "du", "ut", "sin", "dem", "oss", "opp",
|
||||
"man", "kan", "hans", "hvor", "eller", "hva", "skal", "selv", "her", "alle", "vil",
|
||||
"bli", "ble", "blitt", "kunne", "inn", "når", "være", "noen", "noe", "ville", "dere",
|
||||
"deres", "kun", "etter", "ned", "skulle", "denne", "disse", "for", "deg", "sine", "sitt",
|
||||
"mot", "uten", "hvordan", "ingen", "din", "ditt", "blir", "samme", "hvilken", "hvilke",
|
||||
"erfaring", "erfaringer", "kvalifikasjoner", "arbeidsoppgaver", "stilling", "stillingen",
|
||||
"søker", "ser", "ønsker", "mulighet", "spennende", "arbeidsmiljø", "selskap", "bedrift", "kandidat",
|
||||
"relevant", "fordel", "gode", "dyktig", "sammen",
|
||||
// Common source-page chrome and consent text must never become tailoring advice.
|
||||
"cookie", "cookies", "privacy", "terms", "conditions", "menu", "home", "login", "contact",
|
||||
"website", "settings", "navigation", "jobs", "apply", "application", "share", "save", "accept",
|
||||
"reject", "consent", "exciting", "passionate", "dynamic", "innovative", "motivated",
|
||||
};
|
||||
|
||||
public JobCvMatchResult Evaluate(string jobTitle, string jobText, IReadOnlyDictionary<string, string> cvSections)
|
||||
@@ -134,7 +155,8 @@ namespace JobTrackerApi.Services
|
||||
|
||||
private static List<MatchKeyword> BuildKeywords(string jobTitle, string jobText, HashSet<string> titleTokens)
|
||||
{
|
||||
var combined = $"{jobTitle}\n{jobText}";
|
||||
var cleanedJobText = CleanSourceText(jobText);
|
||||
var combined = $"{jobTitle}\n{cleanedJobText}";
|
||||
var byKey = new Dictionary<string, MatchKeyword>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// 1) Curated skill tags: high-signal, canonical spelling.
|
||||
@@ -144,11 +166,22 @@ namespace JobTrackerApi.Services
|
||||
byKey[tag] = new MatchKeyword(tag, CuratedTagWeight + (inTitle ? TitleBonus : 0), inTitle, false, IsCuratedTag: true);
|
||||
}
|
||||
|
||||
// 2) Salient posting terms: frequency-ranked content words from the description.
|
||||
var frequencies = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var token in Tokenize(jobText))
|
||||
// 2) Important multi-word terms. Stop words break phrases, so "erfaring med ASP.NET
|
||||
// Core" keeps the technology but never emits "erfaring" as advice.
|
||||
var phraseTokens = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var phrase in ExtractPhrases(cleanedJobText).Take(8))
|
||||
{
|
||||
if (token.Length is < 3 or > 64 || StopWords.Contains(token) || IsNumeric(token)) continue;
|
||||
if (byKey.ContainsKey(phrase)) continue;
|
||||
var inTitle = TitleContains(jobTitle, phrase);
|
||||
byKey[phrase] = new MatchKeyword(phrase, 2 + (inTitle ? TitleBonus : 0), inTitle, false);
|
||||
foreach (var token in Tokenize(phrase)) phraseTokens.Add(token);
|
||||
}
|
||||
|
||||
// 3) Salient posting terms: frequency-ranked content words from the cleaned description.
|
||||
var frequencies = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var token in Tokenize(cleanedJobText))
|
||||
{
|
||||
if (token.Length is < 3 or > 64 || StopWords.Contains(token) || phraseTokens.Contains(token) || IsNumeric(token)) continue;
|
||||
frequencies[token] = frequencies.TryGetValue(token, out var c) ? c + 1 : 1;
|
||||
}
|
||||
|
||||
@@ -174,6 +207,55 @@ namespace JobTrackerApi.Services
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ExtractPhrases(string text)
|
||||
{
|
||||
var candidates = new Dictionary<string, (string Display, int Count, int First)>(StringComparer.OrdinalIgnoreCase);
|
||||
var order = 0;
|
||||
|
||||
foreach (var segment in SegmentPattern.Split(text))
|
||||
{
|
||||
var run = new List<string>();
|
||||
foreach (Match match in TokenPattern.Matches(segment))
|
||||
{
|
||||
var display = TrimToken(match.Value);
|
||||
var normalized = display.ToLowerInvariant();
|
||||
if (display.Length == 0 || StopWords.Contains(normalized) || IsNumeric(normalized))
|
||||
{
|
||||
AddRun(run);
|
||||
run.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
run.Add(display);
|
||||
}
|
||||
}
|
||||
AddRun(run);
|
||||
}
|
||||
|
||||
return candidates.Values
|
||||
.OrderByDescending(candidate => candidate.Count)
|
||||
.ThenByDescending(candidate => Tokenize(candidate.Display).Count())
|
||||
.ThenBy(candidate => candidate.First)
|
||||
.Select(candidate => candidate.Display);
|
||||
|
||||
void AddRun(List<string> run)
|
||||
{
|
||||
if (run.Count < 2) return;
|
||||
AddCandidate(run.Count <= 4 ? run : run.Take(4).ToList());
|
||||
if (run.Count > 4) AddCandidate(run.TakeLast(4).ToList());
|
||||
}
|
||||
|
||||
void AddCandidate(List<string> selected)
|
||||
{
|
||||
var display = string.Join(" ", selected);
|
||||
var key = display.ToLowerInvariant();
|
||||
if (candidates.TryGetValue(key, out var existing))
|
||||
candidates[key] = (existing.Display, existing.Count + 1, existing.First);
|
||||
else
|
||||
candidates[key] = (display, 1, order++);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TitleContains(string title, string phrase)
|
||||
=> Normalize(title).Contains(Normalize(phrase), StringComparison.Ordinal);
|
||||
|
||||
@@ -199,10 +281,23 @@ namespace JobTrackerApi.Services
|
||||
if (string.IsNullOrWhiteSpace(text)) yield break;
|
||||
foreach (Match m in TokenPattern.Matches(text.ToLowerInvariant()))
|
||||
{
|
||||
yield return m.Value.Trim('-', '.', '+', '#');
|
||||
var token = TrimToken(m.Value);
|
||||
if (token.Length > 0) yield return token;
|
||||
}
|
||||
}
|
||||
|
||||
private static string TrimToken(string token) => token.Trim('-', '.', '/');
|
||||
|
||||
private static string CleanSourceText(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return string.Empty;
|
||||
var withoutBlocks = HtmlBlockPattern.Replace(text, " ");
|
||||
var withoutTags = HtmlTagPattern.Replace(withoutBlocks, "\n");
|
||||
var decoded = WebUtility.HtmlDecode(withoutTags).Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n');
|
||||
var withoutHorizontalRuns = Regex.Replace(decoded, @"[^\S\r\n]+", " ");
|
||||
return Regex.Replace(withoutHorizontalRuns, @"\n{2,}", "\n").Trim();
|
||||
}
|
||||
|
||||
private static bool IsNumeric(string token)
|
||||
=> token.All(c => char.IsDigit(c) || c is '.' or '-' or '+');
|
||||
|
||||
|
||||
@@ -12,18 +12,28 @@ public static class SkillTagger
|
||||
// Symbol skills need punctuation-tolerant boundaries: \b fails next to '#'/'.'
|
||||
// (both non-word chars), which previously left "C#," and ".NET," undetected.
|
||||
("C#", new Regex(@"(?<![A-Za-z0-9#])C#|\bc[-\s]?sharp\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 6),
|
||||
(".NET", new Regex(@"(?<![A-Za-z0-9.])\.NET\b|\bASP\.NET\b|\bDOTNET\b|\bEntity Framework\b|\bEF Core\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 6),
|
||||
("C++", new Regex(@"(?<![A-Za-z0-9+])C\+\+(?!\+)|\bcpp\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 6),
|
||||
("ASP.NET Core", new Regex(@"\bASP\.NET\s+Core\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 6),
|
||||
("Entity Framework Core", new Regex(@"\bEntity Framework(?: Core)?\b|\bEF Core\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 6),
|
||||
(".NET", new Regex(@"(?<![A-Za-z0-9.])\.NET\b|\bDOTNET\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 6),
|
||||
("Python", new Regex(@"\bPython\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 6),
|
||||
("Java", new Regex(@"\bJava\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Go", new Regex(@"\bGolang\b|\bGo\s+(?:programming|language|developer|engineer)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("JavaScript", new Regex(@"\bJavaScript\b|\bJS\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("TypeScript", new Regex(@"\bTypeScript\b|\bTS\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("React", new Regex(@"\bReact\b|\bReact\.js\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Node.js", new Regex(@"\bNode\b|\bNode\.js\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Next.js", new Regex(@"\bNext\.js\b|\bNextJS\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("SQL", new Regex(@"\bSQL\b|\bPostgreSQL\b|\bMySQL\b|\bSQLite\b|\bMS\s*SQL\b|\bT-?SQL\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Docker", new Regex(@"\bDocker\b|\bcontainers?\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Kubernetes", new Regex(@"\bKubernetes\b|\bK8s\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Azure", new Regex(@"\bAzure\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Terraform", new Regex(@"\bTerraform\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Azure DevOps", new Regex(@"\bAzure\s+DevOps\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Azure", new Regex(@"\bAzure\b(?!\s+DevOps)", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("AWS", new Regex(@"\bAWS\b|\bAmazon Web Services\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Kafka", new Regex(@"\b(?:Apache\s+)?Kafka\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Redis", new Regex(@"\bRedis\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("Linux", new Regex(@"\bLinux\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 5),
|
||||
("CI/CD", new Regex(@"\bCI/CD\b|continuous integration|continuous delivery|continuous deployment", RegexOptions.IgnoreCase | RegexOptions.Compiled), 4),
|
||||
("REST APIs", new Regex(@"\bREST\b|RESTful|API development|web services", RegexOptions.IgnoreCase | RegexOptions.Compiled), 4),
|
||||
("GraphQL", new Regex(@"\bGraphQL\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), 4),
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# QA-001 evidence index
|
||||
|
||||
Synthetic/local evidence only. No provider calls, private job descriptions, credentials or personal data were used.
|
||||
|
||||
- Implementation and limits: `docs/verification/qa-001-job-term-quality.md`
|
||||
- Commands/results: `docs/audits/verification-log.md` V-114–V-116
|
||||
- Seven deterministic fixtures: `JobTrackerApi.Tests/JobCvMatchServiceTests.cs`
|
||||
- UI label regression: `job-tracker-ui/src/match-score-panel.test.tsx`
|
||||
- Browser screenshots: not captured; required presentation checks remain explicit in the master plan.
|
||||
@@ -145,3 +145,6 @@ Output was reduced to filenames and commit counts. The token artifact appears un
|
||||
| V-111 | Complete trace of `themePrefs`, auth user-key transitions, App provider/router lifecycle, Settings selector, Next layout/bootstrap and existing tests | Repository root | Reproduce UX-002 precedence and delayed switch path | PASS — login stored the resolved user key without a theme notification, so anonymous mode remained until refresh; provider key/router dependency remounted app state; no cross-tab listener or pre-paint bootstrap existed | Confirmed source execution path; no backend/identity behavior implicated | Application-related |
|
||||
| V-112 | `npm.cmd test -- --runInBand --forceExit src/theme-state.test.tsx`; full `npm.cmd test -- --runInBand --forceExit`; `npm.cmd run build`; `git diff --check` | `job-tracker-ui` / repository root | Verify precedence, login/logout events, System behavior, pre-paint bootstrap, in-place provider state, regressions and TypeScript | PASS — focused 6/6; full 48/48 suites and 172/172 tests; production build and patch hygiene pass | Initial build found unsupported `noSsr` prop and was corrected; Jest retains existing force-exit/open-handle notice | Application issue corrected before commit |
|
||||
| V-113 | In-app browser Settings: Light → Dashboard → refresh; Dark; System; second-tab Dark → Light sync; 375/768/1440 measurements/screenshots; console inspection | Local frontend | Verify actual theme transitions, persistence, cross-tab behavior and responsive access | PASS/PARTIAL — expected scheme at every transition, first tab updated without reload, no final console warnings/errors, no horizontal overflow after scrollable Settings tabs | Authenticated two-user, host OS preference flip and production not run; transient HMR messages occurred while editing and clean build passes | Environmental/deployment limitation |
|
||||
| V-114 | Complete trace of import HTML cleanup/language/tagging, deterministic matcher, analysis endpoint/UI, match endpoint/UI, learning-item sync and storage/cache behavior | Repository root | Reproduce QA-001 low-information term path and version implications | PASS — visible terms come from English-only single-token ranking in `JobCvMatchService`; imported HTML is cleaned earlier but manual text may not be; result is on-demand and has no historical analysis cache; AI is not involved | Confirmed source execution path | Application-related |
|
||||
| V-115 | `dotnet test ... --filter "...JobCvMatchServiceTests|...ApplicationIntelligenceTests|...JobApplicationsEndpointBehaviorTests|...ApplicationChecklist"`; `npm.cmd test ... match-score-panel.test.tsx application-intelligence.test.tsx`; final matcher/UI focused rerun | Repository root / `job-tracker-ui` | Verify seven required fixtures, affected endpoints/learning lifecycle and honest label | PASS — affected backend 54/54; affected UI 13/13; final matcher 15/15 and label 3/3 | Synthetic job/CV text only; no model/provider/browser | N/A |
|
||||
| V-116 | Full `dotnet test ... --no-restore`; full `npm.cmd test -- --runInBand --forceExit`; `npm.cmd run build`; `git diff --check` | Repository root / `job-tracker-ui` | QA-001 full regression, TypeScript/build and patch hygiene | PASS — backend 601/601; frontend 48/48 suites and 172/172 tests; production build; no whitespace errors | Jest existing force-exit/open-handle notice; browser/production presentation not run | Environmental/deployment limitation |
|
||||
|
||||
@@ -45,5 +45,6 @@ This is the rolling action-level evidence index. `PASS (automated/runtime)` is n
|
||||
| CV processing | retry provenance, durable refresh state and cancel/retry controls | PASS (backend + component tests) | BLOCKED | NOT RUN | `ai-004-cv-processing-queue.md` |
|
||||
| Authentication UI | unified username/password and provider alternatives; invalid/cancel/return behavior | PASS (components; mocked providers) | PARTIAL — local dark-theme form at 375/768/1440 | NOT RUN | `ux-001-unified-authentication.md` |
|
||||
| Theme state | Light/Dark/System precedence, login/logout scope, refresh/navigation and two-tab synchronization | PASS (state/provider/bootstrap tests) | PASS/PARTIAL — anonymous local browser at 375/768/1440 | NOT RUN | `ux-002-deterministic-theme-state.md` |
|
||||
| Job match terms | bilingual/noisy/short/technology-heavy/filler extraction and honest labels | PASS (seven fixtures + component tests) | NOT RUN | NOT RUN | `qa-001-job-term-quality.md` |
|
||||
|
||||
Remaining product actions are `NOT STARTED` in the master plan and will be added as their work packages enter verification. The in-app browser is available for local UI checks; configured/real-provider and production access are not documented/configured.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# QA-001 job-analysis and important-term quality
|
||||
|
||||
Updated: 2026-08-09
|
||||
|
||||
Status: `IMPLEMENTED — NOT VERIFIED`. Deterministic fixtures, backend/frontend regressions and production build pass. Browser and production presentation checks remain.
|
||||
|
||||
## Revalidated execution path
|
||||
|
||||
- URL imports already convert structured job-posting HTML to text and run lightweight Norwegian/English language detection plus `SkillTagger`. Manual descriptions and notes can still contain HTML/noise.
|
||||
- The visible problematic terms originate in `JobCvMatchService`: curated skills are combined with frequency-ranked single tokens from description, translated description and notes. Its stop list was English-only and did not clean raw HTML at this boundary.
|
||||
- The separate application-analysis endpoint exposes only curated tags in its `Keywords` field, and the current Analysis UI does not render that field. No AI prompt produces the deterministic match terms.
|
||||
- Match output is recomputed on every request; there is no stored analysis/result row or version to migrate. Missing-term learning items are derived through `SyncLearningRecommendationsAsync`, which preserves user-completed/dismissed decisions and auto-completes only obsolete pending generated items.
|
||||
|
||||
## Implemented contract
|
||||
|
||||
- The shared matcher removes script/style/navigation/header/footer blocks, remaining tags and decoded source chrome before analysis while preserving line boundaries.
|
||||
- English and Norwegian function words, generic recruitment filler and common consent/navigation text are suppressed by category, not only by the reported examples.
|
||||
- Stop words split bounded phrase runs. Useful two-to-four-word responsibility/domain phrases are ranked ahead of remaining single terms; tokens already represented by a phrase are not repeated as isolated advice.
|
||||
- Curated, canonical tags now preserve C++, ASP.NET Core, Entity Framework Core, Go/Golang context, Next.js, Terraform, Azure DevOps, Kafka, Redis and Linux. Existing C#, .NET, Node.js and CI/CD punctuation remains intact.
|
||||
- The UI says “important terms” rather than implying opaque SEO-style keywords, with equivalent Norwegian copy.
|
||||
- Deterministic processing remains local and free; no model or external provider was added.
|
||||
|
||||
## Verification
|
||||
|
||||
- Seven required fixtures pass: Norwegian, English, mixed, short, noisy HTML, technology-heavy and repeated recruitment filler.
|
||||
- Final focused matcher: 15/15. Wider affected backend set: 54/54.
|
||||
- Full backend: 601/601.
|
||||
- Focused match/analysis UI: 13/13; final label test 3/3.
|
||||
- Full frontend: 48/48 suites and 172/172 tests.
|
||||
- Production frontend build/TypeScript and `git diff --check`: pass.
|
||||
|
||||
## Version and regeneration behavior
|
||||
|
||||
There are no historical analysis blobs to silently rewrite. The next GET recomputes from current job/CV text. When the missing-term set changes, existing sync logic creates new generated learning items, auto-completes obsolete pending generated items, and leaves explicit user decisions intact. No schema or data migration is required.
|
||||
|
||||
## Remaining gates
|
||||
|
||||
- The browser session had already been finalized after UX-002; QA-001 label/chip presentation, empty/error/long Norwegian content and 375/768/1440 Light/Dark browser checks were not run in this package.
|
||||
- Synthetic production comparison and rollout monitoring remain unavailable without deployment access.
|
||||
- The curated vocabulary is intentionally bounded. New technologies should be added with representative false-positive tests rather than learned from private job data.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Evidence index: `docs/audits/evidence/qa-001/README.md`
|
||||
- Commands/results: `docs/audits/verification-log.md` V-114–V-116
|
||||
- Backend fixtures: `JobTrackerApi.Tests/JobCvMatchServiceTests.cs`
|
||||
- Frontend label coverage: `job-tracker-ui/src/match-score-panel.test.tsx`
|
||||
- Implementation commit: `da1aa8b`
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert `da1aa8b`. No database downgrade, dependency rollback or cache purge is required. Existing generated learning decisions remain in history; the next request will derive the older term set again.
|
||||
@@ -329,3 +329,13 @@
|
||||
- **Consequences:** users without a saved scoped choice inherit the explicit anonymous choice, otherwise the documented default is System. Explicit Light/Dark ignore OS changes. The pre-paint script must remain behaviorally aligned with `themePrefs`; tests cover both.
|
||||
- **User approval required:** No; this directly implements the approved UX-002 contract without schema, dependency or production changes.
|
||||
- **Reversible:** Yes. Revert `11734ee`; existing preference values remain unchanged and readable.
|
||||
|
||||
## DEC-034 — Important-term quality stays deterministic and on demand
|
||||
|
||||
- **Date:** 2026-08-09
|
||||
- **Decision:** Harden the existing shared `JobCvMatchService` rather than add a model or stored analysis version. Clean HTML/chrome at the matcher boundary, use bilingual/general filler categories, rank bounded phrase runs before singleton terms, and extend the canonical skill vocabulary for punctuation-sensitive technologies. Rename UI output to “important terms.”
|
||||
- **Reason/evidence:** the complete path proved the reported Norwegian words came from English-only deterministic token ranking. Imported descriptions may be clean, but manual descriptions/notes reach the same matcher. Results are recomputed and the generated learning sync already preserves user decisions, so a cache/schema migration would solve a nonexistent storage problem.
|
||||
- **Alternatives considered:** hardcode the five examples; call Ollama for keywords; add a result table/version column; filter only in the UI; silently delete learning items. These are incomplete, less reliable, duplicate state, leave API consumers dirty or discard user history.
|
||||
- **Consequences:** term changes appear on the next request; obsolete pending generated learning items auto-complete under existing behavior while done/dismissed decisions remain. The curated vocabulary remains intentionally bounded and test-driven.
|
||||
- **User approval required:** No; this is the deterministic-first implementation explicitly required by both programmes.
|
||||
- **Reversible:** Yes. Revert `da1aa8b`; no schema, cache or provider state changes.
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
Updated: 2026-08-09
|
||||
|
||||
- **Overall programme status:** Active. Six packages are locally verified; sixteen packages through UX-002 are implemented with automated/runtime evidence but blocked from applicable provider/production gates; QA-001 is in progress.
|
||||
- **Current work package:** `QA-001` — job-analysis and keyword quality (`IN PROGRESS`); source requirements and the deterministic analysis/cache pipeline are the next trace.
|
||||
- **Overall programme status:** Active. Six packages are locally verified; seventeen packages through QA-001 are implemented with automated/runtime evidence but blocked from applicable browser/provider/production gates; CAREER-001 is in progress.
|
||||
- **Current work package:** `CAREER-001` — Career Workspace action-oriented redesign (`IN PROGRESS`); requirements and current first/returning/incomplete/processing/error hierarchy are the next trace.
|
||||
- **Completed work packages:** None are `DONE`; all repository security/AI packages still have applicable browser, provider and/or production gates.
|
||||
- **Locally verified work:** SEC-001, SEC-002, SEC-003, SEC-005A, CORE-001 and PROD-002 (`VERIFIED LOCALLY`).
|
||||
- **Implemented, verification incomplete:** SEC-004, SEC-005B, SEC-008, CORE-002, BG-001, OPS-001A/B/C, POL-001/002, AI-001/002/003/004 and UX-001/002 (`IMPLEMENTED — NOT VERIFIED`). UX-002 fixes login/logout preference scope, startup paint, route remount and cross-tab synchronization; automated/build and local Light/Dark/System browser gates pass.
|
||||
- **Implemented, verification incomplete:** SEC-004, SEC-005B, SEC-008, CORE-002, BG-001, OPS-001A/B/C, POL-001/002, AI-001/002/003/004, UX-001/002 and QA-001 (`IMPLEMENTED — NOT VERIFIED`). QA-001 now cleans HTML/chrome, filters bilingual filler, preserves curated punctuation/phrases and uses honest labels; all automated/build gates pass.
|
||||
- **Production-verified work:** None.
|
||||
- **Blocked work:** SEC-006 requires explicit internet/package-index permission; PROD-001/003/004 and REL-001 require documented production access and unfinished dependencies. Real provider, SMTP/MariaDB and production environments are unavailable; the in-app browser is available for local UI checks.
|
||||
- **Deferred work:** None. Conditional multi-replica coordination, model deletion, realtime operation delivery and unrelated production changes remain outside current packages.
|
||||
- **Next five work packages:** QA-001 job-analysis/keyword quality; CAREER-001 Career Workspace redesign; CAREER-002 CV Builder redesign; EMAIL-001 consolidated email experience; JOBS-001 job-search/application redesign. SEC-006/007 resume after package-index permission.
|
||||
- **Status counts:** 6 `VERIFIED LOCALLY`; 16 `IMPLEMENTED — NOT VERIFIED`; 1 `IN PROGRESS`; 11 `NOT STARTED`; 5 `BLOCKED`; 0 `DONE`; 0 `DEFERRED`.
|
||||
- **Test status:** backend 594/594; AI-004 focused backend 40/40; frontend 48/48 suites and 172/172 tests, UX-002 focused 6/6, production build pass; prior sidecar 22/22. Patch check passes. UX-002 browser passed Light/Dark/System/navigation/refresh/two-tab synchronization at required widths; provider/model/MariaDB/production checks were not run.
|
||||
- **Next five work packages:** CAREER-001 Career Workspace redesign; CAREER-002 CV Builder redesign; EMAIL-001 consolidated email experience; JOBS-001 job-search/application redesign; HOME-001 homepage/Pro promotion. SEC-006/007 resume after package-index permission.
|
||||
- **Status counts:** 6 `VERIFIED LOCALLY`; 17 `IMPLEMENTED — NOT VERIFIED`; 1 `IN PROGRESS`; 10 `NOT STARTED`; 5 `BLOCKED`; 0 `DONE`; 0 `DEFERRED`.
|
||||
- **Test status:** backend 601/601; QA-001 focused matcher 15/15 and affected backend 54/54; frontend 48/48 suites and 172/172 tests, affected UI 13/13; production build and patch check pass. QA-001 browser/production presentation was not run; prior UX-002 browser and sidecar 22/22 remain green.
|
||||
- **Deployment status:** No deployment performed. No production migrations were run. AI operation worker remains disabled by default.
|
||||
- **Production status:** Unchanged and unverified. No provider/model call, model pull, external request or paid API occurred.
|
||||
- **Known regressions:** None found by automated suites. Jest still needs `--forceExit` and reports its existing open-handle notice. Direct clean EF-only SQLite migration still hits the pre-existing historical blank-chain defect before later migrations; normal startup owns reconciliation. Cross-feature monthly AI usage accounting remains a rollout gap.
|
||||
@@ -43,5 +43,6 @@ Updated: 2026-08-09
|
||||
- `docs/verification/ai-004-cv-processing-queue.md`
|
||||
- `docs/verification/ux-001-unified-authentication.md`
|
||||
- `docs/verification/ux-002-deterministic-theme-state.md`
|
||||
- `docs/verification/qa-001-job-term-quality.md`
|
||||
- `docs/verification/prod-002-ai-evaluation.md`
|
||||
- `docs/work-programmes/master-work-plan.md`
|
||||
|
||||
@@ -16,7 +16,7 @@ Allowed statuses are `NOT STARTED`, `IN PROGRESS`, `IMPLEMENTED — NOT VERIFIED
|
||||
|
||||
`DONE` requires every applicable acceptance criterion, focused and regression tests, browser/accessibility/theme/mobile checks, tenant and entitlement checks, documentation, migration/rollback evidence, and production verification. Repository-only work that still requires production is at most `VERIFIED LOCALLY`.
|
||||
|
||||
Exactly one implementation item may be `IN PROGRESS`. As of this revision it is **QA-001**.
|
||||
Exactly one implementation item may be `IN PROGRESS`. As of this revision it is **CAREER-001**.
|
||||
|
||||
## Consolidated dependency order
|
||||
|
||||
@@ -595,11 +595,11 @@ Ordering differences from the suggested list:
|
||||
- **Required tests:** seven specified Norwegian/English/mixed/short/noisy/tech/filler fixtures.
|
||||
- **Required browser verification:** result presentation/empty/error/long Norwegian text at three widths/themes.
|
||||
- **Required production verification:** synthetic analysis comparison; no silent historical rewrite.
|
||||
- **Status:** `IN PROGRESS`.
|
||||
- **Blocker:** none.
|
||||
- **Evidence:** source requirements and deterministic fixture contract are next for complete pipeline/caching trace; PROD-002 fixtures are available.
|
||||
- **Commit:** none.
|
||||
- **Remaining work:** deterministic fix before considering model help.
|
||||
- **Status:** `IMPLEMENTED — NOT VERIFIED`.
|
||||
- **Blocker:** browser presentation and production comparison remain; deterministic repository work is complete.
|
||||
- **Evidence:** `docs/verification/qa-001-job-term-quality.md`; V-114–V-116; seven required fixtures, focused matcher 15/15, affected backend 54/54, full backend 601/601, focused UI 13/13, full frontend 172/172 and build.
|
||||
- **Commit:** `da1aa8b` (`fix(match): prioritize meaningful job terms`).
|
||||
- **Remaining work:** browser empty/error/long Norwegian/three-width/theme presentation; synthetic production comparison. No stored analysis migration exists.
|
||||
|
||||
### CAREER-001 — Career Workspace action-oriented redesign
|
||||
|
||||
@@ -613,9 +613,9 @@ Ordering differences from the suggested list:
|
||||
- **Required tests:** first/returning/incomplete/processing/failure state and navigation.
|
||||
- **Required browser verification:** three widths, light/dark, keyboard/focus/loading/empty/error/Norwegian.
|
||||
- **Required production verification:** synthetic account smoke.
|
||||
- **Status:** `NOT STARTED`.
|
||||
- **Status:** `IN PROGRESS`.
|
||||
- **Blocker:** browser tooling must be available for completion.
|
||||
- **Evidence:** current architecture and source requirement.
|
||||
- **Evidence:** source requirements and current Career Workspace state hierarchy are next for complete trace; CORE-001 and AI-004 contracts are available.
|
||||
- **Commit:** none.
|
||||
- **Remaining work:** no master-profile overwrite.
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
Updated: 2026-08-09
|
||||
|
||||
- **Exact current task:** QA-001 — re-read the specified Norwegian/English quality fixtures and trace import cleanup, language/token/phrase/skill/scoring/cache/version/UI-label behavior before editing.
|
||||
- **Last completed step:** UX-002 implementation `11734ee` was committed. Deterministic user/anonymous/System precedence, pre-paint bootstrap, in-place provider switching, auth-user/cross-tab events and mobile Settings tabs pass automated/build/browser verification. Evidence/tracking are ready for their own commit and push.
|
||||
- **Files currently modified:** UX-002 verification/evidence/tracking documents and three synthetic screenshots under `docs/audits/evidence/ux-002/`. UX-002 application code is committed.
|
||||
- **Commands already run:** UX-002 complete theme/auth/provider/router/bootstrap trace; focused 6/6 and full 172/172 frontend tests; production build; diff checks; local browser Light/Dark/System/navigation/refresh/two-tab/375/768/1440 checks; implementation commit. See V-111–V-113.
|
||||
- **Test results:** backend baseline 594/594; frontend 48/48 suites and 172/172 tests; UX-002 focused 6/6; production frontend build and diff check pass. Browser theme checks and final console inspection pass; authenticated multi-user/production remain.
|
||||
- **Exact current task:** CAREER-001 — re-read Career Workspace requirements and trace first-run, completeness, CV import review, queued processing/failure and recent-document actions before editing.
|
||||
- **Last completed step:** QA-001 implementation `da1aa8b` was committed. Bilingual/chrome filtering, bounded phrases, expanded canonical technology tags and honest important-term labels pass all automated/build gates. Evidence/tracking are ready for their own commit and push.
|
||||
- **Files currently modified:** QA-001 verification/evidence/tracking documents only. QA-001 application code is committed; no screenshots were captured.
|
||||
- **Commands already run:** QA-001 complete import/language/tagger/matcher/API/UI/learning/cache trace; seven required fixtures; affected and full backend/frontend tests; production build; diff checks; implementation commit. See V-114–V-116.
|
||||
- **Test results:** backend 601/601; QA matcher 15/15; affected backend 54/54; frontend 48/48 suites and 172/172 tests; affected UI 13/13; production build and diff check pass. Browser/production QA presentation remains.
|
||||
- **Services currently running:** none started by this session. The Next development server was stopped and in-app browser tabs finalized. Pre-existing Docker services were not changed.
|
||||
- **Temporary files or processes:** none. No dependency declaration, database or private artifact changed.
|
||||
- **Production changes currently active:** none. No deployment, provider/model call, private CV access, model pull, paid service, production migration or external payload occurred.
|
||||
- **Rollback status:** UX-002 has no schema/dependency migration; revert `11734ee` while retaining existing preference keys. UX-001/AI-004 rollback records remain current. Production is unchanged.
|
||||
- **Uncommitted changes:** UX-002 evidence/master tracking documents and screenshots only; commit/push these before QA-001 application code.
|
||||
- **Known failures:** authenticated two-user/production theme smoke remains; configured/real-provider UX-001 browser checks remain; MariaDB/SMTP/production unavailable; SEC-006 upgrades require explicit internet/package-index permission; parser isolation remains SEC-007; historical clean EF-only SQLite chain needs startup reconciliation; Jest reports its existing force-exit/open-handle notice.
|
||||
- **Exact next action:** commit and push UX-002 evidence, then read `work.md:174-263`, `ollama.md:177-223`, PROD-002 fixtures and every job-analysis producer/cache/UI consumer; reproduce the seven specified quality cases before editing.
|
||||
- **Work that can continue independently:** QA-001 and subsequent repository UX packages. SEC-006/007 await package-index permission; PROD-001/003/004 await production access.
|
||||
- **Rollback status:** QA-001 has no schema/cache/dependency migration; revert `da1aa8b` to restore the prior term set/labels. Earlier rollback records remain current. Production is unchanged.
|
||||
- **Uncommitted changes:** QA-001 evidence/master tracking documents only; commit/push these before CAREER-001 application code.
|
||||
- **Known failures:** QA-001 browser/production presentation; authenticated two-user/production theme smoke; configured/real-provider UX-001; MariaDB/SMTP/production unavailable; SEC-006 needs explicit internet/package-index permission; parser isolation remains SEC-007; historical clean EF-only SQLite chain needs startup reconciliation; Jest retains its open-handle notice.
|
||||
- **Exact next action:** commit and push QA-001 evidence, then read `work.md:265-307`, Career Workspace components/tests, CORE-001 data shapes and AI-004 operation states; reproduce first/returning/incomplete/queued/review/failure paths before editing.
|
||||
- **Work that can continue independently:** CAREER-001/002 and later repository UX packages. SEC-006/007 await package-index permission; PROD-001/003/004 await production access.
|
||||
- **Decisions still required from the user:** none for synthetic repository work. Internet/package upgrades, private CV access, external/paid providers and production actions retain their explicit approval/safety gates; SEC-009 retention/legal policy remains unresolved.
|
||||
|
||||
@@ -1065,13 +1065,13 @@ export const translations = {
|
||||
matchScoreBand_Partial: "Partial match",
|
||||
matchScoreBand_Low: "Low match",
|
||||
matchScoreBand_Unknown: "Not enough signal",
|
||||
matchScoreKeywordsCovered: "{matched}/{total} keywords",
|
||||
matchScoreKeywordsCovered: "{matched}/{total} important terms",
|
||||
matchScoreNoSignal: "Add more CV detail or a fuller job description to get a reliable score.",
|
||||
matchScoreDeterministicHint: "Deterministic keyword coverage — no AI, so the score is stable and repeatable. For a written opinion on strengths and gaps, see the AI section below.",
|
||||
matchScoreMatched: "Matched keywords",
|
||||
matchScoreMissing: "Missing keywords",
|
||||
matchScoreDeterministicHint: "Deterministic important-term coverage — no AI, so the score is stable and repeatable. For a written opinion on strengths and gaps, see the AI section below.",
|
||||
matchScoreMatched: "Important terms already in your CV",
|
||||
matchScoreMissing: "Important terms from the job to review",
|
||||
matchScoreNoneYet: "No matches found yet.",
|
||||
matchScoreAllCovered: "Every keyword is covered.",
|
||||
matchScoreAllCovered: "Every important term is covered.",
|
||||
matchScoreSectionCoverage: "Where your CV covers this role",
|
||||
matchScoreLearningPath: "Learning path",
|
||||
matchScoreLearningPathHint: "Job-specific skill gaps from the deterministic match. Verify real evidence before adding a skill to your profile.",
|
||||
@@ -2195,13 +2195,13 @@ export const translations = {
|
||||
matchScoreBand_Partial: "Delvis match",
|
||||
matchScoreBand_Low: "Lav match",
|
||||
matchScoreBand_Unknown: "For lite grunnlag",
|
||||
matchScoreKeywordsCovered: "{matched}/{total} nøkkelord",
|
||||
matchScoreKeywordsCovered: "{matched}/{total} viktige begreper",
|
||||
matchScoreNoSignal: "Legg til mer CV-innhold eller en fyldigere stillingstekst for en pålitelig score.",
|
||||
matchScoreDeterministicHint: "Deterministisk nøkkelorddekning — ingen AI, så scoren er stabil og repeterbar. For en skriftlig vurdering av styrker og svakheter, se AI-seksjonen under.",
|
||||
matchScoreMatched: "Treff på nøkkelord",
|
||||
matchScoreMissing: "Manglende nøkkelord",
|
||||
matchScoreDeterministicHint: "Deterministisk dekning av viktige begreper — ingen AI, så scoren er stabil og repeterbar. For en skriftlig vurdering av styrker og svakheter, se AI-seksjonen under.",
|
||||
matchScoreMatched: "Viktige begreper i CV-en",
|
||||
matchScoreMissing: "Viktige begreper fra stillingen å vurdere",
|
||||
matchScoreNoneYet: "Ingen treff ennå.",
|
||||
matchScoreAllCovered: "Alle nøkkelord er dekket.",
|
||||
matchScoreAllCovered: "Alle viktige begreper er dekket.",
|
||||
matchScoreSectionCoverage: "Hvor CV-en dekker denne rollen",
|
||||
matchScoreLearningPath: "Læringssti",
|
||||
matchScoreLearningPathHint: "Jobbspesifikke ferdighetsgap fra den deterministiske matchen. Bekreft reell erfaring før du legger en ferdighet til profilen.",
|
||||
|
||||
@@ -76,12 +76,15 @@ afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('match score panel shows the score, matched and missing keywords', async () => {
|
||||
test('match score panel shows the score and honest important-term labels', async () => {
|
||||
renderDialog();
|
||||
|
||||
expect(await screen.findByText('82%')).toBeInTheDocument();
|
||||
expect(await screen.findByText(/strong match/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText('4/6 keywords')).toBeInTheDocument();
|
||||
expect(await screen.findByText('4/6 important terms')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Important terms already in your CV')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Important terms from the job to review')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/matched keywords/i)).not.toBeInTheDocument();
|
||||
|
||||
// Matched keyword chips
|
||||
expect(await screen.findByText('C#')).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user