@@ -156,13 +156,19 @@ export interface TemplateCatalogDuplicateNameGroup {
156156 labels : string [ ] ;
157157}
158158
159+ export interface TemplateCatalogDuplicateFunctionalityGroup {
160+ key : string ;
161+ labels : string [ ] ;
162+ }
163+
159164export interface TemplateCatalogLowValueCandidate {
160165 label : string ;
161166 reason : string ;
162167}
163168
164169export interface TemplateCatalogQualitySummary {
165170 duplicateNames : TemplateCatalogDuplicateNameGroup [ ] ;
171+ duplicateFunctionalities : TemplateCatalogDuplicateFunctionalityGroup [ ] ;
166172 lowValueCandidates : TemplateCatalogLowValueCandidate [ ] ;
167173}
168174
@@ -642,14 +648,60 @@ function getTemplateCatalogLabel(result: TemplateAuditMarkdownResult): string {
642648 return result . seedFile ?? result . templateName ;
643649}
644650
651+ function getRuntimeInputSignature ( value : unknown ) : string {
652+ if ( ! value || typeof value !== 'object' || Array . isArray ( value ) ) {
653+ return normalizeCatalogName ( String ( value ?? '' ) ) ;
654+ }
655+
656+ const input = value as Record < string , unknown > ;
657+ const required =
658+ input . required === false || input . optional === true || input . isRequired === false
659+ ? 'optional'
660+ : 'required' ;
661+
662+ return [
663+ normalizeCatalogName ( String ( input . id ?? input . name ?? input . key ?? '' ) ) ,
664+ normalizeCatalogName ( String ( input . type ?? input . inputType ?? '' ) ) ,
665+ required ,
666+ ] . join ( ':' ) ;
667+ }
668+
669+ function shouldCheckTemplateFunctionality ( result : TemplateAuditMarkdownResult ) : boolean {
670+ return (
671+ result . classification === 'live-run' &&
672+ result . components . length > 0 &&
673+ ( result . runtimeInputs . length > 0 || result . requiredSecrets . length > 0 )
674+ ) ;
675+ }
676+
677+ function createTemplateFunctionalityKey ( result : TemplateAuditMarkdownResult ) : string {
678+ const category = normalizeCatalogName ( result . category ?? '' ) ;
679+ const classification = normalizeCatalogName ( result . classification ) ;
680+ const components = [ ...result . components ] . sort ( ) . join ( ',' ) ;
681+ const runtimeInputs = result . runtimeInputs . map ( getRuntimeInputSignature ) . sort ( ) . join ( ',' ) ;
682+ const requiredSecrets = result . requiredSecrets . map ( normalizeCatalogName ) . sort ( ) . join ( ',' ) ;
683+
684+ return [ category , classification , components , runtimeInputs , requiredSecrets ] . join ( '|' ) ;
685+ }
686+
645687export function summarizeTemplateCatalogQuality (
646688 results : TemplateAuditMarkdownResult [ ] ,
647689) : TemplateCatalogQualitySummary {
648690 const byName = new Map < string , TemplateAuditMarkdownResult [ ] > ( ) ;
691+ const byFunctionality = new Map < string , TemplateAuditMarkdownResult [ ] > ( ) ;
649692 for ( const result of results ) {
650693 const key = normalizeCatalogName ( result . templateName ) ;
651- if ( ! key ) continue ;
652- byName . set ( key , [ ...( byName . get ( key ) ?? [ ] ) , result ] ) ;
694+ if ( key ) {
695+ byName . set ( key , [ ...( byName . get ( key ) ?? [ ] ) , result ] ) ;
696+ }
697+
698+ if ( shouldCheckTemplateFunctionality ( result ) ) {
699+ const functionalityKey = createTemplateFunctionalityKey ( result ) ;
700+ byFunctionality . set ( functionalityKey , [
701+ ...( byFunctionality . get ( functionalityKey ) ?? [ ] ) ,
702+ result ,
703+ ] ) ;
704+ }
653705 }
654706
655707 const duplicateNames = Array . from ( byName . entries ( ) )
@@ -659,6 +711,13 @@ export function summarizeTemplateCatalogQuality(
659711 labels : group . map ( getTemplateCatalogLabel ) ,
660712 } ) ) ;
661713
714+ const duplicateFunctionalities = Array . from ( byFunctionality . entries ( ) )
715+ . filter ( ( [ , group ] ) => group . length > 1 )
716+ . map ( ( [ key , group ] ) => ( {
717+ key,
718+ labels : group . map ( getTemplateCatalogLabel ) ,
719+ } ) ) ;
720+
662721 const lowValueCandidates = results
663722 . filter ( ( result ) => result . runtimeInputs . length === 0 && result . requiredSecrets . length === 0 )
664723 . map ( ( result ) => ( {
@@ -668,6 +727,7 @@ export function summarizeTemplateCatalogQuality(
668727
669728 return {
670729 duplicateNames,
730+ duplicateFunctionalities,
671731 lowValueCandidates,
672732 } ;
673733}
@@ -682,6 +742,10 @@ export function getTemplateCatalogQualityFailures(
682742 failures . push ( `Duplicate template name: ${ group . labels . join ( ', ' ) } ` ) ;
683743 }
684744
745+ for ( const group of summary . duplicateFunctionalities ) {
746+ failures . push ( `Duplicate template functionality: ${ group . labels . join ( ', ' ) } ` ) ;
747+ }
748+
685749 for ( const candidate of summary . lowValueCandidates ) {
686750 failures . push ( `Low-value/static template: ${ candidate . label } ${ candidate . reason } ` ) ;
687751 }
@@ -696,6 +760,7 @@ export function renderTemplateCatalogQualityCheck(results: TemplateAuditMarkdown
696760 '# Template Catalog Quality Check' ,
697761 '' ,
698762 `Duplicate names: ${ summary . duplicateNames . length } ` ,
763+ `Duplicate functionality: ${ summary . duplicateFunctionalities . length } ` ,
699764 `Low-value/static candidates: ${ summary . lowValueCandidates . length } ` ,
700765 ] ;
701766
@@ -767,6 +832,14 @@ export function renderTemplateAuditMarkdown({
767832 }
768833 }
769834
835+ if ( catalogQuality . duplicateFunctionalities . length === 0 ) {
836+ lines . push ( '- Duplicate functionality: none' ) ;
837+ } else {
838+ for ( const group of catalogQuality . duplicateFunctionalities ) {
839+ lines . push ( `- Duplicate functionality: ${ group . labels . join ( ', ' ) } ` ) ;
840+ }
841+ }
842+
770843 if ( catalogQuality . lowValueCandidates . length === 0 ) {
771844 lines . push ( '- Low-value/static candidates: none' ) ;
772845 } else {
0 commit comments