@@ -537,6 +537,43 @@ export function findSessionLifecycleCleanupBoundaryViolations(content, fileName
537537 ) ;
538538}
539539
540+ // Source roots shared by the enforced boundary checks in main() and the debt
541+ // ratchet below; keeping one list prevents the two scans from drifting apart.
542+ const readSourceRootPaths = [
543+ "packages/memory-host-sdk/src/host" ,
544+ "extensions/discord/src/monitor" ,
545+ "extensions/memory-core/src" ,
546+ "extensions/telegram/src" ,
547+ "extensions/voice-call/src" ,
548+ "src/acp" ,
549+ "src/agents" ,
550+ "src/auto-reply" ,
551+ "src/commands" ,
552+ "src/config/sessions" ,
553+ "src/cron" ,
554+ "src/gateway" ,
555+ "src/infra" ,
556+ "src/plugins" ,
557+ "src/tui" ,
558+ ] ;
559+ const writeSourceRootPaths = [
560+ "src/acp" ,
561+ "src/agents" ,
562+ "src/auto-reply" ,
563+ "src/commands" ,
564+ "src/config/sessions" ,
565+ "src/gateway" ,
566+ "src/plugins" ,
567+ "src/tui" ,
568+ ] ;
569+ const transcriptWriterSourceRootPaths = [
570+ "src/agents/command" ,
571+ "src/agents/embedded-agent-runner" ,
572+ "src/config/sessions" ,
573+ "src/gateway/server-methods" ,
574+ "src/sessions" ,
575+ ] ;
576+
540577function declarationName ( node ) {
541578 if ( ts . isFunctionDeclaration ( node ) && node . name ) {
542579 return node . name . text ;
@@ -617,42 +654,166 @@ export function findMemoryHostSessionCorpusBoundaryViolations(content, fileName
617654 return violations ;
618655}
619656
657+ // Debt ratchet: the boundary checks above only scan files already on the
658+ // migrated lists, so unmigrated files could quietly gain new legacy call
659+ // sites. The checked-in baseline locks each unmigrated file's current legacy
660+ // call-site count per concern; any drift from the baseline fails the guard.
661+ export const sessionAccessorDebtBaselineRelativePath =
662+ "scripts/lib/session-accessor-debt-baseline.json" ;
663+ const debtBaselineRegenCommand = "pnpm lint:tmp:session-accessor-boundary:gen" ;
664+
665+ // Keys sorted alphabetically so the generated baseline JSON stays deterministic.
666+ const sessionAccessorDebtConcerns = [
667+ {
668+ key : "embeddedAgentSessionTarget" ,
669+ sourceRootPaths : [ "extensions/voice-call/src" ] ,
670+ migratedFiles : migratedEmbeddedAgentSessionTargetFiles ,
671+ findViolations : findEmbeddedAgentSessionTargetViolations ,
672+ } ,
673+ {
674+ key : "memoryHostSessionCorpus" ,
675+ sourceRootPaths : [ "packages/memory-host-sdk/src/host" ] ,
676+ migratedFiles : migratedMemoryHostSessionCorpusFiles ,
677+ findViolations : findMemoryHostSessionCorpusBoundaryViolations ,
678+ } ,
679+ {
680+ key : "sessionAccessorRead" ,
681+ sourceRootPaths : readSourceRootPaths ,
682+ migratedFiles : new Set ( [
683+ ...migratedSessionAccessorFiles ,
684+ ...migratedBundledPluginSessionAccessorFiles ,
685+ ] ) ,
686+ findViolations : findSessionAccessorBoundaryViolations ,
687+ } ,
688+ {
689+ key : "sessionAccessorWrite" ,
690+ sourceRootPaths : writeSourceRootPaths ,
691+ migratedFiles : migratedSessionAccessorWriteFiles ,
692+ findViolations : findSessionAccessorWriteBoundaryViolations ,
693+ } ,
694+ {
695+ key : "sessionCompactManualTrim" ,
696+ sourceRootPaths : [ "src/gateway/server-methods" ] ,
697+ migratedFiles : migratedSessionCompactManualTrimFiles ,
698+ findViolations : findSessionCompactManualTrimBoundaryViolations ,
699+ } ,
700+ {
701+ key : "sessionLifecycleCleanup" ,
702+ sourceRootPaths : readSourceRootPaths ,
703+ migratedFiles : migratedSessionLifecycleCleanupFiles ,
704+ findViolations : findSessionLifecycleCleanupBoundaryViolations ,
705+ } ,
706+ {
707+ key : "transcriptWriter" ,
708+ sourceRootPaths : transcriptWriterSourceRootPaths ,
709+ migratedFiles : migratedTranscriptWriterFiles ,
710+ findViolations : findTranscriptWriterBoundaryViolations ,
711+ } ,
712+ ] ;
713+
714+ function sortRecordByKey ( record ) {
715+ return Object . fromEntries (
716+ Object . entries ( record ) . toSorted ( ( [ left ] , [ right ] ) =>
717+ left < right ? - 1 : left > right ? 1 : 0 ,
718+ ) ,
719+ ) ;
720+ }
721+
722+ /** Counts legacy call sites per unmigrated file for every debt concern. */
723+ export async function collectSessionAccessorDebtCounts ( repoRoot ) {
724+ const counts = { } ;
725+ for ( const concern of sessionAccessorDebtConcerns ) {
726+ const violations = await collectFileViolations ( {
727+ repoRoot,
728+ sourceRoots : resolveSourceRoots ( repoRoot , concern . sourceRootPaths ) ,
729+ // Inverse of the enforcement skip: migrated files are held at zero by the
730+ // boundary checks, so the ratchet tracks only the unmigrated rest.
731+ skipFile : ( filePath ) =>
732+ concern . migratedFiles . has ( normalizeRelativePath ( path . relative ( repoRoot , filePath ) ) ) ,
733+ findViolations : concern . findViolations ,
734+ } ) ;
735+ const fileCounts = { } ;
736+ for ( const violation of violations ) {
737+ const relativePath = normalizeRelativePath ( violation . path ) ;
738+ fileCounts [ relativePath ] = ( fileCounts [ relativePath ] ?? 0 ) + 1 ;
739+ }
740+ counts [ concern . key ] = sortRecordByKey ( fileCounts ) ;
741+ }
742+ return sortRecordByKey ( counts ) ;
743+ }
744+
745+ /** Ratchet compare: counts above baseline are regressions, below are improvements. */
746+ export function compareSessionAccessorDebt ( currentCounts , baselineCounts ) {
747+ const regressions = [ ] ;
748+ const improvements = [ ] ;
749+ const concerns = [
750+ ...new Set ( [ ...Object . keys ( baselineCounts ) , ...Object . keys ( currentCounts ) ] ) ,
751+ ] . toSorted ( ) ;
752+ for ( const concern of concerns ) {
753+ const current = currentCounts [ concern ] ?? { } ;
754+ const baseline = baselineCounts [ concern ] ?? { } ;
755+ const filePaths = [ ...new Set ( [ ...Object . keys ( baseline ) , ...Object . keys ( current ) ] ) ] . toSorted ( ) ;
756+ for ( const filePath of filePaths ) {
757+ const currentCount = current [ filePath ] ?? 0 ;
758+ const baselineCount = baseline [ filePath ] ?? 0 ;
759+ if ( currentCount === baselineCount ) {
760+ continue ;
761+ }
762+ const entry = { concern, path : filePath , currentCount, baselineCount } ;
763+ if ( currentCount > baselineCount ) {
764+ regressions . push ( entry ) ;
765+ } else {
766+ improvements . push ( entry ) ;
767+ }
768+ }
769+ }
770+ return { regressions, improvements } ;
771+ }
772+
773+ // Improvements fail the guard too: passing silently would leave the baseline
774+ // stale, letting a later change reintroduce legacy call sites up to the old
775+ // count without tripping the ratchet.
776+ export function formatSessionAccessorDebtImprovements ( improvements ) {
777+ return [
778+ `Legacy session accessor debt dropped below ${ sessionAccessorDebtBaselineRelativePath } :` ,
779+ ...improvements . map (
780+ ( improvement ) =>
781+ `- ${ improvement . path } [${ improvement . concern } ]: ${ improvement . currentCount } legacy call site(s), stale baseline allows ${ improvement . baselineCount } ` ,
782+ ) ,
783+ `Run \`${ debtBaselineRegenCommand } \` to ratchet the baseline down and commit it.` ,
784+ ] ;
785+ }
786+
787+ function resolveDebtBaselinePath ( repoRoot ) {
788+ return path . join ( repoRoot , ...sessionAccessorDebtBaselineRelativePath . split ( "/" ) ) ;
789+ }
790+
791+ async function readSessionAccessorDebtBaseline ( repoRoot ) {
792+ try {
793+ return JSON . parse ( await fs . readFile ( resolveDebtBaselinePath ( repoRoot ) , "utf8" ) ) ;
794+ } catch ( error ) {
795+ if ( error && typeof error === "object" && error . code === "ENOENT" ) {
796+ return null ;
797+ }
798+ throw error ;
799+ }
800+ }
801+
802+ async function writeSessionAccessorDebtBaseline ( repoRoot ) {
803+ const counts = await collectSessionAccessorDebtCounts ( repoRoot ) ;
804+ await fs . writeFile ( resolveDebtBaselinePath ( repoRoot ) , `${ JSON . stringify ( counts , null , 2 ) } \n` ) ;
805+ }
806+
620807export async function main ( ) {
621808 const repoRoot = resolveRepoRoot ( import . meta. url ) ;
622- const readSourceRoots = resolveSourceRoots ( repoRoot , [
623- "packages/memory-host-sdk/src/host" ,
624- "extensions/discord/src/monitor" ,
625- "extensions/memory-core/src" ,
626- "extensions/telegram/src" ,
627- "extensions/voice-call/src" ,
628- "src/acp" ,
629- "src/agents" ,
630- "src/auto-reply" ,
631- "src/commands" ,
632- "src/config/sessions" ,
633- "src/cron" ,
634- "src/gateway" ,
635- "src/infra" ,
636- "src/plugins" ,
637- "src/tui" ,
638- ] ) ;
639- const writeSourceRoots = resolveSourceRoots ( repoRoot , [
640- "src/acp" ,
641- "src/agents" ,
642- "src/auto-reply" ,
643- "src/commands" ,
644- "src/config/sessions" ,
645- "src/gateway" ,
646- "src/plugins" ,
647- "src/tui" ,
648- ] ) ;
649- const transcriptWriterSourceRoots = resolveSourceRoots ( repoRoot , [
650- "src/agents/command" ,
651- "src/agents/embedded-agent-runner" ,
652- "src/config/sessions" ,
653- "src/gateway/server-methods" ,
654- "src/sessions" ,
655- ] ) ;
809+ if ( process . argv . includes ( "--update-debt-baseline" ) ) {
810+ await writeSessionAccessorDebtBaseline ( repoRoot ) ;
811+ console . log ( `Wrote ${ sessionAccessorDebtBaselineRelativePath } ` ) ;
812+ return ;
813+ }
814+ const readSourceRoots = resolveSourceRoots ( repoRoot , readSourceRootPaths ) ;
815+ const writeSourceRoots = resolveSourceRoots ( repoRoot , writeSourceRootPaths ) ;
816+ const transcriptWriterSourceRoots = resolveSourceRoots ( repoRoot , transcriptWriterSourceRootPaths ) ;
656817 const readViolations = await collectFileViolations ( {
657818 repoRoot,
658819 sourceRoots : readSourceRoots ,
@@ -745,18 +906,50 @@ export async function main() {
745906 ...sessionStoreRuntimeCompatViolations ,
746907 ] ;
747908
748- if ( violations . length === 0 ) {
909+ const baselineCounts = await readSessionAccessorDebtBaseline ( repoRoot ) ;
910+ if ( ! baselineCounts ) {
911+ console . error (
912+ `Missing ${ sessionAccessorDebtBaselineRelativePath } ; run \`${ debtBaselineRegenCommand } \` and commit it.` ,
913+ ) ;
914+ process . exit ( 1 ) ;
915+ }
916+ const debt = compareSessionAccessorDebt (
917+ await collectSessionAccessorDebtCounts ( repoRoot ) ,
918+ baselineCounts ,
919+ ) ;
920+
921+ if ( violations . length === 0 && debt . regressions . length === 0 && debt . improvements . length === 0 ) {
749922 console . log ( "session accessor boundary guard passed." ) ;
750923 return ;
751924 }
752925
753- console . error ( "Found legacy session store usage in session-accessor migrated files:" ) ;
754- for ( const violation of violations ) {
755- console . error ( `- ${ violation . path } :${ violation . line } : ${ violation . reason } ` ) ;
926+ if ( violations . length > 0 ) {
927+ console . error ( "Found legacy session store usage in session-accessor migrated files:" ) ;
928+ for ( const violation of violations ) {
929+ console . error ( `- ${ violation . path } :${ violation . line } : ${ violation . reason } ` ) ;
930+ }
931+ console . error (
932+ "Use src/config/sessions/session-accessor.ts helpers for migrated read/write and transcript-writer paths. Expand file-backed SDK compatibility only as an explicit pre-SQLite migration decision." ,
933+ ) ;
934+ }
935+ if ( debt . regressions . length > 0 ) {
936+ console . error (
937+ `Found new legacy session call sites in unmigrated files (counts exceed ${ sessionAccessorDebtBaselineRelativePath } ):` ,
938+ ) ;
939+ for ( const regression of debt . regressions ) {
940+ console . error (
941+ `- ${ regression . path } [${ regression . concern } ]: ${ regression . currentCount } legacy call site(s), baseline allows ${ regression . baselineCount } ` ,
942+ ) ;
943+ }
944+ console . error (
945+ `Use src/config/sessions/session-accessor.ts helpers instead of adding legacy call sites. If the increase is an intentional seam-owner change, run \`${ debtBaselineRegenCommand } \` and commit the updated baseline.` ,
946+ ) ;
947+ }
948+ if ( debt . improvements . length > 0 ) {
949+ for ( const line of formatSessionAccessorDebtImprovements ( debt . improvements ) ) {
950+ console . error ( line ) ;
951+ }
756952 }
757- console . error (
758- "Use src/config/sessions/session-accessor.ts helpers for migrated read/write and transcript-writer paths. Expand file-backed SDK compatibility only as an explicit pre-SQLite migration decision." ,
759- ) ;
760953 process . exit ( 1 ) ;
761954}
762955
0 commit comments