#
.SYNOPSIS
Renders the JSON output of Suggest-ArticleSignals.ps1 into a single,
non-blocking Markdown PR advisory comment with embedded feedback metadata.
.DESCRIPTION
The automated authoring-assist advisory posts ONE consolidated comment per
PR (update-in-place) built from the per-article suggestions. Each article
section carries HTML metadata markers so downstream feedback tooling (the
BC-ALAgentsInternal harvester) can correlate reactions and applied/not-applied
outcomes back to a specific suggestion via its stable `suggestionId`.
The comment is ADVISORY ONLY: it never fails a build, it only suggests
front-matter an author eyeballs and applies via a normal PR. R29
(validate_frontmatter.py), CI, and CODEOWNERS remain the gate.
Markers emitted (stable contract — do not change without bumping the tool
schema and updating the harvester):
top anchor (find/update)
.PARAMETER JsonPath
Path to a file containing the `Suggest-ArticleSignals.ps1 -AsJson` output.
If omitted, JSON is read from stdin.
.PARAMETER RepoUrl
Base repo URL used to build article permalinks (e.g.
https://github.com/microsoft/BCQuality). Optional; when omitted, article
paths are rendered as inline code without links.
.PARAMETER Ref
Git ref/SHA the suggestions were computed against, used in permalinks and
surfaced in the footer for provenance. Optional.
.PARAMETER DocUrl
URL surfaced in the feedback footer ("reply with why"). Optional.
.OUTPUTS
Writes the Markdown comment body to stdout.
#>
[CmdletBinding()]
param(
[string] $JsonPath,
[string] $RepoUrl,
[string] $Ref,
[string] $DocUrl
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Anchor = ''
if ($JsonPath) {
if (-not (Test-Path -LiteralPath $JsonPath)) { throw "JSON file not found: $JsonPath" }
$raw = Get-Content -LiteralPath $JsonPath -Raw
} else {
$raw = [Console]::In.ReadToEnd()
}
if ([string]::IsNullOrWhiteSpace($raw)) { throw 'No JSON input provided (empty file/stdin).' }
$data = $raw | ConvertFrom-Json
$reports = @()
if ($data.PSObject.Properties.Name -contains 'reports' -and $data.reports) {
$reports = @($data.reports)
}
function Format-Provenance {
param([object] $Proposal)
$tags = [System.Collections.Generic.List[string]]::new()
if ($Proposal.keywordBacked) { $tags.Add('keyword-backed') | Out-Null }
if ($Proposal.inBad) { $tags.Add('anti-pattern sample') | Out-Null }
if ($Proposal.proseOnly) { $tags.Add('prose-only') | Out-Null }
if ($tags.Count -eq 0) { $tags.Add('sample') | Out-Null }
return ($tags -join ', ')
}
function Format-SignalsBlock {
param([object] $Report)
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add('signals:') | Out-Null
foreach ($p in $Report.proposals) {
if ($Report.effect -eq 'suppress') {
$lines.Add(" - token: $($p.token)") | Out-Null
$lines.Add(' effect: suppress') | Out-Null
} else {
$lines.Add(" - $($p.token)") | Out-Null
}
}
return ($lines -join "`n")
}
$body = [System.Collections.Generic.List[string]]::new()
$body.Add($Anchor) | Out-Null
$schema = if ($data.PSObject.Properties.Name -contains 'schemaVersion') { $data.schemaVersion } else { 'unknown' }
$toolVer = if ($data.PSObject.Properties.Name -contains 'toolVersion') { $data.toolVersion } else { 'unknown' }
$generated = if ($data.PSObject.Properties.Name -contains 'generatedAt') { $data.generatedAt } else { '' }
$body.Add(('' -f $schema, $toolVer, $generated, @($reports).Count)) | Out-Null
$body.Add('') | Out-Null
$body.Add('## 🧭 Authoring-assist advisory') | Out-Null
$body.Add('') | Out-Null
if (@($reports).Count -eq 0) {
$body.Add('No routing head-matter suggestions for the knowledge articles changed in this PR. ✅') | Out-Null
$body.Add('') | Out-Null
$body.Add('Advisory only — never blocks a merge. Generated by `tools/Suggest-ArticleSignals.ps1`.') | Out-Null
return ($body -join "`n")
}
$body.Add(("This PR changes knowledge articles whose routing head-matter could be improved. " +
"These are **non-blocking suggestions** — paste the proposed ``signals:`` block into the " +
"article's front-matter if it looks right. R29 + CODEOWNERS still review the change.")) | Out-Null
$body.Add('') | Out-Null
foreach ($r in ($reports | Sort-Object domain, path)) {
$effect = $r.effect
$badge = if ($r.suppressor) { '🛡️ suppressor' } else { '🎯 trigger' }
$suggestionIds = @($r.proposals | ForEach-Object { $_.suggestionId }) -join ','
$body.Add(('' -f `
$r.path, $r.domain, $effect, ([string]$r.suppressor).ToLowerInvariant(), $suggestionIds)) | Out-Null
$title = if ($RepoUrl -and $Ref) { "[``$($r.path)``]($RepoUrl/blob/$Ref/$($r.path))" } else { "``$($r.path)``" }
$body.Add("### $title") | Out-Null
$body.Add("**Domain:** ``$($r.domain)`` · $badge · ``effect: $effect``") | Out-Null
$body.Add('') | Out-Null
if (@($r.proposals).Count -gt 0) {
$body.Add('Proposed front-matter:') | Out-Null
$body.Add('') | Out-Null
$body.Add('```yaml') | Out-Null
$body.Add((Format-SignalsBlock -Report $r)) | Out-Null
$body.Add('```') | Out-Null
$body.Add('') | Out-Null
foreach ($p in $r.proposals) {
$body.Add(("- ``$($p.token)`` — {0} · {1}" -f (Format-Provenance -Proposal $p), $p.note)) | Out-Null
}
$body.Add('') | Out-Null
}
if ($r.mismatch) {
$body.Add("> ⚠️ **Domain check:** $($r.mismatch)") | Out-Null
$body.Add('') | Out-Null
}
if ($r.applicability) {
$body.Add("> ⚠️ **Applicability:** $($r.applicability)") | Out-Null
$body.Add('') | Out-Null
}
if (@($r.keywordSuggestions).Count -gt 0) {
$kw = @($r.keywordSuggestions | ForEach-Object { '`' + $_ + '`' }) -join ', '
$body.Add("> 💡 **Keywords:** consider adding $kw") | Out-Null
$body.Add('') | Out-Null
}
}
$replyLink = if ($DocUrl) { " - reply with why" } else { '' }
$provenance = if ($Ref) { " against ``$Ref``" } else { '' }
$body.Add('---') | Out-Null
$body.Add("👍 useful · ❤️ especially valuable · 👎 wrong$replyLink") | Out-Null
$body.Add("Advisory only — never blocks a merge. Generated by ``tools/Suggest-ArticleSignals.ps1``$provenance. Suggestions carry stable ids for feedback; applying them is optional and human-reviewed.") | Out-Null
return ($body -join "`n")