@@ -1092,15 +1092,16 @@ async function publishResultIfConfigured(task: JsonObject, resultPath: string, t
10921092 }
10931093}
10941094
1095- function publicationCommentBody ( task : JsonObject , result : JsonObject ) : string {
1095+ export function publicationCommentBody ( task : JsonObject , result : JsonObject ) : string {
10961096 const status = result . status || "unknown" ;
1097- const summary = String ( result . summary || "No summary returned." ) ;
1098- const prBody = String ( result . pr_body || "" ) ;
10991097 const filesChanged = Array . isArray ( result . files_changed ) ? result . files_changed : [ ] ;
11001098 const commits = Array . isArray ( result . commits ) ? result . commits : [ ] ;
11011099 const taskId = String ( task . task_id || "" ) ;
11021100 const evidence = ( task . review_evidence as JsonObject | undefined ) || { } ;
11031101 const review = ( result . review as JsonObject | undefined ) || { } ;
1102+ const knownFiles = githubKnownFileSet ( task , review ) ;
1103+ const summary = linkGithubFileMentions ( task , String ( result . summary || "No summary returned." ) , knownFiles ) ;
1104+ const prBody = linkGithubFileMentions ( task , String ( result . pr_body || "" ) , knownFiles ) ;
11041105 const parts = [ "## Cody dogfood result" , "" , `**Status:** ${ status } ` , "" , summary . trim ( ) ] ;
11051106 if ( prBody . trim ( ) && prBody . trim ( ) !== summary . trim ( ) ) {
11061107 parts . push ( "" , prBody . trim ( ) ) ;
@@ -1117,12 +1118,12 @@ function publicationCommentBody(task: JsonObject, result: JsonObject): string {
11171118 `- Review context SHA-256: \`${ evidence . review_context_sha256 } \`` ,
11181119 ) ;
11191120 if ( changedFiles . length ) {
1120- parts . push ( `- Files: ${ changedFiles . slice ( 0 , 20 ) . map ( ( file ) => `\` ${ file } \`` ) . join ( ", " ) } ` ) ;
1121+ parts . push ( `- Files: ${ changedFiles . slice ( 0 , 20 ) . map ( ( file ) => githubFileMarkdown ( task , String ( file ) ) ) . join ( ", " ) } ` ) ;
11211122 }
11221123 } else {
11231124 parts . push ( "- No PR review evidence was captured for this run." ) ;
11241125 }
1125- parts . push ( ...structuredReviewLines ( review ) ) ;
1126+ parts . push ( ...structuredReviewLines ( review , task , knownFiles ) ) ;
11261127 parts . push (
11271128 "" ,
11281129 `**Files changed:** ${ filesChanged . length } ` ,
@@ -1133,6 +1134,171 @@ function publicationCommentBody(task: JsonObject, result: JsonObject): string {
11331134 return parts . join ( "\n" ) ;
11341135}
11351136
1137+ function githubBlobBase ( task : JsonObject ) : string | null {
1138+ const repository = String ( task . repository || "" ) . trim ( ) ;
1139+ if ( ! / ^ [ A - Z a - z 0 - 9 _ . - ] + \/ [ A - Z a - z 0 - 9 _ . - ] + $ / . test ( repository ) ) {
1140+ return null ;
1141+ }
1142+ const evidence = ( task . review_evidence as JsonObject | undefined ) || { } ;
1143+ const ref = String ( evidence . head_sha || evidence . workspace_head_sha || task . default_branch || "" ) . trim ( ) ;
1144+ if ( ! ref ) {
1145+ return null ;
1146+ }
1147+ const encodedRef = encodeURIComponent ( ref ) . replaceAll ( "%2F" , "/" ) ;
1148+ return `https://github.com/${ repository } /blob/${ encodedRef } ` ;
1149+ }
1150+
1151+ function parseRepoRelativePath ( rawPath : string ) : { display : string ; path : string ; line : number | null ; endLine : number | null } | null {
1152+ const display = rawPath . trim ( ) ;
1153+ if ( ! display || display !== rawPath || / [ \s < > \[ \] ] / . test ( display ) ) {
1154+ return null ;
1155+ }
1156+ if ( / ^ [ \\ / ] / . test ( display ) || / ^ [ A - Z a - z ] : [ \\ / ] / . test ( display ) ) {
1157+ return null ;
1158+ }
1159+
1160+ let path = display . replaceAll ( "\\" , "/" ) ;
1161+ let line : number | null = null ;
1162+ let endLine : number | null = null ;
1163+ const lineMatch = / ^ ( .* ) : ( \d + ) (?: - ( \d + ) ) ? $ / . exec ( path ) ;
1164+ if ( lineMatch ) {
1165+ path = lineMatch [ 1 ] ;
1166+ line = Number ( lineMatch [ 2 ] ) ;
1167+ endLine = lineMatch [ 3 ] ? Number ( lineMatch [ 3 ] ) : null ;
1168+ }
1169+ if ( / ^ [ A - Z a - z ] [ A - Z a - z 0 - 9 + . - ] * : / . test ( path ) ) {
1170+ return null ;
1171+ }
1172+ while ( path . startsWith ( "./" ) ) {
1173+ path = path . slice ( 2 ) ;
1174+ }
1175+ const segments = path . split ( "/" ) ;
1176+ const filename = segments . at ( - 1 ) || "" ;
1177+ if (
1178+ ! path ||
1179+ path . includes ( "//" ) ||
1180+ segments . some ( ( segment ) => ! segment || segment === "." || segment === ".." ) ||
1181+ ! / \. [ A - Z a - z ] [ A - Z a - z 0 - 9 . _ - ] { 0 , 15 } $ / . test ( filename )
1182+ ) {
1183+ return null ;
1184+ }
1185+ return { display, path, line, endLine} ;
1186+ }
1187+
1188+ function githubFileMarkdown ( task : JsonObject , rawPath : string , lineOverride : number | null = null ) : string {
1189+ const target = parseRepoRelativePath ( rawPath ) ;
1190+ const base = githubBlobBase ( task ) ;
1191+ if ( ! target || ! base ) {
1192+ return `\`${ rawPath } \`` ;
1193+ }
1194+ const encodedPath = target . path . split ( "/" ) . map ( ( segment ) => encodeURIComponent ( segment ) ) . join ( "/" ) ;
1195+ const line = lineOverride ?? target . line ;
1196+ const endLine = lineOverride === null ? target . endLine : null ;
1197+ const lineAnchor = line !== null && Number . isFinite ( line ) && line > 0
1198+ ? `#L${ line } ${ endLine !== null && Number . isFinite ( endLine ) && endLine > line ? `-L${ endLine } ` : "" } `
1199+ : "" ;
1200+ return `[\`${ target . display } \`](${ base } /${ encodedPath } ${ lineAnchor } )` ;
1201+ }
1202+
1203+ function githubKnownFileSet ( task : JsonObject , review : JsonObject = { } ) : Set < string > {
1204+ const knownFiles = new Set < string > ( ) ;
1205+ const evidence = ( task . review_evidence as JsonObject | undefined ) || { } ;
1206+ const addFiles = ( value : JsonValue | undefined ) => {
1207+ if ( ! Array . isArray ( value ) ) {
1208+ return ;
1209+ }
1210+ for ( const item of value ) {
1211+ const target = parseRepoRelativePath ( String ( item ) ) ;
1212+ if ( target ) {
1213+ knownFiles . add ( target . path ) ;
1214+ }
1215+ }
1216+ } ;
1217+ addFiles ( evidence . changed_files ) ;
1218+ addFiles ( review . reviewed_files ) ;
1219+ addFiles ( review . supporting_files ) ;
1220+ return knownFiles ;
1221+ }
1222+
1223+ function inlineCodeWithGithubFileLinks ( task : JsonObject , rawText : string ) : string {
1224+ const direct = githubFileMarkdown ( task , rawText ) ;
1225+ if ( direct !== `\`${ rawText } \`` ) {
1226+ return direct ;
1227+ }
1228+
1229+ let linkedAny = false ;
1230+ const parts = rawText . split ( / ( \s + ) / ) . map ( ( part ) => {
1231+ if ( ! part || / ^ \s + $ / . test ( part ) ) {
1232+ return part ;
1233+ }
1234+ const linked = githubFileMarkdown ( task , part ) ;
1235+ if ( linked !== `\`${ part } \`` ) {
1236+ linkedAny = true ;
1237+ return linked ;
1238+ }
1239+ return `\`${ part } \`` ;
1240+ } ) ;
1241+
1242+ return linkedAny ? parts . join ( "" ) : `\`${ rawText } \`` ;
1243+ }
1244+
1245+ function bareFileMentionAllowed ( task : JsonObject , rawPath : string , knownFiles : Set < string > ) : boolean {
1246+ const target = parseRepoRelativePath ( rawPath ) ;
1247+ if ( ! target ) {
1248+ return false ;
1249+ }
1250+ return target . path . includes ( "/" ) || knownFiles . has ( target . path ) ;
1251+ }
1252+
1253+ function linkBareGithubFileMentions ( task : JsonObject , text : string , knownFiles : Set < string > ) : string {
1254+ const markdownLinkPattern = / ( \[ [ ^ \] ] + \] \( [ ^ ) ] + \) ) / g;
1255+ const barePathPattern = / ( ^ | [ ^ \w . / : [ \] ( ) ` - ] ) ( [ A - Z a - z 0 - 9 _ . - ] + (?: \/ [ A - Z a - z 0 - 9 _ . - ] + ) * \. [ A - Z a - z ] [ A - Z a - z 0 - 9 _ - ] { 0 , 15 } (?: : \d + (?: - \d + ) ? ) ? ) (? = $ | [ ^ \w / - ] ) / g;
1256+ return text
1257+ . split ( markdownLinkPattern )
1258+ . map ( ( segment , index ) => {
1259+ if ( index % 2 === 1 ) {
1260+ return segment ;
1261+ }
1262+ return segment . replace ( barePathPattern , ( match , prefix : string , rawPath : string ) => {
1263+ if ( ! bareFileMentionAllowed ( task , rawPath , knownFiles ) ) {
1264+ return match ;
1265+ }
1266+ const linked = githubFileMarkdown ( task , rawPath ) ;
1267+ return linked === `\`${ rawPath } \`` ? match : `${ prefix } ${ linked } ` ;
1268+ } ) ;
1269+ } )
1270+ . join ( "" ) ;
1271+ }
1272+
1273+ function linkGithubFileMentions ( task : JsonObject , text : string , knownFiles : Set < string > = githubKnownFileSet ( task ) ) : string {
1274+ const fencePattern = / ( ` ` ` [ \s \S ] * ?` ` ` ) / g;
1275+ return text
1276+ . split ( fencePattern )
1277+ . map ( ( segment , index ) => {
1278+ if ( index % 2 === 1 ) {
1279+ return segment ;
1280+ }
1281+ const linkedInlineCode = segment . replace ( / ` ( [ ^ ` \n ] + ) ` / g, ( match , rawPath : string , offset : number ) => {
1282+ const alreadyLinkText = segment [ offset - 1 ] === "[" && segment . slice ( offset + match . length , offset + match . length + 2 ) === "](" ;
1283+ if ( alreadyLinkText ) {
1284+ return match ;
1285+ }
1286+ const linked = inlineCodeWithGithubFileLinks ( task , rawPath ) ;
1287+ return linked === `\`${ rawPath } \`` ? match : linked ;
1288+ } ) ;
1289+ return linkBareGithubFileMentions ( task , linkedInlineCode , knownFiles ) ;
1290+ } )
1291+ . join ( "" ) ;
1292+ }
1293+
1294+ function reviewTestCommandMarkdown ( task : JsonObject , rawCommand : string ) : string {
1295+ const command = rawCommand . trim ( ) ;
1296+ if ( ! command ) {
1297+ return "`unknown command`" ;
1298+ }
1299+ return inlineCodeWithGithubFileLinks ( task , command ) ;
1300+ }
1301+
11361302function reviewFixLoopLines ( task : JsonObject ) : string [ ] {
11371303 const loops = Array . isArray ( task . review_fix_loops ) ? ( task . review_fix_loops as JsonObject [ ] ) : [ ] ;
11381304 if ( ! loops . length ) {
@@ -1148,7 +1314,7 @@ function reviewFixLoopLines(task: JsonObject): string[] {
11481314 return lines ;
11491315}
11501316
1151- function structuredReviewLines ( review : JsonObject ) : string [ ] {
1317+ function structuredReviewLines ( review : JsonObject , task : JsonObject , knownFiles : Set < string > = githubKnownFileSet ( task , review ) ) : string [ ] {
11521318 if ( ! Object . keys ( review ) . length ) {
11531319 return [ "" , "### Structured review" , "- No structured review result was emitted." ] ;
11541320 }
@@ -1162,15 +1328,15 @@ function structuredReviewLines(review: JsonObject): string[] {
11621328 const reviewedFiles = Array . isArray ( review . reviewed_files ) ? review . reviewed_files : [ ] ;
11631329 lines . push ( `- Reviewed files: ${ reviewedFiles . length } ` ) ;
11641330 if ( reviewedFiles . length ) {
1165- lines . push ( `- Reviewed file list: ${ reviewedFiles . slice ( 0 , 20 ) . map ( ( path ) => `\` ${ path } \`` ) . join ( ", " ) } ` ) ;
1331+ lines . push ( `- Reviewed file list: ${ reviewedFiles . slice ( 0 , 20 ) . map ( ( path ) => githubFileMarkdown ( task , String ( path ) ) ) . join ( ", " ) } ` ) ;
11661332 if ( reviewedFiles . length > 20 ) {
11671333 lines . push ( "- Reviewed file list truncated after 20 entries." ) ;
11681334 }
11691335 }
11701336 const supportingFiles = Array . isArray ( review . supporting_files ) ? review . supporting_files : [ ] ;
11711337 lines . push ( `- Supporting files inspected: ${ supportingFiles . length } ` ) ;
11721338 if ( supportingFiles . length ) {
1173- lines . push ( `- Supporting file list: ${ supportingFiles . slice ( 0 , 20 ) . map ( ( path ) => `\` ${ path } \`` ) . join ( ", " ) } ` ) ;
1339+ lines . push ( `- Supporting file list: ${ supportingFiles . slice ( 0 , 20 ) . map ( ( path ) => githubFileMarkdown ( task , String ( path ) ) ) . join ( ", " ) } ` ) ;
11741340 if ( supportingFiles . length > 20 ) {
11751341 lines . push ( "- Supporting file list truncated after 20 entries." ) ;
11761342 }
@@ -1182,19 +1348,21 @@ function structuredReviewLines(review: JsonObject): string[] {
11821348 if ( finding . line !== undefined && finding . line !== null ) {
11831349 location = `${ location } :${ finding . line } ` ;
11841350 }
1185- lines . push ( ` ${ index + 1 } . \`${ finding . severity || "unknown" } \` ${ location } - ${ finding . title || "Untitled finding" } ` ) ;
1351+ const linkedLocation = githubFileMarkdown ( task , location , Number ( finding . line || 0 ) || null ) ;
1352+ lines . push ( ` ${ index + 1 } . \`${ finding . severity || "unknown" } \` ${ linkedLocation } - ${ finding . title || "Untitled finding" } ` ) ;
11861353 } ) ;
11871354 if ( findings . length > 10 ) {
11881355 lines . push ( "- Findings truncated after 10 entries." ) ;
11891356 }
11901357 if ( review . no_findings_reason ) {
1191- lines . push ( `- No-findings reason: ${ review . no_findings_reason } ` ) ;
1358+ lines . push ( `- No-findings reason: ${ linkGithubFileMentions ( task , String ( review . no_findings_reason ) , knownFiles ) } ` ) ;
11921359 }
11931360 const testsRun = Array . isArray ( review . tests_run ) ? ( review . tests_run as JsonObject [ ] ) : [ ] ;
11941361 lines . push ( `- Tests reported by runtime: ${ testsRun . length } ` ) ;
11951362 testsRun . slice ( 0 , 10 ) . forEach ( ( item ) => {
1196- const summary = item . output_summary ? ` - ${ item . output_summary } ` : "" ;
1197- lines . push ( ` - \`${ item . command || "unknown command" } \`: \`${ item . status || "unknown" } \`${ summary } ` ) ;
1363+ const command = reviewTestCommandMarkdown ( task , String ( item . command || "unknown command" ) ) ;
1364+ const summary = item . output_summary ? ` - ${ linkGithubFileMentions ( task , String ( item . output_summary ) , knownFiles ) } ` : "" ;
1365+ lines . push ( ` - ${ command } : \`${ item . status || "unknown" } \`${ summary } ` ) ;
11981366 } ) ;
11991367 if ( testsRun . length > 10 ) {
12001368 lines . push ( "- Test list truncated after 10 entries." ) ;
0 commit comments