You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by mo...@apache.org on 2026/04/25 00:08:06 UTC

(doris-website) branch master updated: Improve docs AI review comment output (#3580)

This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git


The following commit(s) were added to refs/heads/master by this push:
     new d33f93bfcf0 Improve docs AI review comment output (#3580)
d33f93bfcf0 is described below

commit d33f93bfcf0795522ebdc031de2e12429bdcac2e
Author: Mingyu Chen (Rayner) <mo...@163.com>
AuthorDate: Fri Apr 24 17:08:00 2026 -0700

    Improve docs AI review comment output (#3580)
---
 .github/workflows/docs-ai-review.yml               |  73 +++----
 package.json                                       |   1 +
 .../__tests__/docs-governance.test.js              |  70 ++++++-
 .../docs-governance/render-ai-review-comment.js    | 219 +++++++++++++++++++++
 4 files changed, 317 insertions(+), 46 deletions(-)

diff --git a/.github/workflows/docs-ai-review.yml b/.github/workflows/docs-ai-review.yml
index bdb14371647..23a1fe92b12 100644
--- a/.github/workflows/docs-ai-review.yml
+++ b/.github/workflows/docs-ai-review.yml
@@ -3,12 +3,6 @@ name: Docs AI Review
 on:
   issue_comment:
     types: [created]
-  workflow_dispatch:
-    inputs:
-      pr_number:
-        description: Pull request number to run docs AI review for
-        required: true
-        type: string
 
 # Same trust model as opencode-review.yml:
 # - triggered by a trusted maintainer/collaborator comment
@@ -24,20 +18,16 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 20
     if: >-
-      github.event_name == 'workflow_dispatch' ||
+      github.event.issue.pull_request &&
+      contains(github.event.comment.body, '/review-docs') &&
       (
-        github.event.issue.pull_request &&
-        contains(github.event.comment.body, '/review-docs') &&
-        (
-          github.event.comment.author_association == 'MEMBER' ||
-          github.event.comment.author_association == 'OWNER' ||
-          github.event.comment.author_association == 'COLLABORATOR'
-        )
+        github.event.comment.author_association == 'MEMBER' ||
+        github.event.comment.author_association == 'OWNER' ||
+        github.event.comment.author_association == 'COLLABORATOR'
       )
 
     steps:
       - name: Validate trigger command
-        if: github.event_name == 'issue_comment'
         run: |
           body=$(jq -r '.comment.body' "$GITHUB_EVENT_PATH")
 
@@ -52,16 +42,10 @@ jobs:
         id: pr
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          EVENT_NAME: ${{ github.event_name }}
-          DISPATCH_PR_NUMBER: ${{ inputs.pr_number }}
         run: |
           set -euo pipefail
 
-          if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
-            PR_NUMBER="$DISPATCH_PR_NUMBER"
-          else
-            PR_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
-          fi
+          PR_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
 
           PR_JSON=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER})
 
@@ -124,6 +108,23 @@ jobs:
           jq '{schema_version, changed_files, high_risk, review_agents: [.review_agents[].id], sync_groups: [.sync_groups[].sync_group_id]}' \
             website-quality-governance/generated/ai-review-packet.json
 
+      - name: Run changed docs governance report
+        env:
+          BASE_SHA: ${{ steps.pr.outputs.base_sha }}
+          HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
+        run: |
+          set -euo pipefail
+
+          changed_files=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" | paste -sd, -)
+
+          node scripts/docs-governance/report.js \
+            --files "$changed_files" \
+            --format json \
+            --output website-quality-governance/generated/docs-governance-report.json
+
+          jq '{summary, findings: [.findings[] | {severity, rule, path, line, message, related_paths}]}' \
+            website-quality-governance/generated/docs-governance-report.json
+
       - name: Upsert advisory PR comment
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -134,29 +135,15 @@ jobs:
           set -euo pipefail
 
           packet=website-quality-governance/generated/ai-review-packet.json
+          report=website-quality-governance/generated/docs-governance-report.json
           marker='<!-- docs-ai-review:packet -->'
 
-          changed_count=$(jq '.changed_files | length' "$packet")
-          high_risk=$(jq -r '.high_risk.required // false' "$packet")
-          agent_count=$(jq '.review_agents | length' "$packet")
-          sync_group_count=$(jq '.sync_groups | length' "$packet")
-
-          cat > /tmp/docs-ai-review-comment.md <<EOF_COMMENT
-          $marker
-          ## Docs AI Review
-
-          Docs AI review packet prepared for this PR.
-
-          - Trigger: \`/review-docs\`
-          - Changed files in packet: \`$changed_count\`
-          - High-risk docs path matched: \`$high_risk\`
-          - Review agents included: \`$agent_count\`
-          - Sync groups included: \`$sync_group_count\`
-          - Base SHA: \`$BASE_SHA\`
-          - Head SHA: \`$HEAD_SHA\`
-
-          This MVP prepares the packet for advisory AI review. AI findings, when produced, are suggestions only; deterministic governance CI remains the blocking signal.
-          EOF_COMMENT
+          node scripts/docs-governance/render-ai-review-comment.js \
+            --packet "$packet" \
+            --report "$report" \
+            --base-sha "$BASE_SHA" \
+            --head-sha "$HEAD_SHA" \
+            --output /tmp/docs-ai-review-comment.md
 
           existing_comment_id=$(
             gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
diff --git a/package.json b/package.json
index a7a7a568784..e133faa642c 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
         "docs:ai-review:prepare": "node scripts/docs-governance/prepare-ai-review.js",
         "docs:ai-review:validate": "node scripts/docs-governance/validate-ai-review-output.js",
         "docs:ai-review:dedupe": "node scripts/docs-governance/dedupe-ai-review-comments.js",
+        "docs:ai-review:comment": "node scripts/docs-governance/render-ai-review-comment.js",
         "docs:lint": "node scripts/docs-governance/report.js --format github",
         "docs:lint:changed": "node scripts/docs-governance/report.js --changed --format github",
         "docs:report": "node scripts/docs-governance/report.js --format json --output website-quality-governance/generated/docs-governance-report.json",
diff --git a/scripts/docs-governance/__tests__/docs-governance.test.js b/scripts/docs-governance/__tests__/docs-governance.test.js
index 7d143f8f13a..575dc782df4 100644
--- a/scripts/docs-governance/__tests__/docs-governance.test.js
+++ b/scripts/docs-governance/__tests__/docs-governance.test.js
@@ -1423,6 +1423,64 @@ test('dedupeAiReviewFindings returns stable unique findings', () => {
   );
 });
 
+test('renderDocsAiReviewComment lists actionable changed-file findings', () => {
+  const { renderDocsAiReviewComment } = require('../render-ai-review-comment');
+  const comment = renderDocsAiReviewComment({
+    packet: {
+      changed_files: ['docs/sql-manual/sql-functions/bad-function.md'],
+      high_risk: {
+        required: true,
+        matched_paths: [
+          {
+            path: 'docs/sql-manual/sql-functions/bad-function.md',
+            rule: 'docs/sql-manual/',
+          },
+        ],
+      },
+      review_agents: [{ id: 'docs-clarity' }, { id: 'i18n-version-sync' }],
+      sync_groups: [{ sync_group_id: 'main_docs:sql-manual/sql-functions/bad-function' }],
+    },
+    report: {
+      summary: {
+        findings: 2,
+        by_severity: { error: 1, warning: 1 },
+      },
+      findings: [
+        {
+          severity: 'error',
+          rule: 'sql-function-section-order',
+          path: 'docs/sql-manual/sql-functions/bad-function.md',
+          line: 12,
+          message: 'SQL function docs must contain sections in the required order.',
+          owner: '@apache/doris-website-maintainers',
+        },
+        {
+          severity: 'warning',
+          rule: 'i18n-sync-locale-counterpart',
+          path: 'docs/sql-manual/sql-functions/bad-function.md',
+          line: 1,
+          message: 'Chinese localized counterpart needs the same update.',
+          related_paths: [
+            'i18n/zh-CN/docusaurus-plugin-content-docs/current/sql-manual/sql-functions/bad-function.md',
+          ],
+        },
+      ],
+    },
+    baseSha: '9f5aa557f929f98e064426fcabe90dfc40dc743e',
+    headSha: '2288afd2f3727fa50c438ee3d2be1a813ee7d292',
+  });
+
+  assert.match(comment, /<!-- docs-ai-review:packet -->/);
+  assert.match(comment, /Findings to fix/);
+  assert.match(comment, /`docs\/sql-manual\/sql-functions\/bad-function.md:12`/);
+  assert.match(comment, /sql-function-section-order/);
+  assert.match(comment, /Update the SQL function page so it follows the required function documentation structure/);
+  assert.match(comment, /i18n\/zh-CN\/docusaurus-plugin-content-docs\/current\/sql-manual\/sql-functions\/bad-function.md/);
+  assert.match(comment, /High-risk docs path matched: `true`/);
+  assert.match(comment, /Base SHA: `9f5aa557/);
+  assert.match(comment, /Head SHA: `2288afd2/);
+});
+
 test('Week 7 AI review scripts and workflow are exposed without replacing the generic review workflow', () => {
   const rootDir = path.join(__dirname, '..', '..', '..');
   const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
@@ -1438,12 +1496,18 @@ test('Week 7 AI review scripts and workflow are exposed without replacing the ge
   assert.equal(packageJson.scripts['docs:ai-review:prepare'], 'node scripts/docs-governance/prepare-ai-review.js');
   assert.equal(packageJson.scripts['docs:ai-review:validate'], 'node scripts/docs-governance/validate-ai-review-output.js');
   assert.equal(packageJson.scripts['docs:ai-review:dedupe'], 'node scripts/docs-governance/dedupe-ai-review-comments.js');
+  assert.equal(packageJson.scripts['docs:ai-review:comment'], 'node scripts/docs-governance/render-ai-review-comment.js');
   assert.match(docsAiReviewWorkflow, /\/review-docs/);
-  assert.match(docsAiReviewWorkflow, /ai-review-docs/);
-  assert.match(docsAiReviewWorkflow, /pull-requests: read/);
+  assert.match(docsAiReviewWorkflow, /issue_comment:/);
+  assert.doesNotMatch(docsAiReviewWorkflow, /workflow_dispatch:/);
+  assert.match(docsAiReviewWorkflow, /docs-governance-report\.json/);
+  assert.match(docsAiReviewWorkflow, /render-ai-review-comment\.js/);
+  assert.doesNotMatch(docsAiReviewWorkflow, /DISPATCH_PR_NUMBER/);
+  assert.match(docsAiReviewWorkflow, /pull-requests: write/);
   assert.match(docsAiReviewWorkflow, /contents: read/);
   assert.match(docsAiReviewWorkflow, /issues: write/);
-  assert.ok(docsAiReviewWorkflow.indexOf('Checkout PR head') < docsAiReviewWorkflow.indexOf('issues: write'));
+  assert.ok(docsAiReviewWorkflow.indexOf('Checkout PR head') < docsAiReviewWorkflow.indexOf('Run changed docs governance report'));
+  assert.ok(docsAiReviewWorkflow.indexOf('Run changed docs governance report') < docsAiReviewWorkflow.indexOf('Upsert advisory PR comment'));
   assert.match(openCodeWorkflow, /\/review/);
 });
 
diff --git a/scripts/docs-governance/render-ai-review-comment.js b/scripts/docs-governance/render-ai-review-comment.js
new file mode 100644
index 00000000000..2c2a0edf8b1
--- /dev/null
+++ b/scripts/docs-governance/render-ai-review-comment.js
@@ -0,0 +1,219 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+const { ensureDirForFile, parseArgs } = require('./lib');
+
+const MARKER = '<!-- docs-ai-review:packet -->';
+const DEFAULT_MAX_FINDINGS = 25;
+const SEVERITY_ORDER = {
+  error: 0,
+  warning: 1,
+  info: 2,
+};
+
+function readJson(filePath) {
+  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+}
+
+function shortSha(sha) {
+  return sha ? String(sha).slice(0, 12) : 'unknown';
+}
+
+function inlineCode(value) {
+  const normalized = value === undefined || value === null || value === '' ? 'unknown' : value;
+  return `\`${String(normalized).replace(/`/g, '\\`')}\``;
+}
+
+function countBySeverity(findings, summary = {}) {
+  const fromSummary = summary.by_severity || {};
+  return {
+    error: fromSummary.error || findings.filter((finding) => finding.severity === 'error').length,
+    warning: fromSummary.warning || findings.filter((finding) => finding.severity === 'warning').length,
+    info: fromSummary.info || findings.filter((finding) => finding.severity === 'info').length,
+  };
+}
+
+function sortFindings(findings) {
+  return [...findings].sort((left, right) => {
+    const severity = (SEVERITY_ORDER[left.severity] ?? 99) - (SEVERITY_ORDER[right.severity] ?? 99);
+    if (severity !== 0) {
+      return severity;
+    }
+    const pathCompare = String(left.path || '').localeCompare(String(right.path || ''));
+    if (pathCompare !== 0) {
+      return pathCompare;
+    }
+    const lineCompare = (left.line || 0) - (right.line || 0);
+    if (lineCompare !== 0) {
+      return lineCompare;
+    }
+    return String(left.rule || '').localeCompare(String(right.rule || ''));
+  });
+}
+
+function suggestActionForFinding(finding) {
+  const rule = String(finding.rule || '');
+
+  if (rule.startsWith('sql-function-')) {
+    return 'Update the SQL function page so it follows the required function documentation structure, examples, return type, and NULL-handling coverage.';
+  }
+  if (rule.startsWith('feature-doc-')) {
+    return 'Complete the feature page with overview, basic usage, options, realistic examples, caveats, and best practices where applicable.';
+  }
+  if (rule.startsWith('i18n-sync-')) {
+    return 'Update the current, versioned, and localized counterpart pages consistently, or document why this change is intentionally asymmetric.';
+  }
+  if (rule.startsWith('link-')) {
+    return 'Fix the target path, heading anchor, inbound reference, or redirect so readers do not hit broken navigation.';
+  }
+  if (rule.startsWith('sidebar-')) {
+    return 'Update the sidebar/navigation entry so it matches the actual document path and intended information architecture.';
+  }
+  if (rule.startsWith('frontmatter-')) {
+    return 'Correct the Docusaurus front matter metadata so the page renders, routes, and appears in search correctly.';
+  }
+  if (rule.startsWith('markdown-')) {
+    return 'Fix the Markdown structure so headings, code fences, and document hierarchy are unambiguous.';
+  }
+  if (rule.startsWith('seo-')) {
+    return 'Improve the SEO/GEO metadata or search policy so users and AI answer engines get clear page context.';
+  }
+
+  return 'Review the finding and update the affected documentation so the reader experience remains complete and unambiguous.';
+}
+
+function formatFinding(finding, index) {
+  const severity = String(finding.severity || 'warning').toUpperCase();
+  const location = finding.line ? `${finding.path}:${finding.line}` : finding.path;
+  const lines = [
+    `${index}. **${severity}** ${inlineCode(location)} - ${inlineCode(finding.rule)}`,
+    `   - Problem: ${finding.message || 'No finding message provided.'}`,
+    `   - Fix: ${suggestActionForFinding(finding)}`,
+  ];
+
+  if (finding.related_paths && finding.related_paths.length) {
+    lines.push(`   - Related: ${finding.related_paths.map(inlineCode).join(', ')}`);
+  }
+  if (finding.owner) {
+    lines.push(`   - Owner: ${finding.owner}`);
+  }
+
+  return lines.join('\n');
+}
+
+function formatLimitedList(values, limit = 10) {
+  const uniqueValues = [...new Set(values.filter(Boolean))];
+  const shown = uniqueValues.slice(0, limit);
+  const lines = shown.map((value) => `- ${inlineCode(value)}`);
+  if (uniqueValues.length > shown.length) {
+    lines.push(`- ... ${uniqueValues.length - shown.length} more`);
+  }
+  return lines;
+}
+
+function renderDocsAiReviewComment(options = {}) {
+  const packet = options.packet || {};
+  const report = options.report || {};
+  const findings = sortFindings(Array.isArray(report.findings) ? report.findings : []);
+  const maxFindings = Number.isFinite(options.maxFindings) ? options.maxFindings : DEFAULT_MAX_FINDINGS;
+  const shownFindings = findings.slice(0, maxFindings);
+  const severityCounts = countBySeverity(findings, report.summary);
+  const changedFiles = Array.isArray(packet.changed_files) ? packet.changed_files : [];
+  const highRisk = packet.high_risk || {};
+  const highRiskMatches = Array.isArray(highRisk.matched_paths) ? highRisk.matched_paths : [];
+  const reviewAgents = Array.isArray(packet.review_agents) ? packet.review_agents : [];
+  const syncGroups = Array.isArray(packet.sync_groups) ? packet.sync_groups : [];
+
+  const lines = [
+    MARKER,
+    '## Docs AI Review',
+    '',
+    'Changed-file docs review results for this PR.',
+    '',
+    '### Summary',
+    `- Trigger: ${inlineCode('/review-docs')}`,
+    `- Changed files reviewed: ${inlineCode(changedFiles.length)}`,
+    `- Findings: ${inlineCode(findings.length)} (${severityCounts.error} error, ${severityCounts.warning} warning, ${severityCounts.info} info)`,
+    `- High-risk docs path matched: ${inlineCode(Boolean(highRisk.required))}`,
+    `- Review agents in packet: ${inlineCode(reviewAgents.length)}`,
+    `- Sync groups in packet: ${inlineCode(syncGroups.length)}`,
+    `- Base SHA: ${inlineCode(shortSha(options.baseSha))}`,
+    `- Head SHA: ${inlineCode(shortSha(options.headSha))}`,
+    '',
+    '### Findings to fix',
+  ];
+
+  if (shownFindings.length) {
+    lines.push(...shownFindings.map((finding, index) => formatFinding(finding, index + 1)));
+    if (findings.length > shownFindings.length) {
+      lines.push('', `_${findings.length - shownFindings.length} additional findings are not shown. Check the docs governance job output for the full list._`);
+    }
+  } else {
+    lines.push('No changed-file governance findings were produced. Review the high-risk scope and sync groups below before merging.');
+  }
+
+  if (highRiskMatches.length) {
+    lines.push(
+      '',
+      '### High-risk scope',
+      ...formatLimitedList(highRiskMatches.map((match) => `${match.path} (${match.rule})`)),
+    );
+  }
+
+  if (syncGroups.length) {
+    lines.push(
+      '',
+      '### Sync groups to verify',
+      ...formatLimitedList(syncGroups.map((group) => group.sync_group_id)),
+    );
+  }
+
+  lines.push(
+    '',
+    '### Next steps',
+    '- Fix the findings above, especially errors and warnings.',
+    '- Confirm current, active versioned, and localized docs stay aligned when the same topic is changed.',
+    '- Re-run `/review-docs` after updating the PR.',
+    '',
+    'AI review output is advisory. Deterministic governance CI remains the blocking signal.',
+  );
+
+  return `${lines.join('\n')}\n`;
+}
+
+function runCli() {
+  const args = parseArgs(process.argv.slice(2));
+  if (!args.packet || !args.report) {
+    throw new Error('Usage: node scripts/docs-governance/render-ai-review-comment.js --packet <packet.json> --report <report.json> [--output <comment.md>]');
+  }
+
+  const packet = readJson(path.resolve(process.cwd(), args.packet));
+  const report = readJson(path.resolve(process.cwd(), args.report));
+  const maxFindings = args['max-findings'] ? Number(args['max-findings']) : DEFAULT_MAX_FINDINGS;
+  const comment = renderDocsAiReviewComment({
+    packet,
+    report,
+    baseSha: args['base-sha'],
+    headSha: args['head-sha'],
+    maxFindings,
+  });
+
+  if (args.output) {
+    const outputPath = path.resolve(process.cwd(), args.output);
+    ensureDirForFile(outputPath);
+    fs.writeFileSync(outputPath, comment, 'utf8');
+  } else {
+    process.stdout.write(comment);
+  }
+}
+
+if (require.main === module) {
+  runCli();
+}
+
+module.exports = {
+  MARKER,
+  renderDocsAiReviewComment,
+  suggestActionForFinding,
+};


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org