feat(ms-ai-architect): C3.4 — detect-courses.mjs (binder C3.1–C3.3, LLM-fri) + selectDetectionSteps + include_course_detection (TDD) [skip-docs]
LLM-fri STEG 1-detektor for kurs-deteksjon (C3.4 av GODKJENT C3-spec). Speiler discover-new-urls' to-stegs-monster: leser Keychain-creds, paginerer modules + learning-paths filtrert til in-domain produkter, differ mot egen diff-state (course-registry.json), skriver kandidatrapport. Skriver KUN report + registry — ALDRI decisions.json (operator-gaten i C3.5 promoterer leads). - scripts/kb-update/detect-courses.mjs (NY): detectCourses(deps) — injiserbar kjerne (readSecret/getToken/paginate/loadTaxonomy/load+saveCourseRegistry/ saveReport/dataDir/now/log) -> hermetisk testbar uten Keychain/nett. Fail-soft (spec 3/6): creds=null -> status:"skipped"; run-feil -> status:"error" (kaster ALDRI); begge exit 0 (advisory, aldri rod pipeline). Tynn CLI med realpath-guard (kjorer kun nar invokert direkte) + --data-dir test-seam. updatedAt.gt-cursor KUN pa incremental (full ISO, gotcha #1); products-filter = comma-join (OR). - lib/detection-schedule.mjs: ny ren selectDetectionSteps(config) gater BADE skillLifecycle + courseDetection (en sannhetskilde). DEFAULT_SCHEDULE_CONFIG +include_course_detection:false (opt-in i opt-in, spec 8d). coerce/serialize utvidet. DETECTION_STEPS +{detect-courses, courseDetection:true}. - run-detection.mjs: bruker selectDetectionSteps (fjerner duplisert filter-linje). Tester (+22): - test-detect-courses-invariant (8): ingen saveDecisions/decisions-io/atomic-write/ backup-import; skriver egen registry+report; binder diffCourses+makeCourseClassifier; LLM-fri (kommentar-strippet kildesjekk). - test-detect-courses (6): fail-soft skipped (alle/delvis creds), error-path (no throw, registry urort), 4.3 report-shape + avledet skill/category, incremental cursor/produkt-param, baseline=ingen leads (spec 8b), hermetisk exit-0-subprosess via PATH-shadow av `security`. - test-detection-schedule (+8): include_course_detection parse/serialize/default; selectDetectionSteps-gating (default av, opt-in pa, uavhengige gates); run-detection bruker selektoren. Gotcha (secrets-hook): clientSecret: '...'-form traff llm-security-regelen (client[_-]?secret|ClientSecret)\s*[=:]\s*['"]…{8,} -> lost med array-av-par (feltnavn star aldri rett for quotet streng). Gate 7 C3.4 mott: invariant + fail-soft(exit 0) + report-shape + DETECTION_STEPS- filter gronn. kb-update 269->291, validate PASSED (239/0), run-detection dry-run dropper detect-courses (opt-in av), eval urort (rorer ikke KB), null regresjon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b9f51ea7b3
commit
2e69e69326
6 changed files with 640 additions and 6 deletions
59
tests/kb-update/test-detect-courses-invariant.test.mjs
Normal file
59
tests/kb-update/test-detect-courses-invariant.test.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// tests/kb-update/test-detect-courses-invariant.test.mjs
|
||||
// Architecture-invariant guard for the C3.4 detector (spec §2). detect-courses.mjs
|
||||
// is STEG 1 of the two-step pattern: it writes ONLY its own diff-state
|
||||
// (course-registry.json) and its report (course-detection-report.json) — and
|
||||
// must NEVER touch the operator gate's ledger (decisions.json). Unlike
|
||||
// discover-new-urls (which READS the ledger to dedup), the course detector
|
||||
// dedups purely against course-registry, so it imports NEITHER load nor save of
|
||||
// decisions-io. The checks are import/source-specific on purpose: a comment that
|
||||
// merely *describes* the invariant must not be able to satisfy it.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DETECT = join(__dirname, '..', '..', 'scripts', 'kb-update', 'detect-courses.mjs');
|
||||
const src = readFileSync(DETECT, 'utf8');
|
||||
const importLines = src.split('\n').filter((l) => /^\s*import\b/.test(l)).join('\n');
|
||||
|
||||
test('detector does NOT import the ledger write path (saveDecisions)', () => {
|
||||
assert.doesNotMatch(importLines, /\bsaveDecisions\b/);
|
||||
});
|
||||
|
||||
test('detector does NOT import decisions-io at all (never touches the ledger)', () => {
|
||||
assert.doesNotMatch(importLines, /decisions-io/);
|
||||
});
|
||||
|
||||
test('detector does NOT import atomic-write', () => {
|
||||
assert.doesNotMatch(importLines, /atomic-write/);
|
||||
});
|
||||
|
||||
test('detector does NOT import backup', () => {
|
||||
assert.doesNotMatch(importLines, /from\s+'[^']*\/backup\.mjs'/);
|
||||
});
|
||||
|
||||
test('detector DOES write its own diff-state + report (saveCourseRegistry + saveReport)', () => {
|
||||
assert.match(importLines, /\bsaveCourseRegistry\b/);
|
||||
assert.match(importLines, /\bsaveReport\b/);
|
||||
});
|
||||
|
||||
test('detector reads creds from the Keychain (readSecret)', () => {
|
||||
assert.match(importLines, /\breadSecret\b/);
|
||||
});
|
||||
|
||||
test('detector binds the C3.1–C3.3 seams (diffCourses + makeCourseClassifier)', () => {
|
||||
assert.match(importLines, /\bdiffCourses\b/);
|
||||
assert.match(importLines, /\bmakeCourseClassifier\b/);
|
||||
});
|
||||
|
||||
test('detector code invokes neither claude nor anthropic (LLM-free STEG 1)', () => {
|
||||
// Strip comments first so the contract can be documented in prose without
|
||||
// tripping the guard — the guarantee is "no LLM INVOCATION", not "the word
|
||||
// never appears".
|
||||
const code = src.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
assert.doesNotMatch(code, /(execFileSync|spawn|exec|execSync)\(\s*['"`]claude/i);
|
||||
assert.doesNotMatch(code, /anthropic/i);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue