chore(scripts): add debt ratchet to the session-accessor boundary guard (#101178)

* chore(scripts): add debt ratchet baseline to session-accessor boundary gate

* fix(scripts): fail session-accessor debt gate until baseline ratchets down
This commit is contained in:
Josh Lehman
2026-07-06 13:55:16 -07:00
committed by GitHub
parent 6bffda4166
commit c7295e417d
4 changed files with 370 additions and 41 deletions
+1
View File
@@ -1684,6 +1684,7 @@
"lint:tmp:no-raw-channel-fetch": "node scripts/check-no-raw-channel-fetch.mjs",
"lint:tmp:no-raw-http2-imports": "node scripts/check-no-raw-http2-imports.mjs",
"lint:tmp:session-accessor-boundary": "node scripts/check-session-accessor-boundary.mjs",
"lint:tmp:session-accessor-boundary:gen": "node scripts/check-session-accessor-boundary.mjs --update-debt-baseline",
"lint:tmp:session-transcript-reader-boundary": "node scripts/check-session-transcript-reader-boundary.mjs",
"lint:tmp:tsgo-core-boundary": "node scripts/check-tsgo-core-boundary.mjs",
"lint:ui:no-raw-window-open": "node scripts/check-no-raw-window-open.mjs",
+234 -41
View File
@@ -537,6 +537,43 @@ export function findSessionLifecycleCleanupBoundaryViolations(content, fileName
);
}
// Source roots shared by the enforced boundary checks in main() and the debt
// ratchet below; keeping one list prevents the two scans from drifting apart.
const readSourceRootPaths = [
"packages/memory-host-sdk/src/host",
"extensions/discord/src/monitor",
"extensions/memory-core/src",
"extensions/telegram/src",
"extensions/voice-call/src",
"src/acp",
"src/agents",
"src/auto-reply",
"src/commands",
"src/config/sessions",
"src/cron",
"src/gateway",
"src/infra",
"src/plugins",
"src/tui",
];
const writeSourceRootPaths = [
"src/acp",
"src/agents",
"src/auto-reply",
"src/commands",
"src/config/sessions",
"src/gateway",
"src/plugins",
"src/tui",
];
const transcriptWriterSourceRootPaths = [
"src/agents/command",
"src/agents/embedded-agent-runner",
"src/config/sessions",
"src/gateway/server-methods",
"src/sessions",
];
function declarationName(node) {
if (ts.isFunctionDeclaration(node) && node.name) {
return node.name.text;
@@ -617,42 +654,166 @@ export function findMemoryHostSessionCorpusBoundaryViolations(content, fileName
return violations;
}
// Debt ratchet: the boundary checks above only scan files already on the
// migrated lists, so unmigrated files could quietly gain new legacy call
// sites. The checked-in baseline locks each unmigrated file's current legacy
// call-site count per concern; any drift from the baseline fails the guard.
export const sessionAccessorDebtBaselineRelativePath =
"scripts/lib/session-accessor-debt-baseline.json";
const debtBaselineRegenCommand = "pnpm lint:tmp:session-accessor-boundary:gen";
// Keys sorted alphabetically so the generated baseline JSON stays deterministic.
const sessionAccessorDebtConcerns = [
{
key: "embeddedAgentSessionTarget",
sourceRootPaths: ["extensions/voice-call/src"],
migratedFiles: migratedEmbeddedAgentSessionTargetFiles,
findViolations: findEmbeddedAgentSessionTargetViolations,
},
{
key: "memoryHostSessionCorpus",
sourceRootPaths: ["packages/memory-host-sdk/src/host"],
migratedFiles: migratedMemoryHostSessionCorpusFiles,
findViolations: findMemoryHostSessionCorpusBoundaryViolations,
},
{
key: "sessionAccessorRead",
sourceRootPaths: readSourceRootPaths,
migratedFiles: new Set([
...migratedSessionAccessorFiles,
...migratedBundledPluginSessionAccessorFiles,
]),
findViolations: findSessionAccessorBoundaryViolations,
},
{
key: "sessionAccessorWrite",
sourceRootPaths: writeSourceRootPaths,
migratedFiles: migratedSessionAccessorWriteFiles,
findViolations: findSessionAccessorWriteBoundaryViolations,
},
{
key: "sessionCompactManualTrim",
sourceRootPaths: ["src/gateway/server-methods"],
migratedFiles: migratedSessionCompactManualTrimFiles,
findViolations: findSessionCompactManualTrimBoundaryViolations,
},
{
key: "sessionLifecycleCleanup",
sourceRootPaths: readSourceRootPaths,
migratedFiles: migratedSessionLifecycleCleanupFiles,
findViolations: findSessionLifecycleCleanupBoundaryViolations,
},
{
key: "transcriptWriter",
sourceRootPaths: transcriptWriterSourceRootPaths,
migratedFiles: migratedTranscriptWriterFiles,
findViolations: findTranscriptWriterBoundaryViolations,
},
];
function sortRecordByKey(record) {
return Object.fromEntries(
Object.entries(record).toSorted(([left], [right]) =>
left < right ? -1 : left > right ? 1 : 0,
),
);
}
/** Counts legacy call sites per unmigrated file for every debt concern. */
export async function collectSessionAccessorDebtCounts(repoRoot) {
const counts = {};
for (const concern of sessionAccessorDebtConcerns) {
const violations = await collectFileViolations({
repoRoot,
sourceRoots: resolveSourceRoots(repoRoot, concern.sourceRootPaths),
// Inverse of the enforcement skip: migrated files are held at zero by the
// boundary checks, so the ratchet tracks only the unmigrated rest.
skipFile: (filePath) =>
concern.migratedFiles.has(normalizeRelativePath(path.relative(repoRoot, filePath))),
findViolations: concern.findViolations,
});
const fileCounts = {};
for (const violation of violations) {
const relativePath = normalizeRelativePath(violation.path);
fileCounts[relativePath] = (fileCounts[relativePath] ?? 0) + 1;
}
counts[concern.key] = sortRecordByKey(fileCounts);
}
return sortRecordByKey(counts);
}
/** Ratchet compare: counts above baseline are regressions, below are improvements. */
export function compareSessionAccessorDebt(currentCounts, baselineCounts) {
const regressions = [];
const improvements = [];
const concerns = [
...new Set([...Object.keys(baselineCounts), ...Object.keys(currentCounts)]),
].toSorted();
for (const concern of concerns) {
const current = currentCounts[concern] ?? {};
const baseline = baselineCounts[concern] ?? {};
const filePaths = [...new Set([...Object.keys(baseline), ...Object.keys(current)])].toSorted();
for (const filePath of filePaths) {
const currentCount = current[filePath] ?? 0;
const baselineCount = baseline[filePath] ?? 0;
if (currentCount === baselineCount) {
continue;
}
const entry = { concern, path: filePath, currentCount, baselineCount };
if (currentCount > baselineCount) {
regressions.push(entry);
} else {
improvements.push(entry);
}
}
}
return { regressions, improvements };
}
// Improvements fail the guard too: passing silently would leave the baseline
// stale, letting a later change reintroduce legacy call sites up to the old
// count without tripping the ratchet.
export function formatSessionAccessorDebtImprovements(improvements) {
return [
`Legacy session accessor debt dropped below ${sessionAccessorDebtBaselineRelativePath}:`,
...improvements.map(
(improvement) =>
`- ${improvement.path} [${improvement.concern}]: ${improvement.currentCount} legacy call site(s), stale baseline allows ${improvement.baselineCount}`,
),
`Run \`${debtBaselineRegenCommand}\` to ratchet the baseline down and commit it.`,
];
}
function resolveDebtBaselinePath(repoRoot) {
return path.join(repoRoot, ...sessionAccessorDebtBaselineRelativePath.split("/"));
}
async function readSessionAccessorDebtBaseline(repoRoot) {
try {
return JSON.parse(await fs.readFile(resolveDebtBaselinePath(repoRoot), "utf8"));
} catch (error) {
if (error && typeof error === "object" && error.code === "ENOENT") {
return null;
}
throw error;
}
}
async function writeSessionAccessorDebtBaseline(repoRoot) {
const counts = await collectSessionAccessorDebtCounts(repoRoot);
await fs.writeFile(resolveDebtBaselinePath(repoRoot), `${JSON.stringify(counts, null, 2)}\n`);
}
export async function main() {
const repoRoot = resolveRepoRoot(import.meta.url);
const readSourceRoots = resolveSourceRoots(repoRoot, [
"packages/memory-host-sdk/src/host",
"extensions/discord/src/monitor",
"extensions/memory-core/src",
"extensions/telegram/src",
"extensions/voice-call/src",
"src/acp",
"src/agents",
"src/auto-reply",
"src/commands",
"src/config/sessions",
"src/cron",
"src/gateway",
"src/infra",
"src/plugins",
"src/tui",
]);
const writeSourceRoots = resolveSourceRoots(repoRoot, [
"src/acp",
"src/agents",
"src/auto-reply",
"src/commands",
"src/config/sessions",
"src/gateway",
"src/plugins",
"src/tui",
]);
const transcriptWriterSourceRoots = resolveSourceRoots(repoRoot, [
"src/agents/command",
"src/agents/embedded-agent-runner",
"src/config/sessions",
"src/gateway/server-methods",
"src/sessions",
]);
if (process.argv.includes("--update-debt-baseline")) {
await writeSessionAccessorDebtBaseline(repoRoot);
console.log(`Wrote ${sessionAccessorDebtBaselineRelativePath}`);
return;
}
const readSourceRoots = resolveSourceRoots(repoRoot, readSourceRootPaths);
const writeSourceRoots = resolveSourceRoots(repoRoot, writeSourceRootPaths);
const transcriptWriterSourceRoots = resolveSourceRoots(repoRoot, transcriptWriterSourceRootPaths);
const readViolations = await collectFileViolations({
repoRoot,
sourceRoots: readSourceRoots,
@@ -745,18 +906,50 @@ export async function main() {
...sessionStoreRuntimeCompatViolations,
];
if (violations.length === 0) {
const baselineCounts = await readSessionAccessorDebtBaseline(repoRoot);
if (!baselineCounts) {
console.error(
`Missing ${sessionAccessorDebtBaselineRelativePath}; run \`${debtBaselineRegenCommand}\` and commit it.`,
);
process.exit(1);
}
const debt = compareSessionAccessorDebt(
await collectSessionAccessorDebtCounts(repoRoot),
baselineCounts,
);
if (violations.length === 0 && debt.regressions.length === 0 && debt.improvements.length === 0) {
console.log("session accessor boundary guard passed.");
return;
}
console.error("Found legacy session store usage in session-accessor migrated files:");
for (const violation of violations) {
console.error(`- ${violation.path}:${violation.line}: ${violation.reason}`);
if (violations.length > 0) {
console.error("Found legacy session store usage in session-accessor migrated files:");
for (const violation of violations) {
console.error(`- ${violation.path}:${violation.line}: ${violation.reason}`);
}
console.error(
"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.",
);
}
if (debt.regressions.length > 0) {
console.error(
`Found new legacy session call sites in unmigrated files (counts exceed ${sessionAccessorDebtBaselineRelativePath}):`,
);
for (const regression of debt.regressions) {
console.error(
`- ${regression.path} [${regression.concern}]: ${regression.currentCount} legacy call site(s), baseline allows ${regression.baselineCount}`,
);
}
console.error(
`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.`,
);
}
if (debt.improvements.length > 0) {
for (const line of formatSessionAccessorDebtImprovements(debt.improvements)) {
console.error(line);
}
}
console.error(
"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.",
);
process.exit(1);
}
@@ -0,0 +1,81 @@
{
"embeddedAgentSessionTarget": {},
"memoryHostSessionCorpus": {},
"sessionAccessorRead": {
"extensions/telegram/src/bot-deps.ts": 1,
"extensions/telegram/src/bot-message-context.session-recreate.test-support.ts": 2,
"src/acp/control-plane/manager.background-task.ts": 2,
"src/acp/control-plane/manager.core.ts": 1,
"src/agents/agent-command.ts": 2,
"src/agents/bash-tools.exec-approval-followup.ts": 2,
"src/agents/command/session.ts": 5,
"src/agents/main-session-restart-recovery.ts": 2,
"src/agents/subagent-announce-delivery.ts": 3,
"src/agents/subagent-announce-output.ts": 3,
"src/agents/subagent-announce.test-support.ts": 1,
"src/agents/subagent-capabilities.ts": 2,
"src/agents/subagent-list.ts": 2,
"src/agents/subagent-orphan-recovery.ts": 2,
"src/agents/subagent-session-reconciliation.ts": 2,
"src/agents/subagent-spawn.ts": 2,
"src/agents/tools/transcripts-tool.ts": 2,
"src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts": 2,
"src/auto-reply/reply/commands-export-common.ts": 2,
"src/auto-reply/reply/commands-name.ts": 2,
"src/auto-reply/reply/commands-session-store.ts": 2,
"src/auto-reply/reply/dispatch-acp-transcript.runtime.ts": 4,
"src/auto-reply/reply/dispatch-from-config.ts": 6,
"src/auto-reply/reply/get-reply-fast-path.ts": 4,
"src/auto-reply/reply/get-reply-run.ts": 2,
"src/commands/doctor-heartbeat-session-target.ts": 2,
"src/commands/doctor-state-integrity.ts": 2,
"src/commands/doctor/shared/codex-route-warnings.ts": 3,
"src/config/sessions/cleanup-service.ts": 4,
"src/config/sessions/session-accessor.ts": 25,
"src/config/sessions/session-registry-maintenance.ts": 2,
"src/config/sessions/store-load.ts": 4,
"src/config/sessions/store.ts": 15,
"src/config/sessions/test-helpers.ts": 2,
"src/config/sessions/transcript.ts": 6,
"src/cron/isolated-agent/session.ts": 3,
"src/infra/approval-request-account-binding.ts": 2,
"src/infra/heartbeat-runner.ts": 4,
"src/plugins/runtime/runtime-agent.ts": 1
},
"sessionAccessorWrite": {
"src/agents/embedded-agent-subscribe.handlers.compaction.runtime.ts": 2,
"src/agents/session-suspension.ts": 2,
"src/agents/subagent-orphan-recovery.ts": 3,
"src/agents/subagent-spawn.ts": 2,
"src/auto-reply/reply/commands-name.ts": 2,
"src/auto-reply/reply/dispatch-from-config.ts": 2,
"src/auto-reply/reply/session-fork.ts": 2,
"src/commands/doctor-heartbeat-main-session-repair.ts": 2,
"src/commands/doctor-session-state-providers.ts": 2,
"src/commands/doctor-state-integrity.ts": 2,
"src/commands/doctor/shared/codex-route-warnings.ts": 2,
"src/config/sessions/plugin-host-cleanup.ts": 2,
"src/config/sessions/session-accessor.ts": 11,
"src/config/sessions/session-file.ts": 2,
"src/config/sessions/session-registry-maintenance.ts": 2,
"src/gateway/server-methods/agent.ts": 2,
"src/gateway/server-methods/chat.ts": 2,
"src/gateway/server-methods/sessions.ts": 3,
"src/gateway/session-lifecycle-state.ts": 2,
"src/gateway/test-helpers.mocks.ts": 1,
"src/plugins/runtime/runtime-agent.ts": 2
},
"sessionCompactManualTrim": {},
"sessionLifecycleCleanup": {
"src/config/sessions/store-maintenance-operations.ts": 2,
"src/config/sessions/store.ts": 5
},
"transcriptWriter": {
"src/agents/embedded-agent-runner/compaction-hooks.ts": 2,
"src/agents/embedded-agent-runner/thinking-replay-repair.ts": 2,
"src/agents/embedded-agent-runner/tool-result-truncation.ts": 3,
"src/agents/embedded-agent-runner/transcript-rewrite.ts": 2,
"src/config/sessions/session-accessor.ts": 7,
"src/config/sessions/store.ts": 2
}
}
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
allowedSessionStoreRuntimeFileBackedCompatExports,
collectSessionStoreRuntimeFileBackedCompatExports,
compareSessionAccessorDebt,
findGatewaySessionCreateLifecycleViolations,
findEmbeddedAgentSessionTargetViolations,
findMemoryHostSessionCorpusBoundaryViolations,
@@ -11,6 +12,7 @@ import {
findSessionLifecycleCleanupBoundaryViolations,
findSessionStoreRuntimeFileBackedCompatExportViolations,
findTranscriptWriterBoundaryViolations,
formatSessionAccessorDebtImprovements,
migratedBundledPluginSessionAccessorFiles,
migratedEmbeddedAgentSessionTargetFiles,
migratedMemoryHostSessionCorpusFiles,
@@ -574,3 +576,55 @@ describe("session accessor boundary guard", () => {
).toEqual([]);
});
});
describe("session accessor debt ratchet", () => {
it("flags unmigrated files whose legacy call-site count exceeds the baseline", () => {
expect(
compareSessionAccessorDebt(
{
sessionAccessorRead: { "src/a.ts": 3 },
sessionAccessorWrite: { "src/new.ts": 1 },
},
{
sessionAccessorRead: { "src/a.ts": 2 },
sessionAccessorWrite: {},
},
),
).toEqual({
regressions: [
{ concern: "sessionAccessorRead", path: "src/a.ts", currentCount: 3, baselineCount: 2 },
{ concern: "sessionAccessorWrite", path: "src/new.ts", currentCount: 1, baselineCount: 0 },
],
improvements: [],
});
});
it("passes when counts match the baseline", () => {
expect(
compareSessionAccessorDebt(
{ sessionAccessorRead: { "src/a.ts": 2 } },
{ sessionAccessorRead: { "src/a.ts": 2 } },
),
).toEqual({ regressions: [], improvements: [] });
});
it("fails with a regen instruction when counts drop below the baseline", () => {
const debt = compareSessionAccessorDebt(
{ sessionAccessorRead: { "src/a.ts": 1 } },
{ sessionAccessorRead: { "src/a.ts": 2, "src/gone.ts": 3 } },
);
expect(debt).toEqual({
regressions: [],
improvements: [
{ concern: "sessionAccessorRead", path: "src/a.ts", currentCount: 1, baselineCount: 2 },
{ concern: "sessionAccessorRead", path: "src/gone.ts", currentCount: 0, baselineCount: 3 },
],
});
expect(formatSessionAccessorDebtImprovements(debt.improvements)).toEqual([
"Legacy session accessor debt dropped below scripts/lib/session-accessor-debt-baseline.json:",
"- src/a.ts [sessionAccessorRead]: 1 legacy call site(s), stale baseline allows 2",
"- src/gone.ts [sessionAccessorRead]: 0 legacy call site(s), stale baseline allows 3",
"Run `pnpm lint:tmp:session-accessor-boundary:gen` to ratchet the baseline down and commit it.",
]);
});
});