Compare commits

...

2 Commits

Author SHA1 Message Date
cesnimda 3143568df0 docs(match): record term-quality evidence
CI and Deploy / test (pull_request) Failing after 1m33s
CI and Deploy / deploy (pull_request) Has been skipped
2026-08-09 18:49:41 +02:00
cesnimda da1aa8bb2a fix(match): prioritize meaningful job terms 2026-08-09 18:47:21 +02:00
13 changed files with 308 additions and 46 deletions
@@ -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()
{
+103 -8
View File
@@ -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),
+9
View File
@@ -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-114V-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.
+3
View File
@@ -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-114V-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.
+10
View File
@@ -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.
+7 -6
View File
@@ -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`
+8 -8
View File
@@ -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-114V-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.
+10 -10
View File
@@ -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-111V-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-114V-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.
+10 -10
View File
@@ -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();