@@ -4,6 +4,8 @@ import type {
44 DocumentNode ,
55 ObjectTypeDefinitionNode ,
66 InputObjectTypeDefinitionNode ,
7+ EnumTypeDefinitionNode ,
8+ UnionTypeDefinitionNode ,
79 FieldDefinitionNode ,
810 InputValueDefinitionNode ,
911 ConstDirectiveNode ,
@@ -394,6 +396,124 @@ export default async function processSchemas(
394396 if ( ! changed ) break
395397 }
396398
399+ // (c) General reference-based inheritance. An un-annotated enum, union, or
400+ // input object inherits the category of the type(s) that reference it, but
401+ // only when every referrer resolves to a single category; ambiguous types
402+ // (referrers disagree, or a referrer is itself ambiguous) stay in `other`.
403+ // This is the derived successor to a static exception list: it catches
404+ // generated/indirect types that github/github never annotates directly while
405+ // still letting the upstream team own the outcome via the parent type's
406+ // `docs_category`.
407+ //
408+ // Examples this resolves today:
409+ // - `IssueTimelineItemsItemType` / `PullRequestTimelineItemsItemType`:
410+ // runtime-generated enums used only as the `itemTypes` argument on
411+ // `Issue.timelineItems` (issues) / `PullRequest.timelineItems` (pulls).
412+ // - `RepositoryRuleType` (enum) and `RuleParameters` (union): referenced
413+ // from the annotated `RepositoryRule` object (repos).
414+ // - `RuleParametersInput` (input): referenced from the annotated
415+ // `RepositoryRuleInput` input object (repos).
416+ //
417+ // A "referrer category" is the category of:
418+ // - the owning object type, for a field's return type or a field argument's
419+ // type (interfaces are intentionally excluded: they are cross-cutting and
420+ // make coincidental single-category matches likely);
421+ // - the Mutation root field, for that field's arguments;
422+ // - the owning input object, for an input field's type. Input objects can
423+ // themselves be uncategorized-but-derivable, so this rule propagates
424+ // transitively through nested inputs.
425+ //
426+ // Implemented as a monotone fixpoint over candidate category *sets* rather
427+ // than committing categories as we go: a type is only assigned once its
428+ // candidate set has stopped growing, so the result is independent of
429+ // definition/derivation order and a later-discovered conflicting referrer
430+ // can never be missed. Explicit annotations and derivations (a)/(b) always
431+ // win: we only compute candidates for ids `lookupCat` still can't resolve.
432+ const derivableTargets = schemaAST . definitions . filter (
433+ (
434+ def ,
435+ ) : def is EnumTypeDefinitionNode | UnionTypeDefinitionNode | InputObjectTypeDefinitionNode =>
436+ def . kind === 'EnumTypeDefinition' ||
437+ def . kind === 'UnionTypeDefinition' ||
438+ def . kind === 'InputObjectTypeDefinition' ,
439+ )
440+ const inputDefs = schemaAST . definitions . filter (
441+ ( def ) : def is InputObjectTypeDefinitionNode => def . kind === 'InputObjectTypeDefinition' ,
442+ )
443+
444+ const targetIds = new Set < string > ( )
445+ for ( const def of derivableTargets ) {
446+ const id = helpers . getId ( def . name . value )
447+ if ( ! lookupCat ( id ) ) targetIds . add ( id )
448+ }
449+
450+ if ( targetIds . size > 0 ) {
451+ const candidates = new Map < string , Set < string > > ( )
452+ for ( const id of targetIds ) candidates . set ( id , new Set ( ) )
453+
454+ // Categories a type contributes when it appears as a referrer. Annotated /
455+ // fallback types contribute their single category; an uncommitted derivable
456+ // referrer (only ever an input object here) contributes its current
457+ // candidate set so ambiguity propagates downstream.
458+ const contribution = ( referrerId : string ) : Iterable < string > => {
459+ const explicit = lookupCat ( referrerId )
460+ if ( explicit ) return [ explicit ]
461+ return candidates . get ( referrerId ) ?? [ ]
462+ }
463+ const addRef = ( targetName : string | undefined , cats : Iterable < string > ) : boolean => {
464+ if ( ! targetName ) return false
465+ const id = helpers . getId ( targetName )
466+ const set = candidates . get ( id )
467+ if ( ! set ) return false
468+ let grew = false
469+ for ( const c of cats ) {
470+ if ( ! set . has ( c ) ) {
471+ set . add ( c )
472+ grew = true
473+ }
474+ }
475+ return grew
476+ }
477+
478+ // Bounded by the worst-case propagation depth; each pass only adds to sets,
479+ // so this terminates well before the cap.
480+ const maxPasses = targetIds . size + 2
481+ for ( let pass = 0 ; pass < maxPasses ; pass ++ ) {
482+ let changed = false
483+
484+ for ( const def of objectDefs ) {
485+ const name = def . name . value
486+ if ( name === 'Query' ) continue
487+ const isMutation = name === 'Mutation'
488+ for ( const field of def . fields || [ ] ) {
489+ // Mutation fields carry their own category and their payload return
490+ // type is already annotated, so (like rule (a)) we only walk args.
491+ const fieldCats : Iterable < string > = isMutation
492+ ? ( ( c ) => ( c ? [ c ] : [ ] ) ) ( getMutationCat ( field . name . value ) )
493+ : contribution ( helpers . getId ( name ) )
494+ if ( ! isMutation && addRef ( namedTypeName ( field . type ) , fieldCats ) ) changed = true
495+ for ( const arg of field . arguments || [ ] ) {
496+ if ( addRef ( namedTypeName ( arg . type ) , fieldCats ) ) changed = true
497+ }
498+ }
499+ }
500+
501+ for ( const def of inputDefs ) {
502+ const ownerCats = contribution ( helpers . getId ( def . name . value ) )
503+ for ( const field of def . fields || [ ] ) {
504+ if ( addRef ( namedTypeName ( field . type ) , ownerCats ) ) changed = true
505+ }
506+ }
507+
508+ if ( ! changed ) break
509+ }
510+
511+ // Assign only the targets whose final candidate set is unambiguous.
512+ for ( const [ id , cats ] of candidates ) {
513+ if ( cats . size === 1 ) typeCategoryMap . set ( id , [ ...cats ] [ 0 ] )
514+ }
515+ }
516+
397517 // Normalize unknown categories (e.g. `:checks`, `:search`, `:packages`,
398518 // `:security_advisories`) to `other`. The upstream gh/gh allowlist permits
399519 // many categories that docs-internal hasn't yet built per-category landing
0 commit comments