diff --git a/.github/workflows/rst-preview-comment.yml b/.github/workflows/rst-preview-comment.yml
new file mode 100644
index 000000000..811e97ff2
--- /dev/null
+++ b/.github/workflows/rst-preview-comment.yml
@@ -0,0 +1,46 @@
+name: RST Preview Comment
+
+on:
+ issues:
+ types: [opened, edited]
+
+jobs:
+ post-rst-preview:
+ # Only run for issues created from the Coding Guideline template
+ # The template automatically adds the "coding guideline" label
+ if: contains(github.event.issue.labels.*.name, 'coding guideline')
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+
+ - name: Install Pandoc
+ uses: pandoc/actions/setup@v1
+
+ - name: Generate RST preview comment
+ id: generate-comment
+ run: |
+ # Generate the comment content
+ COMMENT=$(cat <<'EOF' | uv run python scripts/generate-rst-comment.py
+ ${{ toJson(github.event.issue) }}
+ EOF
+ )
+
+ # Write to file to preserve formatting
+ echo "$COMMENT" > /tmp/comment-body.md
+
+ - name: Post or update comment
+ uses: peter-evans/create-or-update-comment@v4
+ with:
+ issue-number: ${{ github.event.issue.number }}
+ body-path: /tmp/comment-body.md
+ comment-author: 'github-actions[bot]'
+ # Use a hidden marker to identify and update the same comment
+ body-includes: ''
+ edit-mode: replace
diff --git a/scripts/README.md b/scripts/README.md
index 0f9b3adbc..3e72ef220 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -1,25 +1,115 @@
-### `auto-pr-helper.py`
+# Scripts
-This script is a utility for automating the generation of guidelines. It takes a GitHub issue's JSON data from standard input, parses its body (which is expected to follow a specific issue template), and converts it into a formatted reStructuredText (`.rst`) guideline.
+This directory contains utility scripts for managing coding guidelines.
+
+**Location: scripts/README.md (replaces existing file)**
+
+## Scripts Overview
+
+| Script | Purpose |
+|--------|---------|
+| `auto-pr-helper.py` | Transforms issue JSON to RST format (used by auto-PR workflow) |
+| `generate-rst-comment.py` | Generates GitHub comment with RST preview |
+| `guideline_utils.py` | Shared utility functions for guideline processing |
+
+---
+
+## `guideline_utils.py`
+
+A shared module containing common functions used by other scripts:
+
+- `md_to_rst()` - Convert Markdown to reStructuredText using Pandoc
+- `normalize_md()` - Fix Markdown formatting issues
+- `normalize_list_separation()` - Ensure proper list formatting for Pandoc
+- `extract_form_fields()` - Parse issue body into field dictionary
+- `guideline_template()` - Generate RST from fields dictionary
+- `chapter_to_filename()` - Convert chapter name to filename slug
+- `save_guideline_file()` - Append guideline to chapter file
---
-### How to Use
+## `auto-pr-helper.py`
+
+This script transforms a GitHub issue's JSON data into reStructuredText format for coding guidelines.
+
+### Usage
+
+```bash
+# From a local JSON file
+cat path/to/issue.json | uv run python scripts/auto-pr-helper.py
+
+# From GitHub API directly
+curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/123 | uv run python scripts/auto-pr-helper.py
+
+# Save the output to the appropriate chapter file
+cat path/to/issue.json | uv run python scripts/auto-pr-helper.py --save
+```
+
+### Options
+
+- `--save`: Save the generated RST content to the appropriate chapter file in `src/coding-guidelines/`
+
+---
-The script reads a JSON payload from **standard input**. The most common way to provide this input is by using a pipe (`|`) to feed the output of another command into the script.
+## `generate-rst-comment.py`
-#### 1. Using a Local JSON File
+This script generates a formatted GitHub comment containing an RST preview of a coding guideline. It's used by the RST Preview Comment workflow to post helpful comments on coding guideline issues.
-For local testing, you can use `cat` to pipe the contents of a saved GitHub issue JSON file into the script.
+### Usage
```bash
-cat path/to/your_issue.json | uv run scripts/auto-pr-helper.py
+# From a local JSON file
+cat path/to/issue.json | uv run python scripts/generate-rst-comment.py
+
+# From GitHub API directly
+curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/123 | uv run python scripts/generate-rst-comment.py
```
-#### 2. Fetching from the GitHub API directly
-You can fetch the data for a live issue directly from the GitHub API using curl and pipe it to the script. This is useful for getting the most up-to-date content.
+### Output
+
+The script outputs a Markdown-formatted comment that includes:
+
+1. **Instructions** on how to use the RST content
+2. **Target file path** indicating which chapter file to add the guideline to
+3. **Collapsible RST content** that can be copied and pasted
+
+### Example Output
+
+```markdown
+## 📋 RST Preview for Coding Guideline
+
+This is an automatically generated preview...
+
+### 📁 Target File
+Add this guideline to: `src/coding-guidelines/concurrency.rst`
+
+### 📝 How to Use This
+1. Fork the repository...
+...
+
+
+📄 Click to expand RST content
+
+\`\`\`rst
+.. guideline:: My Guideline Title
+ :id: gui_ABC123...
+\`\`\`
+
+
+```
+
+---
+
+## How to Get Issue JSON from GitHub API
+
+To work with these scripts locally, you can fetch issue data from the GitHub API:
```bash
-curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156 | uv run ./scripts/auto-pr-helper.py
+curl https://api.github.com/repos/OWNER/REPO/issues/ISSUE_NUMBER > issue.json
```
+
+For example:
+
+```bash
+curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156 > issue.json
```
diff --git a/scripts/auto-pr-helper.py b/scripts/auto-pr-helper.py
index 62d5ca2ee..b55a3d126 100644
--- a/scripts/auto-pr-helper.py
+++ b/scripts/auto-pr-helper.py
@@ -1,190 +1,32 @@
-import argparse
-import json
-import os
-import re
-import sys
-from textwrap import dedent, indent
-
-import pypandoc
-
-scriptpath = "../"
-script_dir = os.path.dirname(os.path.abspath(__file__))
-parent_dir = os.path.abspath(os.path.join(script_dir, ".."))
-sys.path.append(parent_dir)
-
-from generate_guideline_templates import (
- guideline_rst_template,
- issue_header_map,
-)
-
-
-def md_to_rst(markdown: str) -> str:
- return pypandoc.convert_text(
- markdown,
- 'rst',
- format='markdown',
- extra_args=['--wrap=none']
- )
-
-
-def normalize_list_separation(text: str) -> str:
- """
- Ensures every new list block is preceded by a blank line,
- required for robust parsing by Pandoc when targeting RST
- """
- # Regex to identify any line that starts a Markdown list item (* or -)
- _list_item_re = re.compile(r"^[ \t]*[*-][ \t]+")
-
- output_buffer = []
- for line in text.splitlines():
- is_item = bool(_list_item_re.match(line))
-
- # Get the last line appended to the output buffer
- prev = output_buffer[-1] if output_buffer else ""
-
- # Check if a blank line needs to be inserted before list
- # (Current is item) AND (Prev is not blank) AND (Prev is not an item)
- if is_item and prev.strip() and not _list_item_re.match(prev):
- # Insert a blank line to clearly separate the new list block
- output_buffer.append("")
-
- output_buffer.append(line)
-
- return "\n".join(output_buffer)
-
-
-def normalize_md(issue_body: str) -> str:
- """
- Fix links and mixed bold/code that confuse Markdown parser
- """
- # Fix links with inline-code: [`link`](url) => [link](url)
- issue_body = re.sub(
- r"\[\s*`([^`]+)`\s*\]\(([^)]+)\)",
- r"[\1](\2)",
- issue_body
- )
-
- # Fix mixed bold/code formatting
- # **`code`** => `code`
- issue_body = re.sub(
- r"\*\*`([^`]+)`\*\*",
- r"`\1`",
- issue_body
- )
-
- # `**code**` => `code`
- issue_body = re.sub(
- r"`\*\*([^`]+)\*\*`",
- r"`\1`",
- issue_body
- )
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT OR Apache-2.0
+# SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors
- return issue_body
+"""
+This script transforms a GitHub issue JSON into RST format for coding guidelines.
+It reads a GitHub issue's JSON data from standard input, parses its body
+(which is expected to follow a specific issue template), and converts it
+into a formatted reStructuredText (.rst) guideline.
-def extract_form_fields(issue_body: str) -> dict:
- """
- This function parses issues json into a dict of important fields
- """
+Usage:
+ cat issue.json | uv run python scripts/auto-pr-helper.py
+ cat issue.json | uv run python scripts/auto-pr-helper.py --save
- fields = dict.fromkeys(issue_header_map.values(), "")
+Location: scripts/auto-pr-helper.py (replaces existing file)
+"""
- lines = issue_body.splitlines()
- current_key = None
- current_value_lines = []
-
- lines.append("### END") # Sentinel to process last field
-
- # Look for '###' in every line, ### represent a sections/field in a guideline
- for line in lines:
- header_match = re.match(r"^### (.+)$", line.strip())
- if header_match:
- # Save previous field value if any
- if current_key is not None:
- value = "\n".join(current_value_lines).strip()
- # `_No response_` represents an empty field
- if value == "_No response_":
- value = ""
- if current_key in fields:
- fields[current_key] = value
-
- header = header_match.group(1).strip()
- current_key = issue_header_map.get(
- header
- ) # Map to dict key or None if unknown
- current_value_lines = []
- else:
- current_value_lines.append(line)
-
- return fields
-
-
-def save_guideline_file(content: str, chapter: str):
- """
- Appends a guideline to a chapter
- """
- filename = f"src/coding-guidelines/{chapter.lower().replace(' ', '-')}.rst"
- with open(filename, "a", encoding="utf-8") as f:
- f.write(content)
- print(f"Saved guideline to {filename}")
-
-
-def guideline_template(fields: dict) -> str:
- """
- This function turns a dictionary that contains the guideline fields
- into a proper .rst guideline format
- """
-
- def get(key):
- return fields.get(key, "").strip()
-
- def format_code_block(code: str, lang: str = "rust") -> str:
- lines = code.strip().splitlines()
- if lines and lines[0].strip().startswith("```"):
- # Strip the ```rust and ``` lines
- lines = lines[1:]
- if lines and lines[-1].strip() == "```":
- lines = lines[:-1]
-
- # Dedent before adding indentation
- dedented_code = dedent("\n".join(lines))
-
- # Add required indentation
- indented_code = "\n".join(
- f" {line}" for line in dedented_code.splitlines()
- )
-
- return f"\n\n{indented_code}\n"
-
- amplification_text = indent(md_to_rst(get("amplification")), " " * 12)
- rationale_text = indent(md_to_rst(get("rationale")), " " * 16)
- non_compliant_ex_prose_text = indent(
- md_to_rst(get("non_compliant_ex_prose")), " " * 16
- )
- compliant_example_prose_text = indent(
- md_to_rst(get("compliant_example_prose")), " " * 16
- )
-
- guideline_text = guideline_rst_template(
- guideline_title=get("guideline_title"),
- category=get("category"),
- status=get("status"),
- release_begin=get("release_begin"),
- release_end=get("release_end"),
- fls_id=get("fls_id"),
- decidability=get("decidability"),
- scope=get("scope"),
- tags=get("tags"),
- amplification=amplification_text,
- rationale=rationale_text,
- non_compliant_ex_prose=non_compliant_ex_prose_text,
- non_compliant_ex=format_code_block(get("non_compliant_ex")),
- compliant_example_prose=compliant_example_prose_text,
- compliant_example=format_code_block(get("compliant_example")),
- )
-
- return guideline_text
+import argparse
+import json
+import sys
+from guideline_utils import (
+ extract_form_fields,
+ guideline_template,
+ normalize_list_separation,
+ normalize_md,
+ save_guideline_file,
+)
if __name__ == "__main__":
# parse arguments
diff --git a/scripts/generate-rst-comment.py b/scripts/generate-rst-comment.py
new file mode 100644
index 000000000..6c4210797
--- /dev/null
+++ b/scripts/generate-rst-comment.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT OR Apache-2.0
+# SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors
+
+"""
+This script generates a GitHub comment containing the RST preview of a coding guideline.
+It reads a GitHub issue JSON from stdin and outputs a formatted Markdown comment.
+
+Usage:
+ cat issue.json | uv run python scripts/generate-rst-comment.py
+ curl https://api.github.com/repos/.../issues/123 | uv run python scripts/generate-rst-comment.py
+
+Location: scripts/generate-rst-comment.py (new file)
+"""
+
+import json
+import sys
+
+from guideline_utils import (
+ chapter_to_filename,
+ extract_form_fields,
+ guideline_template,
+ normalize_list_separation,
+ normalize_md,
+)
+
+
+def generate_comment(rst_content: str, chapter: str) -> str:
+ """
+ Generate a formatted GitHub comment with instructions and RST content.
+
+ Args:
+ rst_content: The generated RST content for the guideline
+ chapter: The chapter name (e.g., "Concurrency", "Expressions")
+
+ Returns:
+ Formatted Markdown comment string
+ """
+ chapter_slug = chapter_to_filename(chapter)
+ target_file = f"src/coding-guidelines/{chapter_slug}.rst"
+
+ comment = f"""## 📋 RST Preview for Coding Guideline
+
+This is an automatically generated preview of your coding guideline in reStructuredText format.
+
+### 📁 Target File
+
+Add this guideline to: `{target_file}`
+
+### 📝 How to Use This
+
+1. **Fork the repository** (if you haven't already) and clone it locally
+2. **Create a new branch** from `main`:
+ ```bash
+ git checkout main
+ git pull origin main
+ git checkout -b guideline/your-descriptive-branch-name
+ ```
+3. **Open the target file** `{target_file}` in your editor
+4. **Copy the RST content** below and paste it at the end of the file (before any final directives if present)
+5. **Build locally** to verify the guideline renders correctly:
+ ```bash
+ ./make.py
+ ```
+6. **Commit and push** your changes:
+ ```bash
+ git add {target_file}
+ git commit -m "Add guideline: "
+ git push origin guideline/your-descriptive-branch-name
+ ```
+7. **Open a Pull Request** against `main`
+
+
+📄 Click to expand RST content
+
+```rst
+{rst_content}
+```
+
+
+
+---
+🤖 This comment was automatically generated from the issue content. It will be updated when you edit the issue body.
+
+
+"""
+ return comment
+
+
+def main():
+ # Read JSON from stdin
+ stdin_issue_json = sys.stdin.read()
+ json_issue = json.loads(stdin_issue_json)
+
+ issue_body = json_issue["body"]
+ issue_body = normalize_md(issue_body)
+ issue_body = normalize_list_separation(issue_body)
+
+ fields = extract_form_fields(issue_body)
+ chapter = fields["chapter"]
+
+ # Generate RST content
+ rst_content = guideline_template(fields)
+
+ # Generate the comment
+ comment = generate_comment(rst_content.strip(), chapter)
+
+ print(comment)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/guidelines_utils.py b/scripts/guidelines_utils.py
new file mode 100644
index 000000000..90cad97b2
--- /dev/null
+++ b/scripts/guidelines_utils.py
@@ -0,0 +1,234 @@
+# SPDX-License-Identifier: MIT OR Apache-2.0
+# SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors
+
+"""
+Shared utilities for parsing GitHub issues and generating RST guideline content.
+
+This module contains common functions used by:
+- auto-pr-helper.py (for automated PR generation)
+- generate-rst-comment.py (for generating preview comments)
+
+Location: scripts/guideline_utils.py
+"""
+
+import os
+import re
+import sys
+from textwrap import dedent, indent
+
+import pypandoc
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+parent_dir = os.path.abspath(os.path.join(script_dir, ".."))
+sys.path.append(parent_dir)
+
+from generate_guideline_templates import (
+ guideline_rst_template,
+ issue_header_map,
+)
+
+
+def md_to_rst(markdown: str) -> str:
+ """Convert Markdown text to reStructuredText using Pandoc."""
+ return pypandoc.convert_text(
+ markdown,
+ 'rst',
+ format='markdown',
+ extra_args=['--wrap=none']
+ )
+
+
+def normalize_list_separation(text: str) -> str:
+ """
+ Ensures every new list block is preceded by a blank line,
+ required for robust parsing by Pandoc when targeting RST
+ """
+ # Regex to identify any line that starts a Markdown list item (* or -)
+ _list_item_re = re.compile(r"^[ \t]*[*-][ \t]+")
+
+ output_buffer = []
+ for line in text.splitlines():
+ is_item = bool(_list_item_re.match(line))
+
+ # Get the last line appended to the output buffer
+ prev = output_buffer[-1] if output_buffer else ""
+
+ # Check if a blank line needs to be inserted before list
+ # (Current is item) AND (Prev is not blank) AND (Prev is not an item)
+ if is_item and prev.strip() and not _list_item_re.match(prev):
+ # Insert a blank line to clearly separate the new list block
+ output_buffer.append("")
+
+ output_buffer.append(line)
+
+ return "\n".join(output_buffer)
+
+
+def normalize_md(issue_body: str) -> str:
+ """
+ Fix links and mixed bold/code that confuse Markdown parser
+ """
+ # Fix links with inline-code: [`link`](url) => [link](url)
+ issue_body = re.sub(
+ r"\[\s*`([^`]+)`\s*\]\(([^)]+)\)",
+ r"[\1](\2)",
+ issue_body
+ )
+
+ # Fix mixed bold/code formatting
+ # **`code`** => `code`
+ issue_body = re.sub(
+ r"\*\*`([^`]+)`\*\*",
+ r"`\1`",
+ issue_body
+ )
+
+ # `**code**` => `code`
+ issue_body = re.sub(
+ r"`\*\*([^`]+)\*\*`",
+ r"`\1`",
+ issue_body
+ )
+
+ return issue_body
+
+
+def extract_form_fields(issue_body: str) -> dict:
+ """
+ Parse issue body (from GitHub issue template) into a dict of field values.
+
+ Args:
+ issue_body: The raw body text from a GitHub issue
+
+ Returns:
+ Dictionary with field names as keys and their values
+ """
+ fields = dict.fromkeys(issue_header_map.values(), "")
+
+ lines = issue_body.splitlines()
+ current_key = None
+ current_value_lines = []
+
+ lines.append("### END") # Sentinel to process last field
+
+ # Look for '###' in every line, ### represent a sections/field in a guideline
+ for line in lines:
+ header_match = re.match(r"^### (.+)$", line.strip())
+ if header_match:
+ # Save previous field value if any
+ if current_key is not None:
+ value = "\n".join(current_value_lines).strip()
+ # `_No response_` represents an empty field
+ if value == "_No response_":
+ value = ""
+ if current_key in fields:
+ fields[current_key] = value
+
+ header = header_match.group(1).strip()
+ current_key = issue_header_map.get(
+ header
+ ) # Map to dict key or None if unknown
+ current_value_lines = []
+ else:
+ current_value_lines.append(line)
+
+ return fields
+
+
+def format_code_block(code: str, lang: str = "rust") -> str:
+ """
+ Format a code block for RST output, stripping markdown fences if present.
+
+ Args:
+ code: The code content, possibly wrapped in markdown fences
+ lang: The language for syntax highlighting (default: rust)
+
+ Returns:
+ Formatted code block string with proper indentation
+ """
+ lines = code.strip().splitlines()
+ if lines and lines[0].strip().startswith("```"):
+ # Strip the ```rust and ``` lines
+ lines = lines[1:]
+ if lines and lines[-1].strip() == "```":
+ lines = lines[:-1]
+
+ # Dedent before adding indentation
+ dedented_code = dedent("\n".join(lines))
+
+ # Add required indentation
+ indented_code = "\n".join(
+ f" {line}" for line in dedented_code.splitlines()
+ )
+
+ return f"\n\n{indented_code}\n"
+
+
+def guideline_template(fields: dict) -> str:
+ """
+ Convert a dictionary of guideline fields into proper RST format.
+
+ Args:
+ fields: Dictionary containing all guideline fields
+
+ Returns:
+ Formatted RST string for the guideline
+ """
+ def get(key):
+ return fields.get(key, "").strip()
+
+ amplification_text = indent(md_to_rst(get("amplification")), " " * 12)
+ rationale_text = indent(md_to_rst(get("rationale")), " " * 16)
+ non_compliant_ex_prose_text = indent(
+ md_to_rst(get("non_compliant_ex_prose")), " " * 16
+ )
+ compliant_example_prose_text = indent(
+ md_to_rst(get("compliant_example_prose")), " " * 16
+ )
+
+ guideline_text = guideline_rst_template(
+ guideline_title=get("guideline_title"),
+ category=get("category"),
+ status=get("status"),
+ release_begin=get("release_begin"),
+ release_end=get("release_end"),
+ fls_id=get("fls_id"),
+ decidability=get("decidability"),
+ scope=get("scope"),
+ tags=get("tags"),
+ amplification=amplification_text,
+ rationale=rationale_text,
+ non_compliant_ex_prose=non_compliant_ex_prose_text,
+ non_compliant_ex=format_code_block(get("non_compliant_ex")),
+ compliant_example_prose=compliant_example_prose_text,
+ compliant_example=format_code_block(get("compliant_example")),
+ )
+
+ return guideline_text
+
+
+def chapter_to_filename(chapter: str) -> str:
+ """
+ Convert chapter name to filename slug.
+
+ Args:
+ chapter: Chapter name (e.g., "Associated Items", "Concurrency")
+
+ Returns:
+ Filename slug (e.g., "associated-items", "concurrency")
+ """
+ return chapter.lower().replace(" ", "-")
+
+
+def save_guideline_file(content: str, chapter: str):
+ """
+ Append a guideline to a chapter file.
+
+ Args:
+ content: The RST content to append
+ chapter: The chapter name
+ """
+ filename = f"src/coding-guidelines/{chapter_to_filename(chapter)}.rst"
+ with open(filename, "a", encoding="utf-8") as f:
+ f.write(content)
+ print(f"Saved guideline to {filename}")