@@ -14,6 +14,8 @@ import {
1414 KNOB_SUGGESTION_TARGET_PRECISION ,
1515 loadKnobStatus ,
1616 loadLiveKnobStatuses ,
17+ runPerRepoKnobLoosening ,
18+ PER_REPO_LOOSENING_MAX_REPOS_PER_TICK ,
1719 runConfigDriftSentinel ,
1820 runKnobLoosening ,
1921 runScheduledKnobLoosening ,
@@ -48,8 +50,8 @@ async function setOverrideRow(env: Env, key: string, value: string): Promise<voi
4850// Membership-probe seeding (same technique as the satisfaction suites) sized for the AI knob's stricter
4951// floors: borderline-confirmed history between the first candidate (0.9) and the shipped 0.93 in both
5052// slices, plus one genuinely-reversed deep-low firing per slice so precision has a denominator.
51- async function seedAiLooseningFriendlyHistory ( env : Env ) : Promise < void > {
52- const pool = Array . from ( { length : 400 } , ( _ , i ) => `acme/widgets #${ i + 1 } ` ) ;
53+ async function seedAiLooseningFriendlyHistory ( env : Env , repo = "acme/widgets" ) : Promise < void > {
54+ const pool = Array . from ( { length : 400 } , ( _ , i ) => `${ repo } #${ i + 1 } ` ) ;
5355 const probe = pool . map ( ( targetKey ) => ( {
5456 ruleId : AI_KNOB . ruleId ,
5557 targetKey,
@@ -388,6 +390,116 @@ describe("runConfigDriftSentinel (#8213)", () => {
388390 } ) ;
389391} ) ;
390392
393+ describe ( "runPerRepoKnobLoosening (#8217)" , ( ) => {
394+ it ( "a dense repo earns its OWN override while a sparse repo inherits global untouched" , async ( ) => {
395+ const env = enabledEnv ( ) ;
396+ await seedAiLooseningFriendlyHistory ( env , "acme/dense" ) ;
397+ // Sparse repo: a handful of cases, far under the knob's floors.
398+ const store = createSignalStore ( env ) ;
399+ for ( let i = 1 ; i <= 3 ; i += 1 ) {
400+ await store . recordRuleFired ( { ruleId : AI_KNOB . ruleId , targetKey : `acme/sparse#${ i } ` , outcome : "unaddressed" , occurredAt : new Date ( Date . now ( ) - 5000 ) . toISOString ( ) , metadata : { confidence : 0.91 } } ) ;
401+ await store . recordHumanOverride ( { ruleId : AI_KNOB . ruleId , targetKey : `acme/sparse#${ i } ` , verdict : "confirmed" , occurredAt : new Date ( ) . toISOString ( ) } ) ;
402+ }
403+
404+ const results = await runPerRepoKnobLoosening ( env , AI_KNOB ) ;
405+ expect ( results ) . toEqual ( [ { repoFullName : "acme/dense" , applied : true , reason : "applied" } ] ) ;
406+ expect ( await getKnobOverrideForRepo ( env , AI_KNOB , "acme/dense" ) ) . toBe ( AI_KNOB . candidates [ 0 ] ) ;
407+ // Sparse repo: no repo row; resolution falls through to global (none here) -> null.
408+ expect ( await getKnobOverrideForRepo ( env , AI_KNOB , "acme/sparse" ) ) . toBeNull ( ) ;
409+ // The repo-scoped audit event carries the scope + repo.
410+ const events = await env . DB . prepare ( "SELECT metadata_json FROM audit_events WHERE event_type = ?" ) . bind ( AI_KNOB . looseningEventType ) . all < { metadata_json : string } > ( ) ;
411+ const metadata = JSON . parse ( events . results ! [ 0 ] ! . metadata_json ) as { scope ?: string ; repoFullName ?: string } ;
412+ expect ( metadata ) . toMatchObject ( { scope : "repo" , repoFullName : "acme/dense" } ) ;
413+ } ) ;
414+
415+ it ( "second tick evaluates the earned repo from ITS value (no proposal left) — never oscillates; already-at-minimum reports as such" , async ( ) => {
416+ const env = enabledEnv ( ) ;
417+ await seedAiLooseningFriendlyHistory ( env , "acme/dense" ) ;
418+ await runPerRepoKnobLoosening ( env , AI_KNOB ) ;
419+ const second = await runPerRepoKnobLoosening ( env , AI_KNOB ) ;
420+ expect ( second ) . toEqual ( [ { repoFullName : "acme/dense" , applied : false , reason : "no_proposal" } ] ) ;
421+
422+ await setOverrideRow ( env , repoKnobOverrideFlagKey ( AI_KNOB , "acme/dense" ) , String ( AI_KNOB . hardMinimum ) ) ;
423+ const third = await runPerRepoKnobLoosening ( env , AI_KNOB ) ;
424+ expect ( third ) . toEqual ( [ { repoFullName : "acme/dense" , applied : false , reason : "already_applied" } ] ) ;
425+ } ) ;
426+
427+ it ( "gates: report-only knob and flag-off both do nothing; a broken store fails safe with a warn" , async ( ) => {
428+ const reportOnly = { ...AI_KNOB , applyMode : "report_only" as const } ;
429+ expect ( await runPerRepoKnobLoosening ( enabledEnv ( ) , reportOnly ) ) . toEqual ( [ ] ) ;
430+ expect ( await runPerRepoKnobLoosening ( createTestEnv ( ) , AI_KNOB ) ) . toEqual ( [ ] ) ;
431+
432+ const broken = enabledEnv ( ) ;
433+ broken . DB = { prepare : ( ) => { throw new Error ( "boom" ) ; } } as never ;
434+ const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => undefined ) ;
435+ expect ( await runPerRepoKnobLoosening ( broken , AI_KNOB ) ) . toEqual ( [ ] ) ;
436+ expect ( warnSpy . mock . calls . some ( ( c ) => String ( c [ 0 ] ) . includes ( "per_repo_loosening_tick_failed" ) ) ) . toBe ( true ) ;
437+ } ) ;
438+
439+ it ( "empty eligibility returns cleanly; audit rejection is best-effort; a mid-repo error fails safe per repo; non-Error throws degrade" , async ( ) => {
440+ // Enabled but only sparse data -> zero eligible repos -> the early return, no cursor written.
441+ const sparseOnly = enabledEnv ( ) ;
442+ const store = createSignalStore ( sparseOnly ) ;
443+ await store . recordRuleFired ( { ruleId : AI_KNOB . ruleId , targetKey : "acme/sparse#1" , outcome : "unaddressed" , occurredAt : new Date ( ) . toISOString ( ) , metadata : { confidence : 0.91 } } ) ;
444+ await store . recordHumanOverride ( { ruleId : AI_KNOB . ruleId , targetKey : "acme/sparse#1" , verdict : "confirmed" , occurredAt : new Date ( ) . toISOString ( ) } ) ;
445+ expect ( await runPerRepoKnobLoosening ( sparseOnly , AI_KNOB ) ) . toEqual ( [ ] ) ;
446+
447+ // Audit write rejection: the override still lands (best-effort trail, never sacrificed writes).
448+ const env = enabledEnv ( ) ;
449+ await seedAiLooseningFriendlyHistory ( env , "acme/dense" ) ;
450+ const repositories = await import ( "../../src/db/repositories" ) ;
451+ vi . spyOn ( repositories , "recordAuditEvent" ) . mockRejectedValue ( new Error ( "audit down" ) ) ;
452+ const results = await runPerRepoKnobLoosening ( env , AI_KNOB ) ;
453+ expect ( results ) . toEqual ( [ { repoFullName : "acme/dense" , applied : true , reason : "applied" } ] ) ;
454+ expect ( await getKnobOverrideForRepo ( env , AI_KNOB , "acme/dense" ) ) . toBe ( AI_KNOB . candidates [ 0 ] ) ;
455+ vi . restoreAllMocks ( ) ;
456+
457+ // Mid-repo evaluator throw: that repo reports error, the tick survives.
458+ const env2 = enabledEnv ( ) ;
459+ await seedAiLooseningFriendlyHistory ( env2 , "acme/dense" ) ;
460+ const looseningKnobsModule = await import ( "../../src/services/loosening-knobs" ) ;
461+ vi . spyOn ( looseningKnobsModule , "evaluateKnobLoosening" ) . mockImplementation ( ( ) => { throw "evaluator string boom" ; } ) ;
462+ const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => undefined ) ;
463+ const errored = await runPerRepoKnobLoosening ( env2 , AI_KNOB ) ;
464+ expect ( errored ) . toEqual ( [ { repoFullName : "acme/dense" , applied : false , reason : "error" } ] ) ;
465+ expect ( warnSpy . mock . calls . some ( ( c ) => String ( c [ 0 ] ) . includes ( "per_repo_loosening_failed" ) && String ( c [ 0 ] ) . includes ( '"error":"unknown error"' ) ) ) . toBe ( true ) ;
466+ vi . restoreAllMocks ( ) ;
467+
468+ // Inner catch with a REAL Error message, and the outer catch's non-Error arm via a string-throwing store.
469+ const env3 = enabledEnv ( ) ;
470+ await seedAiLooseningFriendlyHistory ( env3 , "acme/dense" ) ;
471+ const knobsModule = await import ( "../../src/services/loosening-knobs" ) ;
472+ vi . spyOn ( knobsModule , "evaluateKnobLoosening" ) . mockImplementation ( ( ) => { throw new Error ( "evaluator real error" ) ; } ) ;
473+ const warn3 = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => undefined ) ;
474+ await runPerRepoKnobLoosening ( env3 , AI_KNOB ) ;
475+ expect ( warn3 . mock . calls . some ( ( c ) => String ( c [ 0 ] ) . includes ( "evaluator real error" ) ) ) . toBe ( true ) ;
476+ vi . restoreAllMocks ( ) ;
477+
478+ const stringStore = enabledEnv ( ) ;
479+ stringStore . DB = { prepare : ( ) => { throw "outer string boom" ; } } as never ;
480+ const warn4 = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => undefined ) ;
481+ expect ( await runPerRepoKnobLoosening ( stringStore , AI_KNOB ) ) . toEqual ( [ ] ) ;
482+ expect ( warn4 . mock . calls . some ( ( c ) => String ( c [ 0 ] ) . includes ( "per_repo_loosening_tick_failed" ) && String ( c [ 0 ] ) . includes ( '"error":"unknown error"' ) ) ) . toBe ( true ) ;
483+ } ) ;
484+
485+ it ( "caps the batch and rotates the cursor across ticks (deterministic order)" , async ( ) => {
486+ expect ( PER_REPO_LOOSENING_MAX_REPOS_PER_TICK ) . toBe ( 10 ) ;
487+ const env = enabledEnv ( ) ;
488+ // Two dense repos; cursor after tick 1 should sit at the last processed repo. With a batch cap of 10
489+ // both fit in one tick, so pin the cursor bookkeeping rather than the wraparound (covered by the
490+ // rotation arithmetic itself being deterministic on the sorted list).
491+ await seedAiLooseningFriendlyHistory ( env , "acme/alpha" ) ;
492+ await seedAiLooseningFriendlyHistory ( env , "acme/beta" ) ;
493+ const results = await runPerRepoKnobLoosening ( env , AI_KNOB ) ;
494+ expect ( results . map ( ( r ) => r . repoFullName ) ) . toEqual ( [ "acme/alpha" , "acme/beta" ] ) ;
495+ const cursor = await env . DB . prepare ( "SELECT value FROM system_flags WHERE key = ?" ) . bind ( `per_repo_loosening_cursor:${ AI_KNOB . knobId } ` ) . first < { value : string } > ( ) ;
496+ expect ( cursor ?. value ) . toBe ( "acme/beta" ) ;
497+ // Next tick starts AFTER the cursor: wraps to alpha first again (both still eligible).
498+ const second = await runPerRepoKnobLoosening ( env , AI_KNOB ) ;
499+ expect ( second . map ( ( r ) => r . repoFullName ) ) . toEqual ( [ "acme/alpha" , "acme/beta" ] ) ;
500+ } ) ;
501+ } ) ;
502+
391503describe ( "loadKnobStatus / loadLiveKnobStatuses (#8161 generalized)" , ( ) => {
392504 it ( "reports a lingering override row even with the flag OFF, and the live value only when ON" , async ( ) => {
393505 const env = createTestEnv ( ) ;
0 commit comments