refactor: unwrap Question namespace + fix script to emit "." for index.ts (#22992)

This commit is contained in:
Kit Langton
2026-04-17 01:51:02 +00:00
committed by GitHub
parent 9c87a144e8
commit 4f8986aa48
2 changed files with 200 additions and 195 deletions
@@ -207,10 +207,15 @@ const rewrittenBody = dedented.map(rewriteLine)
// Assemble the new file. Collapse multiple trailing blank lines so the // Assemble the new file. Collapse multiple trailing blank lines so the
// self-reexport sits cleanly at the end. // self-reexport sits cleanly at the end.
//
// When the file is itself `index.ts`, prefer `"."` over `"./index"` — both are
// valid but `"."` matches the existing convention in the codebase (e.g.
// pty/index.ts, file/index.ts, etc.) and avoids referencing "index" literally.
const basename = path.basename(absPath, ".ts") const basename = path.basename(absPath, ".ts")
const reexportSource = basename === "index" ? "." : `./${basename}`
const assembled = [...before, ...rewrittenBody, ...after].join("\n") const assembled = [...before, ...rewrittenBody, ...after].join("\n")
const trimmed = assembled.replace(/\s+$/g, "") const trimmed = assembled.replace(/\s+$/g, "")
const output = `${trimmed}\n\nexport * as ${nsName} from "./${basename}"\n` const output = `${trimmed}\n\nexport * as ${nsName} from "${reexportSource}"\n`
if (dryRun) { if (dryRun) {
console.log(`--- dry run: ${path.relative(process.cwd(), absPath)} ---`) console.log(`--- dry run: ${path.relative(process.cwd(), absPath)} ---`)
@@ -218,7 +223,7 @@ if (dryRun) {
console.log(`body lines: ${body.length}`) console.log(`body lines: ${body.length}`)
console.log(`declared names: ${Array.from(declaredNames).join(", ") || "(none)"}`) console.log(`declared names: ${Array.from(declaredNames).join(", ") || "(none)"}`)
console.log(`self-refs rewr: ${rewriteCount}`) console.log(`self-refs rewr: ${rewriteCount}`)
console.log(`self-reexport: export * as ${nsName} from "./${basename}"`) console.log(`self-reexport: export * as ${nsName} from "${reexportSource}"`)
console.log(`output preview (last 10 lines):`) console.log(`output preview (last 10 lines):`)
const outputLines = output.split("\n") const outputLines = output.split("\n")
for (const l of outputLines.slice(Math.max(0, outputLines.length - 10))) { for (const l of outputLines.slice(Math.max(0, outputLines.length - 10))) {
@@ -231,7 +236,7 @@ fs.writeFileSync(absPath, output)
console.log(`unwrapped ${path.relative(process.cwd(), absPath)}${nsName}`) console.log(`unwrapped ${path.relative(process.cwd(), absPath)}${nsName}`)
console.log(` body lines: ${body.length}`) console.log(` body lines: ${body.length}`)
console.log(` self-refs rewr: ${rewriteCount}`) console.log(` self-refs rewr: ${rewriteCount}`)
console.log(` self-reexport: export * as ${nsName} from "./${basename}"`) console.log(` self-reexport: export * as ${nsName} from "${reexportSource}"`)
console.log("") console.log("")
console.log("Next: verify with") console.log("Next: verify with")
console.log(" bunx --bun tsgo --noEmit") console.log(" bunx --bun tsgo --noEmit")
+44 -44
View File
@@ -8,23 +8,22 @@ import { Log } from "@/util"
import { withStatics } from "@/util/schema" import { withStatics } from "@/util/schema"
import { QuestionID } from "./schema" import { QuestionID } from "./schema"
export namespace Question { const log = Log.create({ service: "question" })
const log = Log.create({ service: "question" })
// Schemas // Schemas
export class Option extends Schema.Class<Option>("QuestionOption")({ export class Option extends Schema.Class<Option>("QuestionOption")({
label: Schema.String.annotate({ label: Schema.String.annotate({
description: "Display text (1-5 words, concise)", description: "Display text (1-5 words, concise)",
}), }),
description: Schema.String.annotate({ description: Schema.String.annotate({
description: "Explanation of choice", description: "Explanation of choice",
}), }),
}) { }) {
static readonly zod = zod(this) static readonly zod = zod(this)
} }
const base = { const base = {
question: Schema.String.annotate({ question: Schema.String.annotate({
description: "Complete question", description: "Complete question",
}), }),
@@ -37,87 +36,87 @@ export namespace Question {
multiple: Schema.optional(Schema.Boolean).annotate({ multiple: Schema.optional(Schema.Boolean).annotate({
description: "Allow selecting multiple choices", description: "Allow selecting multiple choices",
}), }),
} }
export class Info extends Schema.Class<Info>("QuestionInfo")({ export class Info extends Schema.Class<Info>("QuestionInfo")({
...base, ...base,
custom: Schema.optional(Schema.Boolean).annotate({ custom: Schema.optional(Schema.Boolean).annotate({
description: "Allow typing a custom answer (default: true)", description: "Allow typing a custom answer (default: true)",
}), }),
}) { }) {
static readonly zod = zod(this) static readonly zod = zod(this)
} }
export class Prompt extends Schema.Class<Prompt>("QuestionPrompt")(base) { export class Prompt extends Schema.Class<Prompt>("QuestionPrompt")(base) {
static readonly zod = zod(this) static readonly zod = zod(this)
} }
export class Tool extends Schema.Class<Tool>("QuestionTool")({ export class Tool extends Schema.Class<Tool>("QuestionTool")({
messageID: MessageID, messageID: MessageID,
callID: Schema.String, callID: Schema.String,
}) { }) {
static readonly zod = zod(this) static readonly zod = zod(this)
} }
export class Request extends Schema.Class<Request>("QuestionRequest")({ export class Request extends Schema.Class<Request>("QuestionRequest")({
id: QuestionID, id: QuestionID,
sessionID: SessionID, sessionID: SessionID,
questions: Schema.Array(Info).annotate({ questions: Schema.Array(Info).annotate({
description: "Questions to ask", description: "Questions to ask",
}), }),
tool: Schema.optional(Tool), tool: Schema.optional(Tool),
}) { }) {
static readonly zod = zod(this) static readonly zod = zod(this)
} }
export const Answer = Schema.Array(Schema.String) export const Answer = Schema.Array(Schema.String)
.annotate({ identifier: "QuestionAnswer" }) .annotate({ identifier: "QuestionAnswer" })
.pipe(withStatics((s) => ({ zod: zod(s) }))) .pipe(withStatics((s) => ({ zod: zod(s) })))
export type Answer = Schema.Schema.Type<typeof Answer> export type Answer = Schema.Schema.Type<typeof Answer>
export class Reply extends Schema.Class<Reply>("QuestionReply")({ export class Reply extends Schema.Class<Reply>("QuestionReply")({
answers: Schema.Array(Answer).annotate({ answers: Schema.Array(Answer).annotate({
description: "User answers in order of questions (each answer is an array of selected labels)", description: "User answers in order of questions (each answer is an array of selected labels)",
}), }),
}) { }) {
static readonly zod = zod(this) static readonly zod = zod(this)
} }
class Replied extends Schema.Class<Replied>("QuestionReplied")({ class Replied extends Schema.Class<Replied>("QuestionReplied")({
sessionID: SessionID, sessionID: SessionID,
requestID: QuestionID, requestID: QuestionID,
answers: Schema.Array(Answer), answers: Schema.Array(Answer),
}) {} }) {}
class Rejected extends Schema.Class<Rejected>("QuestionRejected")({ class Rejected extends Schema.Class<Rejected>("QuestionRejected")({
sessionID: SessionID, sessionID: SessionID,
requestID: QuestionID, requestID: QuestionID,
}) {} }) {}
export const Event = { export const Event = {
Asked: BusEvent.define("question.asked", Request.zod), Asked: BusEvent.define("question.asked", Request.zod),
Replied: BusEvent.define("question.replied", zod(Replied)), Replied: BusEvent.define("question.replied", zod(Replied)),
Rejected: BusEvent.define("question.rejected", zod(Rejected)), Rejected: BusEvent.define("question.rejected", zod(Rejected)),
} }
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) { export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) {
override get message() { override get message() {
return "The user dismissed this question" return "The user dismissed this question"
} }
} }
interface PendingEntry { interface PendingEntry {
info: Request info: Request
deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError> deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError>
} }
interface State { interface State {
pending: Map<QuestionID, PendingEntry> pending: Map<QuestionID, PendingEntry>
} }
// Service // Service
export interface Interface { export interface Interface {
readonly ask: (input: { readonly ask: (input: {
sessionID: SessionID sessionID: SessionID
questions: ReadonlyArray<Info> questions: ReadonlyArray<Info>
@@ -126,11 +125,11 @@ export namespace Question {
readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void> readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void>
readonly reject: (requestID: QuestionID) => Effect.Effect<void> readonly reject: (requestID: QuestionID) => Effect.Effect<void>
readonly list: () => Effect.Effect<ReadonlyArray<Request>> readonly list: () => Effect.Effect<ReadonlyArray<Request>>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const bus = yield* Bus.Service const bus = yield* Bus.Service
@@ -223,7 +222,8 @@ export namespace Question {
return Service.of({ ask, reply, reject, list }) return Service.of({ ask, reply, reject, list })
}), }),
) )
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
}
export * as Question from "."