@@ -47,6 +47,7 @@ import {
4747 resolveObjectSchema ,
4848 resolveRef ,
4949 resolveSchema ,
50+ REPO_ROOT ,
5051 rewriteSharedDefinitionReferences ,
5152 writeGeneratedFile ,
5253 type ApiSchema ,
@@ -3437,6 +3438,20 @@ function collectGoSharedSessionEventAliasNames(
34373438 for ( const value of values ?? [ ] ) {
34383439 constNames . add ( `${ typeName } ${ goEnumConstSuffix ( value ) } ` ) ;
34393440 }
3441+
3442+ // Detect anyOf unions with a string-const discriminator property. The
3443+ // api/rpc generator synthesizes an enum (named `<TypeName><DiscProp>`)
3444+ // and per-variant consts for these (e.g. `Attachment` → `AttachmentType`
3445+ // + `AttachmentTypeFile`, ...). They aren't top-level $defs, so we have
3446+ // to surface them explicitly here so the public `copilot` alias file
3447+ // re-exports them alongside the union and its variant structs.
3448+ const synthesized = collectGoSharedAnyOfDiscriminatorAliasNames ( typeName , schema , definitions ) ;
3449+ if ( synthesized ) {
3450+ typeNames . add ( synthesized . enumName ) ;
3451+ for ( const constName of synthesized . constNames ) {
3452+ constNames . add ( constName ) ;
3453+ }
3454+ }
34403455 }
34413456
34423457 return {
@@ -3445,6 +3460,107 @@ function collectGoSharedSessionEventAliasNames(
34453460 } ;
34463461}
34473462
3463+ /**
3464+ * For a shared definition that is an `anyOf` discriminated union with a
3465+ * string-const discriminator property (e.g. `Attachment` with `type: "file" |
3466+ * "directory" | ...`), return the synthesized Go discriminator enum name and
3467+ * per-variant const names that the api/rpc generator emits via
3468+ * `emitGoFlatDiscriminatedUnion`. Returns `undefined` when the definition does
3469+ * not match the const-discriminator pattern.
3470+ */
3471+ function collectGoSharedAnyOfDiscriminatorAliasNames (
3472+ unionTypeName : string ,
3473+ schema : JSONSchema7 ,
3474+ definitions : Record < string , unknown >
3475+ ) : { enumName : string ; constNames : string [ ] } | undefined {
3476+ const variants = Array . isArray ( schema . anyOf ) ? schema . anyOf : undefined ;
3477+ if ( ! variants || variants . length === 0 ) return undefined ;
3478+
3479+ const resolvedVariants : JSONSchema7 [ ] = [ ] ;
3480+ for ( const variant of variants ) {
3481+ const resolved = resolveSharedAnyOfVariant ( variant , definitions ) ;
3482+ if ( ! resolved || ! resolved . properties ) return undefined ;
3483+ resolvedVariants . push ( resolved ) ;
3484+ }
3485+
3486+ const firstVariant = resolvedVariants [ 0 ] ;
3487+ for ( const [ propName , propSchemaRaw ] of Object . entries ( firstVariant . properties ! ) ) {
3488+ if ( typeof propSchemaRaw !== "object" || propSchemaRaw === null ) continue ;
3489+ const firstPropSchema = propSchemaRaw as JSONSchema7 ;
3490+ if ( typeof firstPropSchema . const !== "string" ) continue ;
3491+
3492+ const collectedValues : string [ ] = [ ] ;
3493+ let valid = true ;
3494+ for ( const variant of resolvedVariants ) {
3495+ if ( ! ( variant . required || [ ] ) . includes ( propName ) ) { valid = false ; break ; }
3496+ const variantProp = variant . properties ?. [ propName ] ;
3497+ if ( typeof variantProp !== "object" || variantProp === null ) { valid = false ; break ; }
3498+ const variantConst = ( variantProp as JSONSchema7 ) . const ;
3499+ if ( typeof variantConst !== "string" ) { valid = false ; break ; }
3500+ collectedValues . push ( variantConst ) ;
3501+ }
3502+ if ( ! valid || collectedValues . length === 0 ) continue ;
3503+
3504+ const enumName = `${ unionTypeName } ${ toGoFieldName ( propName ) } ` ;
3505+ const constNames = [ ...new Set ( collectedValues ) ] . map (
3506+ ( value ) => `${ enumName } ${ goEnumConstSuffix ( value ) } `
3507+ ) ;
3508+ return { enumName, constNames } ;
3509+ }
3510+ return undefined ;
3511+ }
3512+
3513+ function resolveSharedAnyOfVariant (
3514+ variant : JSONSchema7 | boolean ,
3515+ definitions : Record < string , unknown >
3516+ ) : JSONSchema7 | undefined {
3517+ if ( typeof variant !== "object" || variant === null ) return undefined ;
3518+ if ( typeof variant . $ref === "string" ) {
3519+ // Local $ref like "#/$defs/AttachmentFile" or "#/definitions/AttachmentFile".
3520+ const localMatch = / ^ # \/ (?: \$ d e f s | d e f i n i t i o n s ) \/ ( .+ ) $ / . exec ( variant . $ref ) ;
3521+ if ( ! localMatch ) return undefined ;
3522+ const target = definitions [ decodeURIComponent ( localMatch [ 1 ] ) ] ;
3523+ if ( ! target || typeof target !== "object" || Array . isArray ( target ) ) return undefined ;
3524+ return target as JSONSchema7 ;
3525+ }
3526+ return variant ;
3527+ }
3528+
3529+ /**
3530+ * Scan hand-written `.go` files under `go/` and return every top-level exported
3531+ * type or const name they declare. We use this to exclude those names from the
3532+ * session-events alias file: when a schema-shared definition (e.g. `ContextTier`)
3533+ * collides with a hand-written declaration of the same name in the public
3534+ * `copilot` package, the hand-written declaration must win — emitting an alias
3535+ * would produce a duplicate package-scope identifier and fail `go build`.
3536+ *
3537+ * Generated files use the `z*.go` naming convention; we skip them so that this
3538+ * scanner never reads (or depends on the output of) its own emit. Test files
3539+ * are scanned too because they share the package namespace, so a hand-written
3540+ * test-only declaration would also collide with an alias of the same name.
3541+ */
3542+ async function collectHandWrittenGoPublicNames ( ) : Promise < Set < string > > {
3543+ const goDir = path . join ( REPO_ROOT , "go" ) ;
3544+ const names = new Set < string > ( ) ;
3545+ let entries : string [ ] ;
3546+ try {
3547+ entries = await fs . readdir ( goDir ) ;
3548+ } catch {
3549+ return names ;
3550+ }
3551+ for ( const entry of entries ) {
3552+ if ( ! entry . endsWith ( ".go" ) ) continue ;
3553+ if ( entry . startsWith ( "z" ) ) continue ;
3554+ const filePath = path . join ( goDir , entry ) ;
3555+ const stat = await fs . stat ( filePath ) ;
3556+ if ( ! stat . isFile ( ) ) continue ;
3557+ const content = await fs . readFile ( filePath , "utf-8" ) ;
3558+ for ( const name of collectGoTopLevelNames ( content , "type" ) ) names . add ( name ) ;
3559+ for ( const name of collectGoTopLevelNames ( content , "const" ) ) names . add ( name ) ;
3560+ }
3561+ return names ;
3562+ }
3563+
34483564function assertNoGoRpcSessionEventConflicts ( rpcGeneratedTypeCode : string ) : void {
34493565 const duplicateTypes = collectGoTopLevelNames ( rpcGeneratedTypeCode , "type" )
34503566 . filter ( ( name ) => rpcSessionEventTopLevelNames . types . has ( name ) ) ;
@@ -3531,9 +3647,19 @@ async function generateSessionEvents(schemaPath?: string, apiSchema?: ApiSchema)
35313647 }
35323648 }
35333649 }
3650+ // Names of public type/const declarations that already exist in hand-written
3651+ // Go files under `go/`. We must not re-export schema-generated names that
3652+ // collide with these, because Go disallows two top-level identifiers with
3653+ // the same name in a single package (`copilot`). The hand-written
3654+ // declaration always wins. Without this filter, schema-shared definitions
3655+ // like `ContextTier` (defined as a shared schema definition and emitted in
3656+ // the rpc package) would generate `copilot.ContextTier = rpc.ContextTier`
3657+ // aliases that clash with the existing hand-written `copilot.ContextTier`.
3658+ const handWrittenPublicNames = await collectHandWrittenGoPublicNames ( ) ;
3659+ const aliasExcludes = new Set < string > ( [ ...internalTypesInSession , ...handWrittenPublicNames ] ) ;
35343660 const aliasOutPath = await writeGeneratedFile (
35353661 "go/zsession_events.go" ,
3536- generateGoSessionEventAliasFile ( generatedTypeCode , sharedAliasNames . typeNames , sharedAliasNames . constNames , internalTypesInSession )
3662+ generateGoSessionEventAliasFile ( generatedTypeCode , sharedAliasNames . typeNames , sharedAliasNames . constNames , aliasExcludes )
35373663 ) ;
35383664 console . log ( ` ✓ ${ aliasOutPath } ` ) ;
35393665
0 commit comments