The Foreman Documentation Contributors' Guide consolidates contribution guidelines for the Foreman Documentation project from the following sources:
-
CONTRIBUTING.md
-
Documentation AI Skills
-
Vale Rules for Foreman documentation
The guide concatenates resources from the above sources into a single guide for easy reference. When one of these resources changes, the guide is automatically updated.
1. About the Foreman Documentation Contributors' Guide
The Foreman Documentation Contributors' Guide consolidates contribution guidelines for the Foreman Documentation project from the following sources:
The guide concatenates resources from the above sources into a single guide for easy reference. When one of these resources changes, the guide is automatically updated. For more information, see the doc-Contributing/README.
Last updated: 2026-07-30
2. Contributing to Foreman Documentation
Foreman community welcomes all feedback, issues, pull requests (PRs), and reviews. Every contributor has different backgrounds, interests, and experiences with Foreman and its open source community, and they are all welcome to help improve and maintain Foreman documentation.
If you need help to get started, open an issue, ask the docs team on Matrix, or ping @docs on the Foreman Community Forum.
2.1. Contributor’s pledge
As a contributor, I will:
-
Familiarize myself with the Pull Request Checklist and Foreman documentation conventions guide.
-
Open additional issues if my contribution is incomplete.
-
Put in my best effort to ensure that my contribution does not worsen the state of any build target. For example, this might include reviewing how my changes affect upstream build targets even when working on behalf of a downstream product.
2.2. Maintainer’s pledge
As a maintainer, I will:
-
Help less experienced community members with git, Github, PRs, asciidoc, and asciidoctor and make friendly and helpful comments.
-
Only merge PRs if the Github Actions are green.
-
Try to review PRs in a timely manner.
-
Keep non-trivial PRs open for at least 24 hours (72 hours if over the weekend) to allow for input from the community. Examples of trivial PRs: Fixing a typo, fixing markup, or fixing links. Non-trivial PRs might not only benefit from additional review but they also represent an opportunity for community members to ask questions and learn.
2.3. Pull request checklists
Checklist for documentation contributions:
-
❏ My contribution is my own work and I agree to the license of the project. See LICENSE.
-
❏ My commits include meaningful commit messages. See seven rules for git commit messages.
-
❏ My change does not add trailing whitespaces. Some editors can help with this. For example, VS Code has multiple settings related to handling whitespaces.
Checklist for raising PRs:
-
❏ I re-read my work carefully before raising a PR or before marking a draft PR as ready for review. This can include using
git show, viewing the diff against master or the target branch, running a local Vale check, and other methods. -
❏ My PR description includes a meaningful description of the changes for the community.
-
❏ I fill out the cherry-picking list in the PR description to the best of my abilities to signify which versions my update applies to. If unsure, I let reviewers know so that they can assist.
-
❏ Before pinging others about my PR, I await the GitHub Actions checks to see if my branch builds. If a GitHub check fails and I’m unsure how to proceed, I let reviewers know so that they can assist.
Each PR should undergo tech review. (Tech review is performed by an Engineer who did not author the PR. It can be skipped if the PR does not significantly change description of product behavior.)
-
❏ The PR documents a recommended, user-friendly path.
-
❏ The PR removes steps that have been made unnecessary or obsolete.
-
❏ Any steps introduced or updated in the PR have been tested to confirm that they lead to the documented end result.
Each PR should undergo style review. (Style review is performed by a Technical Writer who did not author the PR.)
-
❏ The PR conforms with the team’s style guidelines.
-
❏ The PR introduces documentation that describes a user story rather than a product feature.
2.4. Additional resources
-
The AsciiDoc and RedHat style packages for the Vale linter. See Vale at Red Hat
-
Instructions for building and testing documentation locally. See guides/README.adoc
3. AI skills for contribution guidelines
The repository includes specialized skills (slash commands) for common documentation tasks. These can be invoked in AI-assisted editors like Claude Code or Cursor.
3.1. Validate against repository conventions
Overview
Foreman documentation follows specific conventions for code, attributes, conditionals, and file structure. These conventions ensure consistency across all guides and enable multi-product builds from a single source.
Code conventions:
-
Use UTF-8 character encoding in source files
-
Do not add trailing whitespace on lines or in files
-
Surround user input with underscores to indicate variable input (e.g.,
hammer organization create --name "My_Organization")
AsciiDoc attributes:
-
Use attributes instead of hardcoded product names:
{Project}instead ofForeman,{ProjectServer}instead ofSatellite Server,{SmartProxy}instead ofCapsule, etc. -
To use attributes in code blocks, add the
subs="+quotes,attributes"option -
Attributes are located in the files named
attributes*.adocinguides/common/.
Conditional content:
-
Use
ifdef::[]to show content only for specific builds. For example,ifdef::katello[]to show content only for the katello build target. -
Use
ifndef::[]to hide content for specific builds For example,ifndef::satellite[]to hide content for the satellite build target. -
Use comma for logic "or":
ifdef::katello,satellite[] -
Nest conditionals for logic "and".
File structure:
-
Follow the Modular documentation framework
-
Assemblies (
assembly_*.adoc): User stories at the top ofcommon/. Do not nest assemblies: An assembly never contains another assembly as an include. If you need to include an assembly in another assembly, include its modules directly and adjust the[leveloffset]value to show the nesting. -
Concept modules (
con_*.adoc): Explain what and why. Start with:_mod-docs-content-type: CONCEPT -
Procedure modules (
proc_*.adoc): Explain how .Start with:_mod-docs-content-type: PROCEDURE -
Reference modules (
ref_*.adoc): Tables, options, lists, and other referential material. Start with:_mod-docs-content-type: REFERENCE -
Snippets (
snip_*.adoc): Reusable text fragments without IDs -
When unsure what a particular file type should look like, copy an existing file of the same type and use it as a template.
AI Tool Instructions
Instructions
When invoked (manually or automatically after making changes), validate modified .adoc files in guides/ against the conventions described above.
Auto-fix the following:
-
UTF-8 character encoding
-
Trailing whitespace on lines and at end of files
-
Variable input formatting
-
Missing
subs="+quotes,attributes"in code blocks that use attributes -
File naming and prefixes (
con_,proc_,ref_,snip_) -
Content type attribute on first line
-
Vale linting issues
Report only if issues were discovered (do not auto-fix):
-
Nested assemblies
-
Conditional content usage
Report final validation step:
-
Build test results
4. AI skills for style guidelines
The repository includes specialized skills (slash commands) for common documentation tasks. These can be invoked in AI-assisted editors like Claude Code or Cursor.
4.1. Abstracts
Overview
An abstract is the first paragraph of a documentation module that helps readers and AI-powered search tools find the information they need and confirm they are in the right place.
An abstract follows after the module heading and is prefixed by the [role="_abstract"] AsciiDoc tag.
Abstracts typically contain:
-
The "What" - what the content covers
-
The "Why" - why it matters or when to use it
-
Where appropriate, an example use case
Guidelines:
-
Do not simply repeat the heading of the module; build upon it
-
Avoid self-referential language (e.g., "This procedure…", "This module…", "This table…")
-
Avoid feature-centric language (e.g., "This feature…")
-
Do not use sentence fragments
-
When addressing the user, address them as "you"
-
Follow these length constraints: 50-300 characters, 1-2 sentences, a single paragraph
Module-type-specific guidelines:
-
CONCEPTmodules: See references/concept.adoc -
PROCEDUREmodules: See references/procedure.adoc -
REFERENCEmodules: See references/reference.adoc
AI Tool Instructions
Instructions
Review and improve the abstract for this file. Write it if it doesn’t exist yet.
When reviewing or writing the abstract, follow the guidelines in the Overview section.
For module-type-specific abstract rules, look up the reference file that matches the :_mod-docs-content-type: AsciiDoc attribute in the module (open and apply that file in full).
4.2. Headings
Overview
Headings should be clear, concise, and use familiar keywords that help users understand what the content covers.
General principles for all headings:
-
Make the heading 3-11 words long
-
Use clear headings with familiar keywords for users
-
Ensure the heading summarizes the contents of the part of the documentation it introduces
Module-type-specific principles:
-
Concepts (con_*.adoc): Do not start with a gerund or verb. Use a noun phrase and include nouns that appear in the body text.
-
Procedures (proc_*.adoc): Start the heading with a gerund (e.g., "Configuring…", "Creating…").
-
References (ref_*.adoc): Do not start with a gerund or verb. Include nouns that appear in the body text.
-
Assemblies containing procedures: Start the heading with a gerund.
-
Assemblies containing only concepts or references: Do not start with a gerund or verb. Use a noun phrase.
Examples
Concept headings:
-
Provisioning methods in {Project}
-
Security considerations in {Project}
-
Host groups overview
Procedure headings:
-
Deploying SSH keys during provisioning
-
Opening required ports
-
Configuring pull-based transport for remote execution
Reference headings:
-
Job template examples and extensions
-
Host parameter hierarchy
-
Operating system requirements
Assembly headings:
-
Connecting AI applications to the MCP server for {Project}
-
Configuring {SmartProxy} and hosts to authenticate with SSH certificates during remote execution
-
Content and patch management with {Project}
AI Tool Instructions
Instructions
Review and improve the heading for this file. Write it if it doesn’t exist yet.
Follow the principles outlined in the Overview section above.
Post-command cleanup
If you rename a heading, use the /refactor-adoc command to update the anchor, and filename of the module and all references and links to the module in the repository.
4.3. User personas
Overview
Foreman documentation targets different user personas with varying responsibilities and permissions. Understanding the target persona helps ensure documentation addresses the user’s needs and capabilities appropriately.
User persona:
-
Has limited permissions
-
Runs regular Foreman operations
-
Example responsibilities: managing content, provisioning hosts, managing hosts
Admin persona:
-
Has unlimited permissions, including root access to the Foreman Server
-
Example responsibilities: managing Foreman server, configuring system settings
Architect persona:
-
Has no permissions, does not perform practical administrative or management tasks
-
Example responsibilities: evaluating whether Foreman/Katello meets organizational needs, planning deployment architecture
AI Tool Instructions
Instructions
When reviewing or writing documentation, identify the target persona and ensure:
-
The content matches the persona’s permission level and responsibilities
-
Technical depth is appropriate for the persona’s expertise
-
Prerequisites assume the persona’s typical environment
-
Examples and use cases reflect the persona’s real-world scenarios
If content targets multiple personas, report this for human review and suggest how to split it.
4.4. Prerequisites
Overview
Prerequisites are a bulleted list of conditions that must be satisfied before the user starts a procedure. Only procedure modules (proc_*.adoc) can include a .Prerequisites section.
What prerequisites are:
-
Checks that are true before the user begins
-
Conditions the user must have completed beforehand
-
Items the user must have ready
-
Actions that the user, another person, or technology has completed before the user can begin
Formatting guidelines:
-
The first word of each prerequisite must be capitalized
-
Each prerequisite must start with a bullet point
-
Prerequisites must not use imperative voice
-
If prerequisites are full sentences, end all with a period
-
If prerequisites are sentence fragments, do not use any end punctuation
-
Use passive voice for prerequisites representing an action not completed by the current user
-
Prerequisites must use parallel language (all sentences or all fragments, not mixed)
Content guidelines:
-
Focus on relevant prerequisites that users might not otherwise be aware of
-
Do not list obvious prerequisites
-
Do not include procedure steps in prerequisites
-
Do not exceed 10 prerequisites
Examples
Good prerequisites:
-
The
kernelcarepackage is installed on your hosts. -
The base system of the {SmartProxy} is registered to the newly upgraded {ProjectServer}.
-
Your user account has a role that grants the
view_policiespermission. -
You are logged in to the registry.redhat.io container registry.
-
If you use
dzdofor Ansible jobs, thecommunity.generalAnsible collection must be installed.
Bad prerequisites (and why):
-
You are logged in. (Obvious prerequisite)
-
The host is registered to {Project}. (Obvious prerequisite)
-
At least one host exists in {Project}. (Obvious prerequisite)
-
Ensure the
kernelcarepackage is installed on your hosts. (Uses imperative voice) -
Install the
kernelcarepackage on your hosts. (Phrased like a procedure step)
AI Tool Instructions
Instructions
-
Only process procedure modules (proc_*.adoc). These are the only modules that can include a
.Prerequisitessection. -
If a section titled
.Prerequisitesexists in the file, ensure it uses consistent formatting as described in the Overview. -
If a section titled
.Prerequisitesexists in the file, ensure it contains information suitable for prerequisites. -
If a procedure module does not include a
.Prerequisitessection, scan the module to identify steps that meet criteria for prerequisites. If such steps exist, create a.Prerequisitessection and rephrase the steps as prerequisites.
5. AI skills for structure
The repository includes specialized skills (slash commands) for common documentation tasks. These can be invoked in AI-assisted editors like Claude Code or Cursor.
5.1. An assembly covers a single user story
Overview
An assembly must cover a single user story - what the user wants to accomplish. When an assembly attempts to describe multiple user stories, it often loses focus, becomes too long, and can be confusing to follow.
Usage:
/review-assembly-user-story @assembly_example.adoc
The one-user-story principle:
-
Each assembly should answer "What does the user want to accomplish?"
-
All modules in an assembly should support the same goal
-
Think from the user’s perspective, not the product’s features
When an assembly needs splitting:
-
It contains procedures for multiple independent tasks that users might want to do separately
-
The concept module describes multiple distinct use cases or workflows
-
Different sections serve different user goals
-
Modules could be logically grouped into two or more cohesive assemblies
When an assembly is fine:
-
All procedures work toward a single user goal
-
Procedures are sequential steps or alternative approaches for the same task
-
Modules provide context, reference, or examples for one user story
Important considerations:
-
Inverse operations belong together - Creating and deleting, or enabling and disabling, are part of the same user story
-
Alternative methods belong together - Multiple procedures for the same task (web UI, CLI, API) belong in the same assembly
-
Sequential workflows belong together - If steps must be done in order for one goal, they belong in the same assembly
Examples
Good examples (single user story):
-
assembly_backing-up-server-and-proxy.adoc- User wants to back up their system -
assembly_configuring-email-notifications.adoc- User wants to set up email notifications -
assembly_managing-organizations.adoc- User wants to create and delete organizations
AI Tool Instructions
Instructions
Review the assembly file to determine if it follows the one-user-story principle. Each assembly must describe a single user story.
Step 1: Analyze the assembly
Read the assembly file and:
-
Identify the primary user story - What is the main goal or task the user wants to accomplish?
-
Check the introductory concept module (the first include in the assembly) - Does it describe a single focused topic?
-
Review included modules - Do they all support the same user story, or do they describe separate independent tasks?
-
Look for clear boundaries - Are there sections that could stand alone as separate user stories?
Step 2: Determine if splitting is needed
The assembly needs splitting if:
-
It contains procedures for multiple independent tasks that users might want to do separately
-
The concept module describes multiple distinct use cases or workflows
-
Different sections serve different user goals (e.g., "creating/deleting" vs "configuring" vs "monitoring")
-
Modules could be logically grouped into 2 or more cohesive assemblies
|
Note
|
Inverse operations (creating/deleting, enabling/disabling) belong together in the same assembly. |
The assembly is fine if:
-
All procedures work toward a single user goal
-
Procedures are sequential steps or alternative approaches for the same task
-
Modules provide context, reference, or examples for one user story
Step 3: If splitting is needed
For each identified user story:
-
Create a new assembly file - Name it
assembly_<descriptive-name>.adoc -
Include relevant modules - Copy the appropriate module includes for this user story
-
Create or update the introductory concept module - Write a focused concept module with an abstract for this specific user story if needed
-
Update parent files - Replace the original assembly include with includes for all new assemblies in the
master.adocfile -
Delete the original assembly - Once all content is split and includes are updated
Step 4: Report findings
Provide a summary that includes:
-
Whether the assembly follows the one-user-story principle
-
If splitting occurred, list the new assembly files created and their user stories
-
Any recommendations for improving the assembly structure
Principles
-
One user story per assembly - Each assembly should answer "What does the user want to accomplish?"
-
User-centric organization - Think from the user’s perspective, not the product’s features
-
Cohesive modules - All modules in an assembly should support the same goal
6. AI skills for file management
The repository includes specialized skills (slash commands) for common documentation tasks. These can be invoked in AI-assisted editors like Claude Code or Cursor.
6.1. Rename a module
Overview
This command changes the title, ID, and filename of an assembly, module, or snippet according to a new title, and updates all includes and ID references across the documentation.
Usage:
/refactor-adoc @old-file.adoc "New title"
What it does:
-
Renames the file with the appropriate prefix (e.g.,
proc_new-title.adoc) -
Updates the ID if present (e.g.,
[id="new-title"]) -
Updates the title if present (e.g.,
= New title) -
Updates all
include::directives that reference this file -
Updates all cross-references to the old ID throughout the documentation
When to use:
-
When you need to rename a documentation file to better match its content
-
After significantly revising the content of a module
-
When improving heading clarity requires filename changes
AI Tool Instructions
Instructions
Refactor the following .adoc file according to the following title.
Follow these principles:
-
Rename the file as
prefix_new-title.adoc. -
Change the ID of the file as
[id="new-title"]if the file contains an ID. -
Change the title of the file as
= New titleif the file contains a title. -
Update all includes of that file in all other
.adocfiles. -
If the original file contains an ID, update all occurrences of the ID in all other
.adocfiles. If the old ID contains_{context}, the tail of the occurrence after_is a variable string.
7. Foreman-documentation custom Vale rules
The following custom Vale rules enforce documentation standards specific to Foreman. Run the Vale linter locally or in CI to check AsciiDoc files against these rules.
7.1. AbstractLength
Keep abstract paragraphs between 50 and 300 characters. Abstracts shorter than 50 characters lack sufficient context, while those longer than 300 characters become difficult to scan quickly.
Rule Definition
---
extends: script
message: "Abstract must be between 50 and 300 characters. Place any information that exceeds 300 characters in a new paragraph or paragraphs. It cannot be part of the abstract."
level: error
scope: raw
script: |
// Import required Tengo modules
text := import("text") // Text manipulation functions
fmt := import("fmt") // String formatting functions
// Initialize empty array to store rule violations
matches := []
// Compile regex to find the abstract paragraph in AsciiDoc
// Each module contains exactly one abstract (a single paragraph following [role="_abstract"])
// Pattern breakdown:
// \[role="_abstract"\] - Matches the abstract role marker
// \s*\n - Matches optional whitespace and newline after marker
// ([\s\S]+?) - Captures the abstract paragraph (non-greedy, including newlines)
// \n\n - Matches double newline (blank line) that ends paragraph
// Note: Using [\s\S] instead of . to match across multiple lines
abstract_regex := text.re_compile(`\[role="_abstract"\]\s*\n([\s\S]+?)\n\n`)
// Find the abstract (there's only one per module)
// scope = the full document text being checked
// 1 = find only first match (since there's only one abstract per module)
result := abstract_regex.find(scope, 1)
// Check if an abstract was found (find returns undefined if no match)
if result != undefined && len(result) > 0 && len(result[0]) > 1 {
// result[0][0] = full match including [role="_abstract"] marker
// result[0][1] = captured group (just the abstract paragraph text)
abstract_text := result[0][1].text
// Count characters in the abstract
char_count := len(abstract_text)
// Check if character count is outside valid range (50-300)
if char_count < 50 || char_count > 300 {
// Build custom error message with actual character count
msg := fmt.sprintf("Abstract must be between 50 and 300 characters (currently %d characters).", char_count)
// Add this violation to the matches array
matches = append(matches, {
begin: result[0][1].begin, // Start position of abstract paragraph
end: result[0][1].end, // End position of abstract paragraph
message: msg // Custom error message with character count
})
}
}
7.2. AvoidBrandedTerminology
Do not use branded terms in documentation. Use AsciiDoc attributes like {Project}, {ProjectServer}, and {SmartProxy} instead of product-specific names like "Satellite", "orcharhino", or "Capsule". This ensures the documentation works for all build targets.
Rule Definition
---
extends: substitution
level: error
message: "Use attribute '%s' instead of a branded term."
action:
name: replace
swap:
"(?:orcharhino|Satellite) Server": "{ProjectServer}"
"(?:orcharhino Proxy|Capsule)": "{SmartProxy}"
"(?:orcharhino Proxy|Capsule) Server": "{SmartProxyServer}"
"Red Hat Satellite": "{ProjectName}"
"(?:orcharhino|Satellite)": "{Project}"
7.3. Capitalization
Use proper capitalization for product names and technical terms. For example, write "Ansible" not "ansible", "PostgreSQL" not "postgresql", and "lifecycle environment" not "Lifecycle Environment". Different capitalization may be required in specific contexts like Web UI buttons, settings, or templates.
Rule Definition
---
extends: substitution
ignorecase: false
level: error
message: "Use '%s' instead of '%s' unless you are using the term in a context that requires different capitalization. For example, different capitalization might be required in Web UI buttons, settings, or templates."
action:
name: replace
swap:
ansible: Ansible
candlepin: candlepin
chef: Chef
discovery: Discovery
enterprise linux: Enterprise Linux
hammer: Hammer
katello: Katello
library: Library
openscap: OpenSCAP
postgresql: PostgreSQL
pulp: Pulp
python: Python
salt: Salt
salt minions: Salt Minions
secure boot: Secure Boot
subscription manager: Subscription Manager
tracer: Tracer
Lifecycle Environment: lifecycle environment
'(?<!Pulp )Manifest': manifest
Pulp manifest: Pulp Manifest
Remote Execution: remote execution
'(?<=\s)Subscription(?! Manager)': subscription
Red Hat network: Red{nbsp}Hat Network
7.4. ConceptAttributeLink
Do not include links or cross references in the main body of a concept module. In concept modules, links are allowed only in the Additional resources section. Based on ConceptLink.yml from https://github.com/jhradilek/asciidoctor-dita-vale.
Rule Definition
---
extends: script
message: "To improve content usability and user focus, links and cross references in the main body of a concept module are discouraged. Move them to the Additional resources section."
level: warning
scope: raw
script: |
text := import("text")
matches := []
r_add_resources := text.re_compile("^(?:={2,}[ \\t]+|\\.{1,2})Additional resources[ \\t]*$")
r_any_title := text.re_compile("^(?:={2,}[ \\t]+|\\.{1,2})[^ \\t.].*$")
r_attr_link := text.re_compile("\\{[0-9A-Za-z_][0-9A-Za-z_-]*URL\\}[^\\[\\n]*\\[[^\\]]*\\]")
r_code_block := text.re_compile("^(?:\\.{4,}|-{4,})[ \\t]*$")
r_comment_block := text.re_compile("^/{4,}\\s*$")
r_comment_line := text.re_compile("^(//|//[^/].*)$")
r_content_type := text.re_compile("^:_(?:mod-docs-content|content|module)-type:[ \\t]+(?i:assembly|concept)")
document := text.split(text.trim_suffix(scope, "\n"), "\n")
is_relevant := false
in_code_block := false
in_comment_block := false
in_add_resources := false
start := 0
end := 0
for line in document {
start += end
end = len(line) + 1
if r_comment_block.match(line) {
delimiter := text.trim_space(line)
if ! in_comment_block {
in_comment_block = delimiter
} else if in_comment_block == delimiter {
in_comment_block = false
}
continue
}
if in_comment_block { continue }
if r_comment_line.match(line) { continue }
if r_code_block.match(line) {
delimiter := text.trim_space(line)
if ! in_code_block {
in_code_block = delimiter
} else if in_code_block == delimiter {
in_code_block = false
}
continue
}
if in_code_block { continue }
if r_content_type.match(line) {
is_relevant = true
continue
}
if r_add_resources.match(line) {
in_add_resources = true
continue
}
if r_any_title.match(line) {
in_add_resources = false
continue
}
if in_add_resources { continue }
for i, entry in r_attr_link.find(line, -1) {
link := entry[0]
matches = append(matches, {begin: start + link.begin, end: start + link.end - 1})
}
}
if ! is_relevant {
matches = []
}
7.5. HeadingWordCount
Check that module headings have between 3 and 11 words. A module has a single heading (the line starting with =). Note: AsciiDoc attributes like {Project} count as one word each.
Rule Definition
---
extends: script
message: "Heading must have between 3 and 11 words."
level: error
scope: raw
script: |
// Import required Tengo modules
text := import("text") // Text manipulation functions
fmt := import("fmt") // String formatting functions
// Initialize empty array to store rule violations
matches := []
// Compile regex to find the module heading
// Each module contains exactly one heading (line starting with = )
// Pattern breakdown:
// (?m) - Multi-line mode (^ and $ match line boundaries, not just string boundaries)
// ^=[ \t]+ - Matches line starting with = followed by whitespace
// (.+)$ - Captures the heading text to end of line
heading_regex := text.re_compile(`(?m)^=[ ]+(.+)$`)
// Find the heading (there's only one per module)
// scope = the full document text being checked
// 1 = find only first match (since there's only one heading per module)
result := heading_regex.find(scope, 1)
// Check if a heading was found (find returns undefined if no match)
if result != undefined && len(result) > 0 && len(result[0]) > 1 {
// result[0][1] = captured group (just the heading text)
heading_text := result[0][1].text
// Count words including AsciiDoc attributes as words
// Match either {AttributeName} or regular words
word_regex := text.re_compile(`\{[A-Za-z][A-Za-z0-9_-]*\}|\b\w+\b`)
words := word_regex.find(heading_text, -1)
// Get word count (0 if no words found)
word_count := 0
if words != undefined {
word_count = len(words)
}
// Check if word count is outside valid range (3-11)
if word_count < 3 || word_count > 11 {
msg := fmt.sprintf("Heading must have between 3 and 11 words (currently %d words).", word_count)
matches = append(matches, {
begin: result[0][1].begin,
end: result[0][1].end,
message: msg
})
}
}
7.6. OneSentencePerLine
Write one sentence per line. This makes diffs easier to read, simplifies reviewing changes, and helps track modifications to individual sentences. End each sentence with proper punctuation (period, question mark, or exclamation point) before starting a new line.
Rule Definition
---
extends: existence
message: "Write one sentence per line."
level: error
scope: raw
nonword: true
tokens:
# Match: word/)/]/quote + sentence-end punctuation + space/tab + capital letter/[
# Using [ \t] instead of \s to match only horizontal whitespace, not newlines
- '[a-z0-9\)\]"][.!?][ \t]+[A-Z\[]'
7.7. ProcedureStepLimit
Limit procedures to a maximum of 10 steps. Procedures with too many steps are difficult to follow.
Rule Definition
---
extends: script
message: "Procedure contains more than 10 steps. Split it into multiple procedures or leverage substeps to group related steps. If the procedure contains conditionals, review the preview to ensure all built targets comply with the limit of 10 steps."
level: error
scope: raw
script: |
text := import("text")
matches := []
lines := text.split(scope, "\n")
count := 0
step_positions := []
position := 0
in_procedure := false
for line in lines {
// Detect .Procedure heading (only one per file)
if text.re_match("^\\.Procedure", line) {
in_procedure = true
}
// Reset when we encounter another heading at the same level
if text.re_match("^\\.[A-Z]", line) && !text.re_match("^\\.Procedure", line) {
in_procedure = false
}
// Count steps only when inside a .Procedure section
if in_procedure && text.re_match("^\\. [^\\.]", line) {
count += 1
step_positions = append(step_positions, position)
}
position += len(line) + 1
}
if count > 10 {
// Report error on ALL steps to ensure it appears in GitHub Actions diff context
// when ANY step is modified in a procedure that exceeds the 10-step limit
for i := 0; i < count; i++ {
step_pos := step_positions[i]
matches = append(matches, {begin: step_pos, end: step_pos + 1})
}
}