1- import aiConfig from '../../../mcp/server/github-workflow/config.mjs' ;
2- import Base from '../../../../src/core/Base.mjs' ;
3- import crypto from 'crypto' ;
4- import fs from 'fs/promises' ;
5- import logger from '../../../mcp/server/github-workflow/logger.mjs' ;
6- import matter from 'gray-matter' ;
7- import path from 'path' ;
8- import GraphqlService from '../GraphqlService.mjs' ;
9- import ReleaseNotesSyncer from './ReleaseNotesSyncer.mjs' ;
1+ import aiConfig from '../../../mcp/server/github-workflow/config.mjs' ;
2+ import Base from '../../../../src/core/Base.mjs' ;
3+ import crypto from 'crypto' ;
4+ import fs from 'fs/promises' ;
5+ import logger from '../../../mcp/server/github-workflow/logger.mjs' ;
6+ import matter from 'gray-matter' ;
7+ import path from 'path' ;
8+ import GraphqlService from '../GraphqlService.mjs' ;
9+ import ReleaseNotesSyncer from './ReleaseNotesSyncer.mjs' ;
1010import { FETCH_DISCUSSIONS_FOR_SYNC } from '../queries/discussionQueries.mjs' ;
1111import contentPath from '../shared/contentPath.mjs' ;
1212import {
1313 createContentIndexEntry ,
1414 updateContentIndex
1515} from '../shared/contentIndex.mjs' ;
16- import pruneEmptyDirs from '../shared/pruneEmptyDirs.mjs' ;
17- import { verifyDiscussionFrontmatter } from './verifyFrontmatterIntegrity.mjs' ;
16+ import { createContentTrustSummary , projectAuthoredNodeTrust } from '../shared/conversationTrust.mjs' ;
17+ import pruneEmptyDirs from '../shared/pruneEmptyDirs.mjs' ;
18+ import { verifyDiscussionFrontmatter } from './verifyFrontmatterIntegrity.mjs' ;
1819
1920const issueSyncConfig = aiConfig . issueSync ;
2021
@@ -77,6 +78,22 @@ class DiscussionSyncer extends Base {
7778 return path . relative ( aiConfig . projectRoot , p ) ;
7879 }
7980
81+ /**
82+ * @summary Projects one GitHub-authored discussion sync node through content trust policy.
83+ * @param {Object } node GitHub authored node carrying optional `author.login` and `body`.
84+ * @param {Object } summary Machine-readable content-trust summary accumulator.
85+ * @param {String } signalPath Stable path label for sanitizer signal metadata.
86+ * @returns {Object } Projected node with sanitized body for untrusted authors.
87+ * @private
88+ */
89+ #projectAuthoredNode( node , summary , signalPath ) {
90+ return projectAuthoredNodeTrust ( node , {
91+ summary,
92+ path : signalPath ,
93+ productNameDenylist : issueSyncConfig . productNameDenylist || [ ]
94+ } ) . node ;
95+ }
96+
8097 /**
8198 * @summary Pre-computes bucket counts and indices for all discussions based on historical releases.
8299 * @param {Object } metadata The sync metadata.
@@ -89,16 +106,16 @@ class DiscussionSyncer extends Base {
89106
90107 for ( const [ idStr , discussion ] of Object . entries ( metadata . discussions || { } ) ) {
91108 combined . set ( parseInt ( idStr , 10 ) , {
92- number : parseInt ( idStr , 10 ) ,
93- closed : discussion . closed ,
109+ number : parseInt ( idStr , 10 ) ,
110+ closed : discussion . closed ,
94111 closedAt : discussion . closedAt
95112 } ) ;
96113 }
97114
98115 for ( const discussion of fetchedDiscussions ) {
99116 combined . set ( discussion . number , {
100- number : discussion . number ,
101- closed : discussion . closed ,
117+ number : discussion . number ,
118+ closed : discussion . closed ,
102119 closedAt : discussion . closedAt
103120 } ) ;
104121 }
@@ -230,9 +247,9 @@ class DiscussionSyncer extends Base {
230247
231248 while ( hasNextPage ) {
232249 const data = await GraphqlService . query ( FETCH_DISCUSSIONS_FOR_SYNC , {
233- owner : aiConfig . owner ,
234- repo : aiConfig . repo ,
235- limit : 50 ,
250+ owner : aiConfig . owner ,
251+ repo : aiConfig . repo ,
252+ limit : 50 ,
236253 cursor,
237254 maxComments : 50 ,
238255 maxReplies : 20
@@ -296,38 +313,53 @@ class DiscussionSyncer extends Base {
296313 try {
297314 const targetPath = this . #getDiscussionPath( discussion , planBuckets ) ;
298315 if ( ! targetPath ) continue ;
316+ const contentTrust = createContentTrustSummary ( ) ;
317+ const projectedDiscussion = this . #projectAuthoredNode( discussion , contentTrust , 'body' ) ;
299318
300319 const frontmatter = {
301- number : discussion . number ,
302- title : discussion . title ,
303- author : discussion . author ?. login || 'unknown' ,
304- category : discussion . category ?. name || 'Uncategorized' ,
305- createdAt : discussion . createdAt ,
306- updatedAt : discussion . updatedAt ,
307- closed : discussion . closed ,
308- closedAt : discussion . closedAt
320+ number : discussion . number ,
321+ title : discussion . title ,
322+ author : discussion . author ?. login || 'unknown' ,
323+ category : discussion . category ?. name || 'Uncategorized' ,
324+ createdAt : discussion . createdAt ,
325+ updatedAt : discussion . updatedAt ,
326+ closed : discussion . closed ,
327+ closedAt : discussion . closedAt ,
328+ contentTrust
309329 } ;
310330
311- let body = discussion . body || '' ;
331+ let body = projectedDiscussion . body || '' ;
312332
313333 // Build comments structure
314334 if ( discussion . comments && discussion . comments . nodes && discussion . comments . nodes . length > 0 ) {
315335 body += '\n\n## Comments\n\n' ;
316336 for ( const comment of discussion . comments . nodes ) {
337+ const projectedComment = this . #projectAuthoredNode(
338+ comment ,
339+ contentTrust ,
340+ `comment:${ comment . id || comment . createdAt || 'unknown' } `
341+ ) ;
342+
317343 body += `### \`@${ comment . author ?. login || 'unknown' } \` commented on ${ comment . createdAt } \n\n` ;
318344 if ( comment . isAnswer ) {
319345 body += '> [!ANSWER]\n\n' ;
320346 }
321- body += `${ comment . body } \n\n` ;
347+ body += `${ projectedComment . body } \n\n` ;
322348
323349 // Parse replies if any
324350 if ( comment . replies && comment . replies . nodes && comment . replies . nodes . length > 0 ) {
325351 for ( const reply of comment . replies . nodes ) {
352+ const projectedReply = this . #projectAuthoredNode(
353+ reply ,
354+ contentTrust ,
355+ `comment:${ comment . id || comment . createdAt || 'unknown' } /reply:${ reply . id || reply . createdAt || 'unknown' } `
356+ ) ;
357+
326358 body += `#### Reply depth=1 by \`@${ reply . author ?. login || 'unknown' } \` on ${ reply . createdAt } \n\n` ;
327359 if ( reply . isAnswer ) {
328360 body += '> [!ANSWER]\n\n' ;
329361 }
330- body += `${ reply . body } \n\n` ;
362+ body += `${ projectedReply . body } \n\n` ;
331363 }
332364 }
333365 body += '---\n\n' ;
@@ -401,23 +433,23 @@ class DiscussionSyncer extends Base {
401433
402434 allDiscussions . forEach ( d => {
403435 metadata . discussions [ d . number ] = {
404- number : d . number ,
405- closed : d . closed ,
406- closedAt : d . closedAt ,
436+ number : d . number ,
437+ closed : d . closed ,
438+ closedAt : d . closedAt ,
407439 contentHash : d . contentHash ,
408- path : d . relativeOutputPath
440+ path : d . relativeOutputPath
409441 } ;
410442
411443 const plan = planBuckets . get ( d . number ) ;
412444
413445 indexEntries . push ( createContentIndexEntry ( {
414446 issueSyncConfig,
415- type : 'discussions' ,
416- id : d . number ,
417- filePath : path . resolve ( aiConfig . projectRoot , d . relativeOutputPath ) ,
447+ type : 'discussions' ,
448+ id : d . number ,
449+ filePath : path . resolve ( aiConfig . projectRoot , d . relativeOutputPath ) ,
418450 itemIndex : plan ? plan . itemIndex : 0 ,
419- version : plan ?. version || null ,
420- bucket : null
451+ version : plan ?. version || null ,
452+ bucket : null
421453 } ) ) ;
422454 } ) ;
423455
0 commit comments