refactor(settings): make project imports pluggable

This commit is contained in:
Mohamed Boudra
2026-07-17 14:42:02 +02:00
parent f7a6548b68
commit f030ddefb1
53 changed files with 2128 additions and 1546 deletions
@@ -34,94 +34,37 @@ async function ensureZodAotRuntimeImportExtensionPatch() {
async function ensureZodAotDiscriminatedUnionOutputPatch() {
let discriminatedUnionEmitter = await readFile(discriminatedUnionPath, "utf8");
let changed = false;
if (
!discriminatedUnionEmitter.includes(
discriminatedUnionEmitter.includes(
"const needsOutputPropagation = ir.options.some(hasMutation);",
)
) {
const importBefore = 'import { escapeString } from "../context.js";';
const importAfter = 'import { escapeString, hasMutation } from "../context.js";';
const outputFlagBefore =
"const discKey = escapeString(ir.discriminator);\n let code = emit `";
const outputFlagAfter =
"const discKey = escapeString(ir.discriminator);\n const needsOutputPropagation = ir.options.some(hasMutation);\n let code = emit `";
const propagationBefore =
" ${g.visit(option, { input: objVar, output: objVar })}\n break;`;";
const propagationAfter =
' ${g.visit(option, { input: objVar, output: objVar })}\n ${needsOutputPropagation ? `${g.output}=${objVar};` : ""}\n break;`;';
if (
!discriminatedUnionEmitter.includes(importBefore) ||
!discriminatedUnionEmitter.includes(outputFlagBefore) ||
!discriminatedUnionEmitter.includes(propagationBefore)
) {
throw new Error("zod-aot discriminated-union emitter shape changed; update the output patch");
}
discriminatedUnionEmitter = discriminatedUnionEmitter
.replace(importBefore, importAfter)
.replace(outputFlagBefore, outputFlagAfter)
.replace(propagationBefore, propagationAfter);
changed = true;
return;
}
if (!discriminatedUnionEmitter.includes("function discriminatorValueLiteral(")) {
const helperBefore = 'import { invalidType } from "../emit-issue.js";\n';
const helperAfter = `${helperBefore}function discriminatorValueLiteral(value, option, discriminator) {
const property = option?.type === "object" ? option.properties?.[discriminator] : undefined;
if (property?.type === "literal" && property.values.length === 1) {
return JSON.stringify(property.values[0]);
}
return escapeString(value);
}
`;
const slowCaseBefore = " const option = ir.options[index];\n code += emit `";
const slowCaseAfter =
" const option = ir.options[index];\n const caseValue = discriminatorValueLiteral(value, option, ir.discriminator);\n code += emit `";
const slowCaseValueBefore = " case ${escapeString(value)}:";
const slowCaseValueAfter = " case ${caseValue}:";
const validValuesBefore = `const validValues = Object.keys(ir.mapping)
.map((v) => escapeString(v))
.join(",");`;
const validValuesAfter = `const validValues = Object.entries(ir.mapping)
.map(([v, i]) => discriminatorValueLiteral(v, ir.options[i], ir.discriminator))
.join(",");`;
const fastCaseBefore =
" const option = ir.options[index];\n const check = g.visit(option, { input: helperParam });";
const fastCaseAfter =
" const option = ir.options[index];\n const caseValue = discriminatorValueLiteral(value, option, ir.discriminator);\n const check = g.visit(option, { input: helperParam });";
const fastCaseValueBefore =
" cases.push(`case ${escapeString(value)}:return ${check};`);";
const fastCaseValueAfter = " cases.push(`case ${caseValue}:return ${check};`);";
const importBefore = 'import { escapeString } from "../context.js";';
const importAfter = 'import { escapeString, hasMutation } from "../context.js";';
const outputFlagBefore = "const discKey = escapeString(ir.discriminator);\n let code = emit `";
const outputFlagAfter =
"const discKey = escapeString(ir.discriminator);\n const needsOutputPropagation = ir.options.some(hasMutation);\n let code = emit `";
const propagationBefore =
" ${g.visit(option, { input: objVar, output: objVar })}\n break;`;";
const propagationAfter =
' ${g.visit(option, { input: objVar, output: objVar })}\n ${needsOutputPropagation ? `${g.output}=${objVar};` : ""}\n break;`;';
if (
!discriminatedUnionEmitter.includes(helperBefore) ||
!discriminatedUnionEmitter.includes(slowCaseBefore) ||
!discriminatedUnionEmitter.includes(slowCaseValueBefore) ||
!discriminatedUnionEmitter.includes(validValuesBefore) ||
!discriminatedUnionEmitter.includes(fastCaseBefore) ||
!discriminatedUnionEmitter.includes(fastCaseValueBefore)
) {
throw new Error(
"zod-aot discriminated-union emitter shape changed; update the literal patch",
);
}
discriminatedUnionEmitter = discriminatedUnionEmitter
.replace(helperBefore, helperAfter)
.replace(slowCaseBefore, slowCaseAfter)
.replace(slowCaseValueBefore, slowCaseValueAfter)
.replace(validValuesBefore, validValuesAfter)
.replace(fastCaseBefore, fastCaseAfter)
.replace(fastCaseValueBefore, fastCaseValueAfter);
changed = true;
if (
!discriminatedUnionEmitter.includes(importBefore) ||
!discriminatedUnionEmitter.includes(outputFlagBefore) ||
!discriminatedUnionEmitter.includes(propagationBefore)
) {
throw new Error("zod-aot discriminated-union emitter shape changed; update the output patch");
}
if (changed) {
await writeFile(discriminatedUnionPath, discriminatedUnionEmitter);
}
discriminatedUnionEmitter = discriminatedUnionEmitter
.replace(importBefore, importAfter)
.replace(outputFlagBefore, outputFlagAfter)
.replace(propagationBefore, propagationAfter);
await writeFile(discriminatedUnionPath, discriminatedUnionEmitter);
}
await Promise.all([
+17
View File
@@ -308,6 +308,23 @@ describe("agent detach RPC", () => {
}
expect(parsed.features?.importSessionWorkspaceTarget).toBe(true);
});
test("parses future project config import source advertisements", () => {
const parsed = parseServerInfoStatusPayload({
status: "server_info",
serverId: "srv-test",
features: {
projectConfigImportSources: [{ kind: "future-source", extra: { version: 2 } }],
},
});
if (!parsed) {
throw new Error("Expected server info payload to parse");
}
expect(parsed.features?.projectConfigImportSources).toEqual([
{ kind: "future-source", extra: { version: 2 } },
]);
});
});
describe("agent setting action responses", () => {
+8 -4
View File
@@ -66,6 +66,7 @@ import {
PaseoScriptEntryRawSchema,
PaseoWorktreeConfigRawSchema,
PaseoConfigRevisionSchema,
ProjectConfigImportAdvertisedSourceSchema,
ProjectConfigImportItemSchema,
ProjectConfigImportPreviewSchema,
ProjectConfigImportSourceSchema,
@@ -76,6 +77,7 @@ import {
type PaseoMetadataGenerationEntry,
type PaseoScriptEntryRaw,
type ProjectConfigImportInput,
type ProjectConfigImportAdvertisedSource,
type ProjectConfigImportItem,
type ProjectConfigImportPreview,
type ProjectConfigImportSource,
@@ -88,6 +90,7 @@ export {
PaseoMetadataGenerationSchema,
PaseoScriptEntryRawSchema,
PaseoWorktreeConfigRawSchema,
ProjectConfigImportAdvertisedSourceSchema,
ProjectConfigImportItemSchema,
ProjectConfigImportPreviewSchema,
ProjectConfigImportSourceSchema,
@@ -97,6 +100,7 @@ export {
type PaseoMetadataGenerationEntry,
type PaseoScriptEntryRaw,
type ProjectConfigImportInput,
type ProjectConfigImportAdvertisedSource,
type ProjectConfigImportItem,
type ProjectConfigImportPreview,
type ProjectConfigImportSource,
@@ -2706,8 +2710,8 @@ export const ServerInfoStatusPayloadSchema = z
providerRemoval: z.boolean().optional(),
// COMPAT(importSessionWorkspaceTarget): added in v0.1.110, remove gate after 2027-01-16.
importSessionWorkspaceTarget: z.boolean().optional(),
// COMPAT(projectConfigImportConductor): added in v0.1.110, drop the gate when floor >= v0.1.110.
projectConfigImportConductor: z.boolean().optional(),
// COMPAT(projectConfigImportSources): added in v0.1.110, remove the gate after 2027-01-17.
projectConfigImportSources: z.array(ProjectConfigImportAdvertisedSourceSchema).optional(),
// COMPAT(forgeProviders): added in v0.1.106, drop the gate when daemon floor >= v0.1.106.
// Daemon advertises pluggable non-GitHub forge support (the forge registry);
// the client gates non-GitHub setup UI on it.
@@ -3660,7 +3664,7 @@ export const WriteProjectConfigResponseMessageSchema = z.object({
export const GetProjectConfigImportResponseMessageSchema = z.object({
type: z.literal("project.config.get_import.response"),
payload: z.discriminatedUnion("ok", [
payload: z.union([
ProjectConfigImportPreviewSchema.extend({
requestId: z.string(),
ok: z.literal(true),
@@ -3676,7 +3680,7 @@ export const GetProjectConfigImportResponseMessageSchema = z.object({
export const ApplyProjectConfigImportResponseMessageSchema = z.object({
type: z.literal("project.config.apply_import.response"),
payload: z.discriminatedUnion("ok", [
payload: z.union([
z.object({
requestId: z.string(),
repoRoot: z.string(),
@@ -83,6 +83,10 @@ export const ProjectConfigImportSourceSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("conductor") }),
]);
export const ProjectConfigImportAdvertisedSourceSchema = z
.object({ kind: z.string().min(1) })
.passthrough();
export const ProjectConfigImportInputSchema = z.object({
role: z.string(),
relativePath: z.string(),
@@ -150,6 +154,9 @@ export type PaseoConfigRaw = z.infer<typeof PaseoConfigRawSchema>;
export type PaseoConfig = z.infer<typeof PaseoConfigSchema>;
export type PaseoConfigRevision = z.infer<typeof PaseoConfigRevisionSchema>;
export type ProjectConfigImportSource = z.infer<typeof ProjectConfigImportSourceSchema>;
export type ProjectConfigImportAdvertisedSource = z.infer<
typeof ProjectConfigImportAdvertisedSourceSchema
>;
export type ProjectConfigImportInput = z.infer<typeof ProjectConfigImportInputSchema>;
export type ProjectConfigImportItem = z.infer<typeof ProjectConfigImportItemSchema>;
export type ProjectConfigImportPreview = z.infer<typeof ProjectConfigImportPreviewSchema>;
@@ -82,24 +82,6 @@ const SourceSchema = z.discriminatedUnion("type", [
});
});
it("routes boolean discriminated-union branches with boolean switch cases", async () => {
const schema = await compileInlineSchema(`
const SourceSchema = z.discriminatedUnion("ok", [
z.object({ ok: z.literal(true), value: z.string() }),
z.object({ ok: z.literal(false), error: z.string() }),
]);
`);
expect(schema.safeParse({ ok: true, value: "yes" })).toMatchObject({
success: true,
data: { ok: true, value: "yes" },
});
expect(schema.safeParse({ ok: false, error: "no" })).toMatchObject({
success: true,
data: { ok: false, error: "no" },
});
});
it("routes tool-call-like status unions through the current sequential item union", async () => {
const schema = await compileInlineSchema(`
const ToolCallItemSchema = z.discriminatedUnion("status", [