mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(doctor): enforce deprecation registry deadlines (#120868)
This commit is contained in:
committed by
GitHub
parent
642b486986
commit
26ee1b4935
@@ -192,8 +192,9 @@ every human `Thanks @...` attribution.
|
||||
9. Check release-note side conditions:
|
||||
- inspect `src/plugins/compat/registry.ts`
|
||||
- inspect `src/commands/doctor/shared/deprecation-compat.ts`
|
||||
- if any compatibility `removeAfter` is on/before release date, resolve it
|
||||
or explicitly record the blocker before shipping
|
||||
- if a deprecated compatibility record reaches `removeAfter`, remove it when
|
||||
proven safe or move it to `removal-pending` and record the blocker; keep a
|
||||
due `removal-pending` record only until its documented conditions are met
|
||||
10. Validate and ship:
|
||||
|
||||
- after the manifest-driven rewrite, regenerate and verify the complete
|
||||
|
||||
@@ -103,11 +103,13 @@ a workflow fix that the existing parent run cannot consume.
|
||||
returns to the Code SHA loop.
|
||||
- During release planning, inspect both `src/plugins/compat/registry.ts` and
|
||||
`src/commands/doctor/shared/deprecation-compat.ts` before branching and again
|
||||
before final publish. For every deprecated or removal-pending compatibility
|
||||
record whose `removeAfter` date is on or before the release date, either
|
||||
remove the compatibility path where safe and validate the affected tests, or
|
||||
write down why removal is blocked and get explicit maintainer approval before
|
||||
shipping the expired compatibility path.
|
||||
before final publish. For every `deprecated` compatibility record whose
|
||||
`removeAfter` date is on or before the release date, either remove the
|
||||
compatibility path where safe and validate the affected tests, or change it
|
||||
to `removal-pending`, document the blocker, and get explicit maintainer
|
||||
approval. Revalidate every due `removal-pending` record's blocker and upgrade
|
||||
conditions before shipping; keep it only with explicit maintainer approval
|
||||
until those conditions are met.
|
||||
- When removing deprecated runtime/config compatibility, preserve any doctor
|
||||
migration, repair, or hint that is still needed by supported upgrade paths.
|
||||
Doctor-side compatibility should stay tracked in
|
||||
|
||||
@@ -2227,6 +2227,14 @@ jobs:
|
||||
else
|
||||
echo "[skip] historical target predates the script declaration contract"
|
||||
fi
|
||||
if [[ "$HISTORICAL_TARGET" == "true" ]]; then
|
||||
echo "[skip] historical target skips the wall-clock doctor deprecation registry guard"
|
||||
elif has_package_script "check:doctor-deprecation-registry"; then
|
||||
pnpm check:doctor-deprecation-registry
|
||||
else
|
||||
echo "Current CI targets must provide the check:doctor-deprecation-registry package script." >&2
|
||||
exit 1
|
||||
fi
|
||||
pnpm tool-display:check
|
||||
pnpm check:host-env-policy:swift
|
||||
pnpm dup:check:coverage
|
||||
|
||||
@@ -36,6 +36,13 @@ Doctor repair and migration compatibility is tracked separately at
|
||||
config shapes, install-ledger layouts, and repair shims that may need to
|
||||
stay available after the runtime compatibility path is removed.
|
||||
|
||||
Every doctor compatibility record declares `introduced` and `removeAfter`.
|
||||
The `pnpm check:doctor-deprecation-registry` guard fails when a record is still
|
||||
`deprecated` on or after `removeAfter`; maintainers must either remove it after
|
||||
supported-upgrade proof or move it to `removal-pending` with a documented
|
||||
blocker. `removal-pending` records do not fail the date guard, but remain in the
|
||||
explicit review queue until their upgrade conditions are met.
|
||||
|
||||
Release sweeps should check both registries. Do not delete a doctor
|
||||
migration just because the matching runtime or config compatibility record
|
||||
expired; first verify there is no supported upgrade path that still needs
|
||||
|
||||
@@ -1510,6 +1510,7 @@
|
||||
"check:database-first-legacy-stores": "node scripts/check-database-first-legacy-stores.mjs",
|
||||
"check:deprecated-api-usage": "node scripts/check-deprecated-api-usage.mjs",
|
||||
"check:deprecated-jsdoc": "node scripts/check-deprecated-jsdoc.mjs",
|
||||
"check:doctor-deprecation-registry": "node --import tsx scripts/check-doctor-deprecation-registry.ts",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-mdx && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
"check:env-var-count": "node scripts/check-env-var-count.mjs",
|
||||
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
|
||||
|
||||
@@ -534,6 +534,7 @@ export function createChangedCheckPlan(result, options = {}) {
|
||||
]);
|
||||
}
|
||||
add("changelog attributions", ["check:changelog-attributions"]);
|
||||
add("doctor deprecation registry", ["check:doctor-deprecation-registry"]);
|
||||
add("guarded extension wildcard re-exports", ["lint:extensions:no-guarded-wildcard-reexports"]);
|
||||
add("plugin-sdk wildcard re-exports", ["lint:extensions:no-plugin-sdk-wildcard-reexports"]);
|
||||
add("duplicate scan target coverage", ["dup:check:coverage"]);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
listDoctorDeprecationCompatRecords,
|
||||
type DoctorDeprecationCompatRecord,
|
||||
} from "../src/commands/doctor/shared/deprecation-compat.js";
|
||||
|
||||
type DeadlineRecord = Pick<DoctorDeprecationCompatRecord, "code" | "status" | "removeAfter">;
|
||||
type ExpiredDeadlineRecord = DeadlineRecord & { removeAfter: string };
|
||||
|
||||
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
|
||||
|
||||
export function findExpiredDeprecatedDoctorRecords(
|
||||
records: readonly DeadlineRecord[],
|
||||
asOf: string,
|
||||
): ExpiredDeadlineRecord[] {
|
||||
return records
|
||||
.filter(
|
||||
(record): record is ExpiredDeadlineRecord =>
|
||||
record.status === "deprecated" &&
|
||||
record.removeAfter !== undefined &&
|
||||
record.removeAfter <= asOf,
|
||||
)
|
||||
.toSorted(
|
||||
(left, right) =>
|
||||
left.removeAfter.localeCompare(right.removeAfter) || left.code.localeCompare(right.code),
|
||||
);
|
||||
}
|
||||
|
||||
function isUtcDate(value: string): boolean {
|
||||
if (!DATE_PATTERN.test(value)) {
|
||||
return false;
|
||||
}
|
||||
const timestamp = Date.parse(`${value}T00:00:00Z`);
|
||||
return Number.isFinite(timestamp) && new Date(timestamp).toISOString().slice(0, 10) === value;
|
||||
}
|
||||
|
||||
function parseAsOf(argv: readonly string[]): string | undefined {
|
||||
if (argv.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (argv.length === 2 && argv[0] === "--as-of" && isUtcDate(argv[1] ?? "")) {
|
||||
return argv[1];
|
||||
}
|
||||
throw new Error(
|
||||
"Usage: node --import tsx scripts/check-doctor-deprecation-registry.ts [--as-of YYYY-MM-DD]",
|
||||
);
|
||||
}
|
||||
|
||||
function main(argv = process.argv.slice(2)): void {
|
||||
let requestedAsOf: string | undefined;
|
||||
try {
|
||||
requestedAsOf = parseAsOf(argv);
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
const asOf = requestedAsOf ?? new Date().toISOString().slice(0, 10);
|
||||
const expired = findExpiredDeprecatedDoctorRecords(listDoctorDeprecationCompatRecords(), asOf);
|
||||
if (expired.length === 0) {
|
||||
console.log(`[doctor-deprecation-registry] OK as of ${asOf}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(
|
||||
`[doctor-deprecation-registry] ${expired.length} deprecated record(s) reached removeAfter by ${asOf}:`,
|
||||
);
|
||||
for (const record of expired) {
|
||||
console.error(`- ${record.code}: removeAfter ${record.removeAfter}`);
|
||||
}
|
||||
console.error(
|
||||
"Remove each migration after supported-upgrade proof, or move it to removal-pending with a documented blocker.",
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
main();
|
||||
}
|
||||
@@ -89,6 +89,7 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
{ name: "max-lines suppression ratchet", args: ["check:max-lines-ratchet"] },
|
||||
{ name: "changelog attributions", args: ["check:changelog-attributions"] },
|
||||
{ name: "database-first legacy-store guard", args: ["check:database-first-legacy-stores"] },
|
||||
{ name: "doctor deprecation registry", args: ["check:doctor-deprecation-registry"] },
|
||||
{
|
||||
name: "guarded extension wildcard re-exports",
|
||||
args: ["lint:extensions:no-guarded-wildcard-reexports"],
|
||||
|
||||
@@ -10,6 +10,12 @@ import {
|
||||
|
||||
const datePattern = /^\d{4}-\d{2}-\d{2}$/u;
|
||||
|
||||
function addUtcMonths(date: string, months: number): string {
|
||||
const next = new Date(`${date}T00:00:00Z`);
|
||||
next.setUTCMonth(next.getUTCMonth() + months);
|
||||
return next.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
const requiredDoctorCompatCodes = [
|
||||
"doctor-agent-runtime-embedded-harness",
|
||||
"doctor-agent-embedded-pi-config",
|
||||
@@ -24,21 +30,12 @@ const requiredDoctorCompatCodes = [
|
||||
"doctor-x-search-plugin-config",
|
||||
] as const;
|
||||
|
||||
function parseDate(date: string): Date {
|
||||
return new Date(`${date}T00:00:00Z`);
|
||||
}
|
||||
|
||||
function addUtcMonths(date: Date, months: number): Date {
|
||||
const next = new Date(date);
|
||||
next.setUTCMonth(next.getUTCMonth() + months);
|
||||
return next;
|
||||
}
|
||||
|
||||
describe("doctor deprecation compatibility inventory", () => {
|
||||
it("keeps compatibility codes unique and lookup-safe", () => {
|
||||
const records = listDoctorDeprecationCompatRecords();
|
||||
const codes = records.map((record) => record.code);
|
||||
|
||||
expect(records).toHaveLength(43);
|
||||
expect(new Set(codes).size).toBe(codes.length);
|
||||
expect(isDoctorDeprecationCompatCode("doctor-web-search-plugin-config")).toBe(true);
|
||||
expect(isDoctorDeprecationCompatCode("missing-code")).toBe(false);
|
||||
@@ -53,26 +50,28 @@ describe("doctor deprecation compatibility inventory", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("requires dated deprecation metadata with a three-month maximum window", () => {
|
||||
it("keeps dated deprecation metadata in chronological order", () => {
|
||||
for (const record of listDeprecatedDoctorDeprecationCompatRecords()) {
|
||||
expect(record.introduced, record.code).toMatch(datePattern);
|
||||
expect(record.deprecated, record.code).toMatch(datePattern);
|
||||
expect(record.warningStarts, record.code).toMatch(datePattern);
|
||||
expect(record.removeAfter, record.code).toMatch(datePattern);
|
||||
if (!record.warningStarts || !record.removeAfter) {
|
||||
if (!record.deprecated || !record.warningStarts || !record.removeAfter) {
|
||||
throw new Error(`${record.code} is missing deprecation window dates`);
|
||||
}
|
||||
const maxRemoveAfter = addUtcMonths(parseDate(record.warningStarts), 3);
|
||||
const removeAfter = parseDate(record.removeAfter);
|
||||
expect(removeAfter <= maxRemoveAfter, record.code).toBe(true);
|
||||
expect(record.introduced <= record.deprecated, record.code).toBe(true);
|
||||
expect(record.deprecated <= record.warningStarts, record.code).toBe(true);
|
||||
expect(record.warningStarts <= record.removeAfter, record.code).toBe(true);
|
||||
expect(record.removeAfter <= addUtcMonths(record.warningStarts, 3), record.code).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps every record actionable", () => {
|
||||
for (const record of listDoctorDeprecationCompatRecords()) {
|
||||
expect(record.introduced, record.code).toMatch(datePattern);
|
||||
expect(record.source, record.code).not.toBe("");
|
||||
expect(record.migration, record.code).not.toBe("");
|
||||
expect(record.replacement, record.code).not.toBe("");
|
||||
expect(record.source, record.code).toMatch(/\S/u);
|
||||
expect(record.migration, record.code).toMatch(/\S/u);
|
||||
expect(record.replacement, record.code).toMatch(/\S/u);
|
||||
expect(record.docsPath, record.code).toMatch(/^\//u);
|
||||
expect(fs.existsSync(record.migration), `${record.code}: ${record.migration}`).toBe(true);
|
||||
expect(record.tests.length, record.code).toBeGreaterThan(0);
|
||||
|
||||
@@ -33,8 +33,6 @@ export type DoctorDeprecationCompatRecord<Code extends string = string> = {
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
const TODAY = "2026-04-26";
|
||||
const MAX_REMOVE_AFTER = "2026-07-26";
|
||||
const DEFAULT_TESTS = ["src/commands/doctor/shared/legacy-config-migrate.test.ts"] as const;
|
||||
|
||||
function deprecatedCompatRecord<Code extends string>(
|
||||
@@ -43,22 +41,16 @@ function deprecatedCompatRecord<Code extends string>(
|
||||
DoctorDeprecationCompatRecord<Code>,
|
||||
"code" | "introduced" | "deprecated" | "warningStarts" | "removeAfter" | "status" | "tests"
|
||||
> &
|
||||
Required<Pick<DoctorDeprecationCompatRecord<Code>, "introduced" | "removeAfter">> &
|
||||
Partial<
|
||||
Pick<
|
||||
DoctorDeprecationCompatRecord<Code>,
|
||||
"introduced" | "deprecated" | "removeAfter" | "status" | "warningStarts" | "tests"
|
||||
>
|
||||
Pick<DoctorDeprecationCompatRecord<Code>, "deprecated" | "status" | "warningStarts" | "tests">
|
||||
>,
|
||||
): DoctorDeprecationCompatRecord<Code> {
|
||||
const introduced = record.introduced ?? TODAY;
|
||||
const deprecated = record.deprecated ?? (record.removeAfter ? introduced : TODAY);
|
||||
return {
|
||||
code,
|
||||
status: "deprecated",
|
||||
introduced,
|
||||
deprecated,
|
||||
warningStarts: deprecated,
|
||||
removeAfter: MAX_REMOVE_AFTER,
|
||||
deprecated: record.introduced,
|
||||
warningStarts: record.introduced,
|
||||
tests: DEFAULT_TESTS,
|
||||
...record,
|
||||
};
|
||||
@@ -245,8 +237,10 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
tests: ["src/config/dead-config-keys.test.ts"],
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-agent-llm-timeout", {
|
||||
status: "removal-pending",
|
||||
owner: "agent-runtime",
|
||||
introduced: "2026-04-27",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "agents.defaults.llm.idleTimeoutSeconds",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts",
|
||||
replacement: "models.providers.<id>.timeoutSeconds",
|
||||
@@ -255,8 +249,12 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
"The old agent-level idle timeout knob was collapsed into provider request timeout handling, bounded by the agent/run timeout ceiling.",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-agent-runtime-embedded-harness", {
|
||||
status: "removal-pending",
|
||||
owner: "agent-runtime",
|
||||
introduced: "2026-04-25",
|
||||
deprecated: "2026-04-26",
|
||||
warningStarts: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "agents.defaults.embeddedHarness; agents.list[].embeddedHarness",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts",
|
||||
replacement: "models.providers.<provider>.agentRuntime or model-scoped agentRuntime",
|
||||
@@ -265,8 +263,10 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
"Whole-agent runtime pins are retired; doctor preserves intent only when it can move the value to provider/model runtime policy.",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-agent-embedded-pi-config", {
|
||||
status: "removal-pending",
|
||||
owner: "agent-runtime",
|
||||
introduced: "2026-05-21",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "agents.defaults.embeddedPi; agents.list[].embeddedPi",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts",
|
||||
replacement: "agents.defaults.embeddedAgent; agents.list[].embeddedAgent",
|
||||
@@ -275,7 +275,10 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
"Runtime code no longer reads the legacy key; doctor keeps this migration only to preserve shipped configs during upgrade.",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-agent-sandbox-persession", {
|
||||
status: "removal-pending",
|
||||
owner: "agent-runtime",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "agents.defaults.sandbox.perSession; agents.list[].sandbox.perSession",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts",
|
||||
replacement: "agents.*.sandbox.scope",
|
||||
@@ -300,15 +303,20 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
docsPath: "/concepts/typing-indicators",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-top-level-heartbeat", {
|
||||
status: "removal-pending",
|
||||
owner: "config",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "heartbeat",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts",
|
||||
replacement: "agents.defaults.heartbeat and channels.defaults.heartbeat",
|
||||
docsPath: "/automation",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-mcp-server-type-alias", {
|
||||
status: "removal-pending",
|
||||
owner: "config",
|
||||
introduced: "2026-04-27",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "mcp.servers.*.type",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.mcp.ts",
|
||||
replacement: "mcp.servers.*.transport",
|
||||
@@ -317,36 +325,50 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
"OpenClaw stores transport names; CLI backends receive their own type fields through runtime adapters.",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-gateway-bind-host-aliases", {
|
||||
status: "removal-pending",
|
||||
owner: "gateway",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "gateway.bind host aliases such as 0.0.0.0 and localhost",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.gateway.ts",
|
||||
replacement: "gateway.bind.mode values such as lan, loopback, custom, tailnet, and auto",
|
||||
docsPath: "/gateway/configuration",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-audio-transcription-command", {
|
||||
status: "removal-pending",
|
||||
owner: "audio",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "audio.transcription",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.audio.ts",
|
||||
replacement: "capability-tagged tools.media.models",
|
||||
docsPath: "/tools/media-overview",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-channel-thread-binding-ttl", {
|
||||
status: "removal-pending",
|
||||
owner: "channel",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "threadBindings.ttlHours",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.channels.ts",
|
||||
replacement: "threadBindings.idleHours",
|
||||
docsPath: "/channels/channel-routing",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-message-queue-steering-modes", {
|
||||
status: "removal-pending",
|
||||
owner: "config",
|
||||
introduced: "2026-05-04",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "messages.queue.mode and messages.queue.byChannel retired queue modes",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.queue.ts",
|
||||
replacement: "steer, followup, collect, or interrupt queue modes",
|
||||
docsPath: "/concepts/queue",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-channel-dm-aliases", {
|
||||
status: "removal-pending",
|
||||
owner: "channel",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "channels.<id>.dm.policy and channels.<id>.dm.allowFrom",
|
||||
migration: "src/config/channel-compat-normalization.ts",
|
||||
replacement: "channels.<id>.dmPolicy and channels.<id>.allowFrom",
|
||||
@@ -354,7 +376,10 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
tests: ["src/commands/doctor/shared/channel-legacy-config-migrate.test.ts"],
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-channel-streaming-aliases", {
|
||||
status: "removal-pending",
|
||||
owner: "channel",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "streamMode, scalar streaming, chunkMode, blockStreaming, draftChunk, nativeStreaming",
|
||||
migration: "src/config/channel-compat-normalization.ts",
|
||||
replacement: "channels.<id>.streaming.*",
|
||||
@@ -368,6 +393,7 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
owner: "channel",
|
||||
introduced: "2026-05-18",
|
||||
deprecated: "2026-05-31",
|
||||
warningStarts: "2026-05-31",
|
||||
removeAfter: "2026-08-31",
|
||||
source: "channels.webchat",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.channels.ts",
|
||||
@@ -387,15 +413,20 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
tests: ["src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts"],
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-tts-provider-aliases", {
|
||||
status: "removal-pending",
|
||||
owner: "tts",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "messages.tts.openai/elevenlabs/edge and plugins.entries.voice-call.config.tts aliases",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.tts.ts",
|
||||
replacement: "tts.providers.<provider> and microsoft instead of edge",
|
||||
docsPath: "/tools/tts",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-tts-enabled-auto-mode", {
|
||||
status: "removal-pending",
|
||||
owner: "tts",
|
||||
introduced: "2026-04-29",
|
||||
removeAfter: "2026-07-26",
|
||||
source:
|
||||
"messages.tts.enabled, agents.list[].tts.enabled, supported channel TTS enabled fields, and voice-call plugin tts.enabled",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.tts.ts",
|
||||
@@ -405,8 +436,10 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
tests: ["src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts"],
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-tts-speaker-selection-fields", {
|
||||
status: "removal-pending",
|
||||
owner: "tts",
|
||||
introduced: "2026-05-28",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "TTS provider speaker selection fields named voice, voiceName, and voiceId",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.tts.ts",
|
||||
replacement: "speakerVoice and speakerVoiceId",
|
||||
@@ -414,8 +447,12 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
tests: ["src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts"],
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-plugin-install-config-ledger", {
|
||||
status: "removal-pending",
|
||||
owner: "plugin",
|
||||
introduced: "2026-04-25",
|
||||
deprecated: "2026-04-26",
|
||||
warningStarts: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "plugins.installs in authored config",
|
||||
migration: "src/config/plugin-install-config-migration.ts",
|
||||
replacement: "shared SQLite installed_plugin_index install ledger",
|
||||
@@ -426,8 +463,12 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
],
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-bundled-plugin-load-paths", {
|
||||
status: "removal-pending",
|
||||
owner: "plugin",
|
||||
introduced: "2026-04-25",
|
||||
deprecated: "2026-04-26",
|
||||
warningStarts: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "plugins.load.paths entries that point at bundled plugin source/dist locations",
|
||||
migration: "src/commands/doctor/shared/bundled-plugin-load-paths.ts",
|
||||
replacement: "packaged bundled plugins and the persisted plugin registry",
|
||||
@@ -435,8 +476,12 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
tests: ["src/commands/doctor/shared/bundled-plugin-load-paths.test.ts"],
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-bundled-provider-discovery-allowlist", {
|
||||
status: "removal-pending",
|
||||
owner: "plugin",
|
||||
introduced: "2026-04-25",
|
||||
deprecated: "2026-04-26",
|
||||
warningStarts: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "plugins.allow configs created before bundled provider discovery was explicit",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.providers.ts",
|
||||
replacement: "plugins.bundledDiscovery allowlist mode plus explicit plugin/provider entries",
|
||||
@@ -448,6 +493,7 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
owner: "plugin",
|
||||
introduced: "2026-05-29",
|
||||
deprecated: "2026-07-09",
|
||||
warningStarts: "2026-07-09",
|
||||
removeAfter: "2026-10-09",
|
||||
source: "plugins.entries.codex-supervisor and codex-supervisor plugin policy references",
|
||||
migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.providers.ts",
|
||||
@@ -457,7 +503,10 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
"The core bootstrap migration must remain available when the external Codex plugin is not installed yet.",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-web-search-plugin-config", {
|
||||
status: "removal-pending",
|
||||
owner: "provider",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "tools.web.search.apiKey and tools.web.search.<provider>",
|
||||
migration: "src/commands/doctor/shared/legacy-web-search-migrate.ts",
|
||||
replacement: "plugins.entries.<plugin>.config.webSearch",
|
||||
@@ -467,7 +516,10 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
"Provider/plugin ownership can move as bundled providers externalize; verify the current manifest owner before deleting migration support.",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-web-fetch-plugin-config", {
|
||||
status: "removal-pending",
|
||||
owner: "provider",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "tools.web.fetch.firecrawl",
|
||||
migration: "src/commands/doctor/shared/legacy-web-fetch-migrate.ts",
|
||||
replacement: "plugins.entries.firecrawl.config.webFetch",
|
||||
@@ -475,7 +527,10 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
tests: ["src/commands/doctor/shared/legacy-web-fetch-migrate.test.ts"],
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-x-search-plugin-config", {
|
||||
status: "removal-pending",
|
||||
owner: "provider",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "tools.web.x_search.apiKey",
|
||||
migration: "src/commands/doctor/shared/legacy-x-search-migrate.ts",
|
||||
replacement: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
@@ -486,14 +541,20 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [
|
||||
],
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-talk-provider-shape", {
|
||||
status: "removal-pending",
|
||||
owner: "tts",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "legacy talk provider scalar fields and provider/provider ids",
|
||||
migration: "src/commands/doctor/shared/legacy-talk-config-normalizer.ts",
|
||||
replacement: "talk.providers.<provider>",
|
||||
docsPath: "/tools/tts",
|
||||
}),
|
||||
deprecatedCompatRecord("doctor-legacy-tools-by-sender", {
|
||||
status: "removal-pending",
|
||||
owner: "tools",
|
||||
introduced: "2026-04-26",
|
||||
removeAfter: "2026-07-26",
|
||||
source: "untyped toolsBySender keys",
|
||||
migration: "src/commands/doctor/shared/legacy-tools-by-sender.ts",
|
||||
replacement: "typed id:, e164:, username:, or name: sender keys",
|
||||
|
||||
@@ -1460,6 +1460,7 @@ describe("scripts/changed-lanes", () => {
|
||||
"environment variable count ratchet",
|
||||
"max-lines suppression ratchet",
|
||||
"changelog attributions",
|
||||
"doctor deprecation registry",
|
||||
"guarded extension wildcard re-exports",
|
||||
"plugin-sdk wildcard re-exports",
|
||||
"duplicate scan target coverage",
|
||||
@@ -1640,6 +1641,7 @@ describe("scripts/changed-lanes", () => {
|
||||
expect(plan.commands.map((command) => command.args[0])).toEqual([
|
||||
"check:no-conflict-markers",
|
||||
"check:changelog-attributions",
|
||||
"check:doctor-deprecation-registry",
|
||||
"lint:extensions:no-guarded-wildcard-reexports",
|
||||
"lint:extensions:no-plugin-sdk-wildcard-reexports",
|
||||
"dup:check:coverage",
|
||||
@@ -2314,6 +2316,7 @@ describe("scripts/changed-lanes", () => {
|
||||
expect(plan.commands).toEqual([
|
||||
{ name: "conflict markers", args: ["check:no-conflict-markers"] },
|
||||
{ name: "changelog attributions", args: ["check:changelog-attributions"] },
|
||||
{ name: "doctor deprecation registry", args: ["check:doctor-deprecation-registry"] },
|
||||
{
|
||||
name: "guarded extension wildcard re-exports",
|
||||
args: ["lint:extensions:no-guarded-wildcard-reexports"],
|
||||
@@ -2336,6 +2339,7 @@ describe("scripts/changed-lanes", () => {
|
||||
expect(plan.commands).toEqual([
|
||||
{ name: "conflict markers", args: ["check:no-conflict-markers"] },
|
||||
{ name: "changelog attributions", args: ["check:changelog-attributions"] },
|
||||
{ name: "doctor deprecation registry", args: ["check:doctor-deprecation-registry"] },
|
||||
{
|
||||
name: "guarded extension wildcard re-exports",
|
||||
args: ["lint:extensions:no-guarded-wildcard-reexports"],
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findExpiredDeprecatedDoctorRecords } from "../../scripts/check-doctor-deprecation-registry.js";
|
||||
import { listDoctorDeprecationCompatRecords } from "../../src/commands/doctor/shared/deprecation-compat.js";
|
||||
|
||||
const deadlineRecord = {
|
||||
code: "doctor-test-deadline",
|
||||
status: "deprecated",
|
||||
removeAfter: "2026-07-26",
|
||||
} as const;
|
||||
|
||||
describe("doctor deprecation registry guard", () => {
|
||||
it.each([
|
||||
["before", "2026-07-25", 0],
|
||||
["on", "2026-07-26", 1],
|
||||
["after", "2026-07-27", 1],
|
||||
])("handles the %s-deadline date", (_label, asOf, expectedCount) => {
|
||||
expect(findExpiredDeprecatedDoctorRecords([deadlineRecord], asOf)).toHaveLength(expectedCount);
|
||||
});
|
||||
|
||||
it("leaves removal-pending records in the explicit review queue", () => {
|
||||
expect(
|
||||
findExpiredDeprecatedDoctorRecords(
|
||||
[{ ...deadlineRecord, status: "removal-pending" }],
|
||||
"2026-08-08",
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the real registry current as of 2026-08-08", () => {
|
||||
const records = listDoctorDeprecationCompatRecords();
|
||||
const removalPending = records.filter((record) => record.status === "removal-pending");
|
||||
|
||||
expect(records).toHaveLength(43);
|
||||
expect(removalPending).toHaveLength(23);
|
||||
expect(new Set(removalPending.map((record) => record.removeAfter))).toEqual(
|
||||
new Set(["2026-07-26"]),
|
||||
);
|
||||
expect(findExpiredDeprecatedDoctorRecords(records, "2026-08-08")).toEqual([]);
|
||||
});
|
||||
|
||||
it("prints every offending code and date with actionable guidance", () => {
|
||||
const asOf = "9999-12-31";
|
||||
const offenders = findExpiredDeprecatedDoctorRecords(
|
||||
listDoctorDeprecationCompatRecords(),
|
||||
asOf,
|
||||
);
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["--import", "tsx", "scripts/check-doctor-deprecation-registry.ts", "--as-of", asOf],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
|
||||
expect(offenders.length).toBeGreaterThan(0);
|
||||
expect(result.status).toBe(1);
|
||||
for (const offender of offenders) {
|
||||
expect(result.stderr).toContain(`${offender.code}: removeAfter ${offender.removeAfter}`);
|
||||
}
|
||||
expect(result.stderr).toContain("Remove each migration after supported-upgrade proof");
|
||||
expect(result.stderr).toContain("move it to removal-pending with a documented blocker");
|
||||
});
|
||||
});
|
||||
@@ -4377,6 +4377,17 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
expect(preflightGuards).toContain(
|
||||
"Current CI targets must provide the check:script-declarations package script.",
|
||||
);
|
||||
expect(preflightGuards).toContain('has_package_script "check:doctor-deprecation-registry"');
|
||||
expect(preflightGuards).toContain("pnpm check:doctor-deprecation-registry");
|
||||
expect(preflightGuards).toContain(
|
||||
"[skip] historical target skips the wall-clock doctor deprecation registry guard",
|
||||
);
|
||||
expect(preflightGuards).toContain(
|
||||
"Current CI targets must provide the check:doctor-deprecation-registry package script.",
|
||||
);
|
||||
expect(preflightGuards.indexOf('if [[ "$HISTORICAL_TARGET" == "true" ]]')).toBeLessThan(
|
||||
preflightGuards.indexOf("pnpm check:doctor-deprecation-registry"),
|
||||
);
|
||||
expect(npmLockGuards).toContain("pnpm deps:npm-lock:check");
|
||||
expect(preflightGuards).toContain("pnpm deps:patches:check");
|
||||
expect(parsedWorkflow.jobs.preflight.outputs.diff_base_revision).toBe(
|
||||
|
||||
Reference in New Issue
Block a user