Skip to content

Commit 4e08e45

Browse files
committed
fix: detect duplicate template functionality
Signed-off-by: zebbern <185730623+zebbern@users.noreply.github.com>
1 parent cedc501 commit 4e08e45

2 files changed

Lines changed: 131 additions & 2 deletions

File tree

scripts/__tests__/template-library-live-audit-utils.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,22 @@ describe('template library live audit helpers', () => {
626626
recommendation: 'keep',
627627
rationale: 'Live execution completed and produced at least one artifact.',
628628
},
629+
{
630+
templateId: 'tpl-functional-copy',
631+
templateName: 'Package Advisory Scanner',
632+
seedFile: 'package-advisory-scanner.json',
633+
category: 'cve-research',
634+
components: ['sentris.osv.query'],
635+
requiredSecrets: [],
636+
runtimeInputs: [{ id: 'packageName' }],
637+
classification: 'live-run',
638+
createOk: true,
639+
runAttempted: true,
640+
terminalStatus: 'COMPLETED',
641+
artifactsCount: 1,
642+
recommendation: 'keep',
643+
rationale: 'Live execution completed and produced at least one artifact.',
644+
},
629645
{
630646
templateId: 'tpl-static',
631647
templateName: 'Static Demo Template',
@@ -648,6 +664,9 @@ describe('template library live audit helpers', () => {
648664
expect(markdown).toContain(
649665
'- Duplicate name: npm-dependency-cve-hunt.json, npm-dependency-cve-hunt-copy.json',
650666
);
667+
expect(markdown).toContain(
668+
'- Duplicate functionality: npm-dependency-cve-hunt.json, npm-dependency-cve-hunt-copy.json, package-advisory-scanner.json',
669+
);
651670
expect(markdown).toContain(
652671
'- Low-value/static candidate: static-demo-template.json has no runtime inputs or required secrets.',
653672
);
@@ -702,10 +721,27 @@ describe('template library live audit helpers', () => {
702721
recommendation: 'review',
703722
rationale: 'Static demonstration workflow.',
704723
},
724+
{
725+
templateId: 'tpl-functional-copy',
726+
templateName: 'Package Advisory Scanner',
727+
seedFile: 'package-advisory-scanner.json',
728+
category: 'cve-research',
729+
components: ['sentris.osv.query'],
730+
requiredSecrets: [],
731+
runtimeInputs: [{ id: 'packageName' }],
732+
classification: 'live-run',
733+
createOk: true,
734+
runAttempted: true,
735+
terminalStatus: 'COMPLETED',
736+
artifactsCount: 1,
737+
recommendation: 'keep',
738+
rationale: 'Live execution completed and produced at least one artifact.',
739+
},
705740
]);
706741

707742
expect(failures).toEqual([
708743
'Duplicate template name: npm-dependency-cve-hunt.json, npm-dependency-cve-hunt-copy.json',
744+
'Duplicate template functionality: npm-dependency-cve-hunt.json, npm-dependency-cve-hunt-copy.json, package-advisory-scanner.json',
709745
'Low-value/static template: static-demo-template.json has no runtime inputs or required secrets.',
710746
]);
711747
});
@@ -759,15 +795,35 @@ describe('template library live audit helpers', () => {
759795
recommendation: 'review',
760796
rationale: 'Static demonstration workflow.',
761797
},
798+
{
799+
templateId: 'tpl-functional-copy',
800+
templateName: 'Package Advisory Scanner',
801+
seedFile: 'package-advisory-scanner.json',
802+
category: 'cve-research',
803+
components: ['sentris.osv.query'],
804+
requiredSecrets: [],
805+
runtimeInputs: [{ id: 'packageName' }],
806+
classification: 'live-run',
807+
createOk: true,
808+
runAttempted: true,
809+
terminalStatus: 'COMPLETED',
810+
artifactsCount: 1,
811+
recommendation: 'keep',
812+
rationale: 'Live execution completed and produced at least one artifact.',
813+
},
762814
]);
763815

764816
expect(report).toContain('# Template Catalog Quality Check');
765817
expect(report).toContain('Duplicate names: 1');
818+
expect(report).toContain('Duplicate functionality: 1');
766819
expect(report).toContain('Low-value/static candidates: 1');
767820
expect(report).toContain('## Catalog Quality Failures');
768821
expect(report).toContain(
769822
'- Duplicate template name: npm-dependency-cve-hunt.json, npm-dependency-cve-hunt-copy.json',
770823
);
824+
expect(report).toContain(
825+
'- Duplicate template functionality: npm-dependency-cve-hunt.json, npm-dependency-cve-hunt-copy.json, package-advisory-scanner.json',
826+
);
771827
expect(report).toContain(
772828
'- Low-value/static template: static-demo-template.json has no runtime inputs or required secrets.',
773829
);

scripts/template-library-live-audit-utils.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,19 @@ export interface TemplateCatalogDuplicateNameGroup {
156156
labels: string[];
157157
}
158158

159+
export interface TemplateCatalogDuplicateFunctionalityGroup {
160+
key: string;
161+
labels: string[];
162+
}
163+
159164
export interface TemplateCatalogLowValueCandidate {
160165
label: string;
161166
reason: string;
162167
}
163168

164169
export 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+
645687
export 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

Comments
 (0)