feat(ms-ai-architect): C3.6 — SessionStart-surfacing av kurs-leads (summarizeCourses) → C3 kurs-spor komplett (TDD)

Siste C3-fase. `summarizeCourses(report)` ren one-liner i detection-schedule.mjs
(«Kurs-signaler: N nye / M endrede kurs i dekkede produkter»; null ved
0/skipped/error/removed-only/malformed — removed er informasjons-signal, aldri
surfacet lead, spec §4.2). Speiler summarizeSkillLifecycle.

Wiring i session-start-context.mjs: read-only, leser
scripts/kb-update/data/course-detection-report.json, foreslår /architect:kb-update,
speiler skill-signaler-blokka. Hooken spawner ingenting for kurs-sporet.

docs/development.md: ny C3-workflow-seksjon (to-stegs: Claude-fri deteksjon →
SessionStart-surfacing → operatør-gate §3c). CLAUDE.md hook-tabell oppdatert.

Tester (+6): summarizeCourses — new+updated one-liner, only-new/only-updated,
zero→null, removed-only→null, skipped/error→null, missing/partial→null.
kb-update 307→313, validate 239/0, kb-eval 100/0, hooks 11/11, discovery 13/13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 15:00:40 +02:00
commit f4dd4d3fb6
5 changed files with 120 additions and 1 deletions

View file

@ -25,6 +25,7 @@ import {
shouldRunDetection,
selectDetectionSteps,
summarizeSkillLifecycle,
summarizeCourses,
daysSinceLastPoll,
} from '../../scripts/kb-update/lib/detection-schedule.mjs';
import { writeScheduleConfig } from '../../scripts/kb-update/write-schedule-config.mjs';
@ -369,6 +370,65 @@ test('summarizeSkillLifecycle — tolerates a missing/partial report shape', ()
assert.equal(summarizeSkillLifecycle(null, { threshold: 7.0 }), null);
});
// ===========================================================================
// (C2) summarizeCourses — one-liner surfaced at session start (Spor C / C3.6)
// ===========================================================================
// Mirrors summarizeSkillLifecycle: a pure one-liner over a parsed
// course-detection-report.json, surfacing NEW + UPDATED course leads in
// covered products. `removed` is an informational rapport-signal (spec §4.2),
// NOT a surfaced lead, so it must never make this return non-null on its own.
const COURSE_REPORT_OK = {
generated_at: '2026-06-23T12:00:00Z',
status: 'ok',
skipped_reason: null,
new: [{ uid: 'm1' }, { uid: 'm2' }],
updated: [{ uid: 'm3' }],
removed: [],
counts: { new: 2, updated: 1, removed: 0 },
};
test('summarizeCourses — surfaces new + updated leads in the canonical one-liner', () => {
const s = summarizeCourses(COURSE_REPORT_OK);
assert.match(s, /Kurs-signaler:/);
assert.match(s, /2 nye/);
assert.match(s, /1 endrede/);
assert.match(s, /kurs i dekkede produkter/);
});
test('summarizeCourses — only-new and only-updated each render alone', () => {
const onlyNew = summarizeCourses({ status: 'ok', counts: { new: 3, updated: 0, removed: 0 } });
assert.match(onlyNew, /3 nye kurs i dekkede produkter/);
assert.doesNotMatch(onlyNew, /endrede/);
const onlyUpdated = summarizeCourses({ status: 'ok', counts: { new: 0, updated: 4, removed: 0 } });
assert.match(onlyUpdated, /4 endrede kurs i dekkede produkter/);
assert.doesNotMatch(onlyUpdated, /nye/);
});
test('summarizeCourses — zero new + zero updated returns null (nothing to surface)', () => {
assert.equal(summarizeCourses({ status: 'ok', counts: { new: 0, updated: 0, removed: 0 } }), null);
});
test('summarizeCourses — removed-only is informational, not a surfaced lead (spec §4.2)', () => {
assert.equal(
summarizeCourses({ status: 'ok', counts: { new: 0, updated: 0, removed: 5 } }),
null,
);
});
test('summarizeCourses — skipped/error reports surface nothing', () => {
const skipped = { status: 'skipped', skipped_reason: 'creds', counts: { new: 0, updated: 0, removed: 0 } };
const errored = { status: 'error', skipped_reason: 'network', counts: { new: 0, updated: 0, removed: 0 } };
assert.equal(summarizeCourses(skipped), null);
assert.equal(summarizeCourses(errored), null);
});
test('summarizeCourses — tolerates a missing/partial report shape', () => {
assert.equal(summarizeCourses({}, {}), null);
assert.equal(summarizeCourses(null), null);
assert.equal(summarizeCourses({ status: 'ok' }), null); // no counts
});
// ===========================================================================
// (D) STRUCTURAL ToS GUARANTEE — detection is Claude-free
// ===========================================================================