feat(ms-ai-architect): Spor D Steg C — skill-quality SessionStart-surfacing (TDD) [skip-docs]

summarizeSkillQuality(cache) (pure) in detection-schedule.mjs: one-liner
"Skill-kvalitet: N skills < 90 % (navn)"; null when all skills meet target,
when the cache is missing (gitignored → absent in a fresh clone), or when the
shape is malformed. Reads the CACHED skill-score-report.json (from Steg B) —
never scores live (buildReport reads ~400 ref-files). Wired into
session-start-context.mjs after the course-signals block (same
existsSync/try-catch pattern). 7 new tests (kb-update 316 → 323).

[skip-docs]: README is the public end-user doc; skill-quality scoring is
maintainer-only tooling (the cache is gitignored, so the signal never surfaces
for end-users). The right doc homes ARE updated: CLAUDE.md hook table + a
caveat note (this commit), and docs/skill-quality-scoring-plan.md §3 komponent 4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 18:38:35 +02:00
commit 97ebcd737a
4 changed files with 156 additions and 1 deletions

View file

@ -256,3 +256,47 @@ export function summarizeCourses(report) {
if (changed > 0) parts.push(`${changed} endrede`);
return parts.length ? `Kurs-signaler: ${parts.join(' / ')} kurs i dekkede produkter` : null;
}
// Default skill-quality target (%). The score cache always carries its own
// `target` (buildScoreCache); this literal is only a defensive fallback for an
// older/partial cache. Mirrors TARGET in scripts/kb-eval/lib/skill-score.mjs —
// kept local so this Claude-free kb-update module never imports the kb-eval lib.
const SKILL_QUALITY_TARGET_DEFAULT = 90;
/**
* Build a compact one-liner summarizing skill-quality scores (Spor D / Steg C)
* for the SessionStart hook. Pure; tolerant of partial/missing caches. Mirrors
* summarizeSkillLifecycle / summarizeCourses.
*
* Consumes the CACHED score report (scripts/kb-eval/data/skill-score-report.json,
* produced by `score-skill.mjs --write`) it NEVER scores live: buildReport
* reads ~400 ref-files, far too expensive for a SessionStart hook. That cache is
* gitignored (derived/regenerable), so it is ABSENT in a fresh clone this MUST
* tolerate a missing/malformed cache null and never crash the hook. In
* practice the one-liner therefore surfaces only for a maintainer who has run
* `--write` locally, never for an end-user of a fresh checkout.
*
* Surfaces ONLY skills below target. Returns null when every skill meets target,
* when the cache is missing/non-object, or when it carries no usable skill data.
* @param {object|null} cache parsed skill-score-report.json
* ({target, scored:[{name, meetsTarget}], below:[name]})
* @returns {string|null}
*/
export function summarizeSkillQuality(cache) {
if (!cache || typeof cache !== 'object') return null;
const target = Number.isFinite(cache.target) ? cache.target : SKILL_QUALITY_TARGET_DEFAULT;
// Trust the precomputed `below` name-list; fall back to deriving it from
// `scored` (tolerant of an older/partial shape). Neither present => unusable.
let below;
if (Array.isArray(cache.below)) {
below = cache.below;
} else if (Array.isArray(cache.scored)) {
below = cache.scored.filter((s) => s && s.meetsTarget === false).map((s) => s.name);
} else {
return null;
}
below = below.filter(Boolean);
if (below.length === 0) return null;
const noun = below.length === 1 ? 'skill' : 'skills';
return `Skill-kvalitet: ${below.length} ${noun} < ${target} % (${below.join(', ')})`;
}