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

jamesfredley pushed a commit to branch docs/configuration-hybrid
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 58327298862d376ee69b27c21d646d4dc041da36
Author: James Fredley <[email protected]>
AuthorDate: Fri Feb 20 19:28:00 2026 -0500

    feat: hybrid ConfigReportCommand with static metadata and runtime values
    
    Merge curated property metadata from the Application Properties
    documentation with runtime-collected values from the Spring Environment.
    
    The command now outputs a 3-column AsciiDoc table (Property, Description,
    Default) organized by functional category (Core Properties, Web &
    Controllers, CORS, GORM, etc.). Runtime values override static defaults
    for known properties. Properties not found in the metadata appear in a
    separate Other Properties section.
    
    - Add config-properties.yml with ~160 properties across 21 categories
    - Modify ConfigReportCommand to load YAML metadata and merge with runtime
    - Update unit tests for 3-column hybrid format
    - Update integration tests for hybrid category-based layout
    
    Assisted-by: Claude Code <[email protected]>
---
 .../grails/dev/commands/ConfigReportCommand.groovy | 130 +++-
 .../META-INF/grails/config-properties.yml          | 799 +++++++++++++++++++++
 .../dev/commands/ConfigReportCommandSpec.groovy    | 149 +++-
 .../ConfigReportCommandIntegrationSpec.groovy      | 111 +--
 4 files changed, 1099 insertions(+), 90 deletions(-)

diff --git 
a/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy 
b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy
index e10da60382..67e3aaf316 100644
--- a/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy
+++ b/grails-core/src/main/groovy/grails/dev/commands/ConfigReportCommand.groovy
@@ -21,6 +21,8 @@ package grails.dev.commands
 import groovy.transform.CompileStatic
 import groovy.util.logging.Slf4j
 
+import org.yaml.snakeyaml.Yaml
+
 import org.springframework.core.env.ConfigurableEnvironment
 import org.springframework.core.env.EnumerablePropertySource
 import org.springframework.core.env.PropertySource
@@ -108,7 +110,26 @@ class ConfigReportCommand implements ApplicationCommand {
      * @param sorted the sorted configuration properties
      * @param reportFile the file to write the report to
      */
-    void writeReport(Map<String, String> sorted, File reportFile) {
+    void writeReport(Map<String, String> runtimeProperties, File reportFile) {
+        Map<String, Map<String, String>> metadata = loadPropertyMetadata()
+        Map<String, List<Map<String, String>>> categories = new 
LinkedHashMap<String, List<Map<String, String>>>()
+        for (Map.Entry<String, Map<String, String>> entry : 
metadata.entrySet()) {
+            Map<String, String> property = entry.value
+            String category = property.get('category')
+            if (!categories.containsKey(category)) {
+                categories.put(category, new ArrayList<Map<String, String>>())
+            }
+            categories.get(category).add(property)
+        }
+
+        Set<String> knownKeys = metadata.keySet()
+        Map<String, String> otherProperties = new TreeMap<String, String>()
+        runtimeProperties.each { String key, String value ->
+            if (!knownKeys.contains(key)) {
+                otherProperties.put(key, value)
+            }
+        }
+
         reportFile.withWriter('UTF-8') { BufferedWriter writer ->
             writer.writeLine('= Grails Application Configuration Report')
             writer.writeLine(':toc: left')
@@ -116,32 +137,107 @@ class ConfigReportCommand implements ApplicationCommand {
             writer.writeLine(':source-highlighter: coderay')
             writer.writeLine('')
 
-            String currentSection = ''
-            sorted.each { String key, String value ->
-                String section = key.contains('.') ? key.substring(0, 
key.indexOf('.')) : key
-                if (section != currentSection) {
-                    if (currentSection) {
-                        writer.writeLine('|===')
-                        writer.writeLine('')
+            categories.each { String categoryName, List<Map<String, String>> 
categoryProperties ->
+                writer.writeLine("== ${categoryName}")
+                writer.writeLine('')
+                writer.writeLine('[cols="2,5,2", options="header"]')
+                writer.writeLine('|===')
+                writer.writeLine('| Property | Description | Default')
+                writer.writeLine('')
+
+                categoryProperties.each { Map<String, String> property ->
+                    String key = property.get('key')
+                    String description = property.get('description')
+                    String defaultValue = property.get('default')
+                    String resolvedValue
+                    if (runtimeProperties.containsKey(key)) {
+                        resolvedValue = 
"`${escapeAsciidoc(runtimeProperties.get(key))}`"
                     }
-                    currentSection = section
-                    writer.writeLine("== ${section}")
-                    writer.writeLine('')
-                    writer.writeLine('[cols="2,3", options="header"]')
-                    writer.writeLine('|===')
-                    writer.writeLine('| Property | Value')
+                    else {
+                        resolvedValue = escapeAsciidoc(defaultValue)
+                    }
+                    writer.writeLine("| `${key}`")
+                    writer.writeLine("| ${escapeAsciidoc(description)}")
+                    writer.writeLine("| ${resolvedValue}")
                     writer.writeLine('')
                 }
-                writer.writeLine("| `${key}`")
-                writer.writeLine("| `${escapeAsciidoc(value)}`")
+                writer.writeLine('|===')
                 writer.writeLine('')
             }
-            if (currentSection) {
+
+            if (!otherProperties.isEmpty()) {
+                writer.writeLine('== Other Properties')
+                writer.writeLine('')
+                writer.writeLine('[cols="2,3", options="header"]')
+                writer.writeLine('|===')
+                writer.writeLine('| Property | Default')
+                writer.writeLine('')
+                otherProperties.each { String key, String value ->
+                    writer.writeLine("| `${key}`")
+                    writer.writeLine("| `${escapeAsciidoc(value)}`")
+                    writer.writeLine('')
+                }
                 writer.writeLine('|===')
             }
         }
     }
 
+    Map<String, Map<String, String>> loadPropertyMetadata() {
+        InputStream stream = 
ConfigReportCommand.classLoader.getResourceAsStream('META-INF/grails/config-properties.yml')
+        if (stream == null) {
+            return new LinkedHashMap<String, Map<String, String>>()
+        }
+        Map<String, Map<String, String>> metadata = new LinkedHashMap<String, 
Map<String, String>>()
+        Map<String, Object> yamlData
+        try {
+            yamlData = (Map<String, Object>) new Yaml().load(stream)
+        }
+        finally {
+            stream.close()
+        }
+        Object categories = yamlData.get('categories')
+        if (!(categories instanceof List)) {
+            return metadata
+        }
+        for (Object categoryObject : (List<Object>) categories) {
+            if (!(categoryObject instanceof Map)) {
+                continue
+            }
+            Map<String, Object> categoryMap = (Map<String, Object>) 
categoryObject
+            Object categoryNameObject = categoryMap.get('name')
+            if (!(categoryNameObject instanceof String)) {
+                continue
+            }
+            String categoryName = (String) categoryNameObject
+            Object properties = categoryMap.get('properties')
+            if (!(properties instanceof List)) {
+                continue
+            }
+            for (Object propertyObject : (List<Object>) properties) {
+                if (!(propertyObject instanceof Map)) {
+                    continue
+                }
+                Map<String, Object> propertyMap = (Map<String, Object>) 
propertyObject
+                Object keyObject = propertyMap.get('key')
+                Object descriptionObject = propertyMap.get('description')
+                Object defaultObject = propertyMap.get('default')
+                if (!(keyObject instanceof String)) {
+                    continue
+                }
+                String key = (String) keyObject
+                String description = descriptionObject instanceof String ? 
(String) descriptionObject : ''
+                String defaultValue = defaultObject instanceof String ? 
(String) defaultObject : ''
+                Map<String, String> entry = new LinkedHashMap<String, String>()
+                entry.put('key', key)
+                entry.put('description', description)
+                entry.put('default', defaultValue)
+                entry.put('category', categoryName)
+                metadata.put(key, entry)
+            }
+        }
+        metadata
+    }
+
     /**
      * Escapes special AsciiDoc characters in a value string.
      *
diff --git 
a/grails-core/src/main/resources/META-INF/grails/config-properties.yml 
b/grails-core/src/main/resources/META-INF/grails/config-properties.yml
new file mode 100644
index 0000000000..f82c1b38e4
--- /dev/null
+++ b/grails-core/src/main/resources/META-INF/grails/config-properties.yml
@@ -0,0 +1,799 @@
+categories:
+  - name: "Core Properties"
+    properties:
+      - key: "grails.profile"
+        description: "The active Grails application profile (e.g., web, 
rest-api, plugin)."
+        default: "Set by project template"
+      - key: "grails.codegen.defaultPackage"
+        description: "The default package used when generating artefacts with 
grails create-* commands."
+        default: "Set by project template"
+      - key: "grails.serverURL"
+        description: "The server URL used to generate absolute links (e.g., 
https://my.app.com) and used by redirects."
+        default: "_(derived from request)_"
+      - key: "grails.enable.native2ascii"
+        description: "Whether to perform native2ascii conversion of i18n 
properties files."
+        default: "`true`"
+      - key: "grails.bootstrap.skip"
+        description: "Whether to skip execution of BootStrap.groovy classes on 
startup."
+        default: "`false`"
+      - key: "grails.spring.bean.packages"
+        description: "List of packages to scan for Spring beans."
+        default: "`[]`"
+      - key: "grails.spring.disable.aspectj.autoweaving"
+        description: "Whether to disable AspectJ auto-weaving."
+        default: "`false`"
+      - key: "grails.spring.placeholder.prefix"
+        description: "The prefix for property placeholder resolution."
+        default: "`${`"
+      - key: "grails.spring.transactionManagement.proxies"
+        description: "Whether to enable Spring proxy-based transaction 
management since @Transactional uses an AST transform and proxies are typically 
redundant."
+        default: "`false`"
+      - key: "grails.plugin.includes"
+        description: "List of plugin names to include in the plugin manager 
(all others excluded)."
+        default: "`[]` _(all plugins)_"
+      - key: "grails.plugin.excludes"
+        description: "List of plugin names to exclude from the plugin manager."
+        default: "`[]`"
+  - name: "Web & Controllers"
+    properties:
+      - key: "grails.controllers.defaultScope"
+        description: "The default scope for controllers (singleton, prototype, 
session)."
+        default: "`singleton`"
+      - key: "grails.controllers.upload.location"
+        description: "The directory for temporary file uploads."
+        default: "`System.getProperty('java.io.tmpdir')`"
+      - key: "grails.controllers.upload.maxFileSize"
+        description: "Maximum file size for uploads (in bytes)."
+        default: "`1048576` (1 MB)"
+      - key: "grails.controllers.upload.maxRequestSize"
+        description: "Maximum request size for multipart uploads (in bytes)."
+        default: "`10485760` (10 MB)"
+      - key: "grails.controllers.upload.fileSizeThreshold"
+        description: "File size threshold (in bytes) above which uploads are 
written to disk."
+        default: "`0`"
+      - key: "grails.web.url.converter"
+        description: "The URL token converter strategy, use hyphenated for 
hyphen-separated URLs."
+        default: "`camelCase`"
+      - key: "grails.web.linkGenerator.useCache"
+        description: "Whether to cache links generated by the link generator."
+        default: "`true`"
+      - key: "grails.web.servlet.path"
+        description: "The path the Grails dispatcher servlet is mapped to."
+        default: "`/+++*+++`"
+      - key: "grails.filter.encoding"
+        description: "The character encoding for the Grails character encoding 
filter."
+        default: "`UTF-8`"
+      - key: "grails.filter.forceEncoding"
+        description: "Whether to force the encoding filter to set the encoding 
on the response."
+        default: "`true`"
+      - key: "grails.exceptionresolver.logRequestParameters"
+        description: "Whether to log request parameters in exception stack 
traces."
+        default: "`true`"
+      - key: "grails.exceptionresolver.params.exclude"
+        description: "List of parameter names to mask (replace with [*****]) 
in exception stack traces, typically used for password and creditCard."
+        default: "`[]`"
+      - key: "grails.logging.stackTraceFiltererClass"
+        description: "Fully qualified class name of a custom 
StackTraceFilterer implementation."
+        default: "`org.grails.exceptions.reporting.DefaultStackTraceFilterer`"
+  - name: "CORS"
+    properties:
+      - key: "grails.cors.enabled"
+        description: "Whether CORS support is enabled."
+        default: "`false`"
+      - key: "grails.cors.filter"
+        description: "Whether CORS is handled via a servlet filter (true) or 
an interceptor (false)."
+        default: "`true`"
+      - key: "grails.cors.allowedOrigins"
+        description: "List of allowed origins (e.g., http://localhost:5000), 
only applies when grails.cors.enabled is true."
+        default: "`['*']`"
+      - key: "grails.cors.allowedMethods"
+        description: "List of allowed HTTP methods."
+        default: "`['*']`"
+      - key: "grails.cors.allowedHeaders"
+        description: "List of allowed request headers."
+        default: "`['*']`"
+      - key: "grails.cors.exposedHeaders"
+        description: "List of response headers to expose to the client."
+        default: "`[]`"
+      - key: "grails.cors.maxAge"
+        description: "How long (in seconds) the preflight response can be 
cached."
+        default: "`1800`"
+      - key: "grails.cors.allowCredentials"
+        description: "Whether credentials (cookies, authorization headers) are 
supported."
+        default: "`false`"
+      - key: "grails.cors.mappings"
+        description: "Map of URL patterns to per-path CORS configuration where 
defining any mapping disables the global /** mapping."
+        default: "`{}` _(global `/**` mapping)_"
+  - name: "Views & GSP"
+    properties:
+      - key: "grails.views.default.codec"
+        description: "The default encoding codec for GSP output where html 
reduces XSS risk and options are none, html, base64."
+        default: "`none`"
+      - key: "grails.views.gsp.encoding"
+        description: "The file encoding for GSP source files."
+        default: "`UTF-8`"
+      - key: "grails.views.gsp.htmlcodec"
+        description: "The HTML codec for GSP output (xml or html)."
+        default: "`xml`"
+      - key: "grails.views.gsp.codecs.expression"
+        description: "The codec applied to GSP ${} expressions."
+        default: "`html`"
+      - key: "grails.views.gsp.codecs.scriptlet"
+        description: "The codec applied to GSP <% %> scriptlet output."
+        default: "`html`"
+      - key: "grails.views.gsp.codecs.taglib"
+        description: "The codec applied to tag library output."
+        default: "`none`"
+      - key: "grails.views.gsp.codecs.staticparts"
+        description: "The codec applied to static HTML parts of GSP pages."
+        default: "`none`"
+      - key: "grails.views.gsp.layout.preprocess"
+        description: "Whether GSP layout preprocessing is enabled, where 
disabling allows Grails to parse rendered HTML but slows rendering."
+        default: "`true`"
+      - key: "grails.views.enable.jsessionid"
+        description: "Whether to include the jsessionid in rendered links."
+        default: "`false`"
+      - key: "grails.views.filteringCodecForContentType"
+        description: "Map of content types to encoding codecs."
+        default: "`{}`"
+      - key: "grails.gsp.disable.caching.resources"
+        description: "Whether to disable GSP resource caching."
+        default: "`false`"
+      - key: "grails.gsp.enable.reload"
+        description: "Whether to enable GSP reloading in production."
+        default: "`false`"
+      - key: "grails.gsp.view.dir"
+        description: "Custom directory for GSP view resolution."
+        default: "`grails-app/views`"
+  - name: "Content Negotiation & MIME Types"
+    properties:
+      - key: "grails.mime.types"
+        description: "Map of MIME type names to content type strings used for 
content negotiation."
+        default: "_(see web profile `application.yml`)_"
+      - key: "grails.mime.file.extensions"
+        description: "Whether to use the file extension to determine the MIME 
type in content negotiation."
+        default: "`true`"
+      - key: "grails.mime.use.accept.header"
+        description: "Whether to use the Accept header for content 
negotiation."
+        default: "`true`"
+      - key: "grails.mime.disable.accept.header.userAgents"
+        description: "List of user agent substrings (e.g., Gecko, WebKit) for 
which Accept header processing is disabled."
+        default: "`[]`"
+      - key: "grails.mime.disable.accept.header.userAgentsXhr"
+        description: "When true, XHR requests also respect the 
grails.mime.disable.accept.header.userAgents setting, while by default XHR 
requests ignore user agent filtering."
+        default: "`false`"
+      - key: "grails.converters.encoding"
+        description: "The character encoding for converter output (JSON or 
XML)."
+        default: "`UTF-8`"
+  - name: "Data Binding"
+    properties:
+      - key: "grails.databinding.trimStrings"
+        description: "Whether to trim whitespace from String values during 
data binding."
+        default: "`true`"
+      - key: "grails.databinding.convertEmptyStringsToNull"
+        description: "Whether empty String values are converted to null during 
data binding."
+        default: "`true`"
+      - key: "grails.databinding.autoGrowCollectionLimit"
+        description: "The maximum size to which indexed collections can 
auto-grow during data binding."
+        default: "`256`"
+      - key: "grails.databinding.dateFormats"
+        description: "List of date format strings used to parse date values 
during data binding."
+        default: "`[\"yyyy-MM-dd HH:mm:ss.S\", \"yyyy-MM-dd'T'HH:mm:ss'Z'\", 
\"yyyy-MM-dd HH:mm:ss.S z\", \"yyyy-MM-dd'T'HH:mm:ssX\"]`"
+      - key: "grails.databinding.dateParsingLenient"
+        description: "Whether date parsing is lenient (accepting invalid dates 
like Feb 30)."
+        default: "`false`"
+  - name: "Internationalization"
+    properties:
+      - key: "grails.i18n.cache.seconds"
+        description: "How long (in seconds) to cache resolved message bundles 
with -1 to cache indefinitely and 0 to disable caching."
+        default: "`-1`"
+      - key: "grails.i18n.filecache.seconds"
+        description: "How long (in seconds) to cache the message bundle file 
lookup with -1 to cache indefinitely."
+        default: "`-1`"
+  - name: "Static Resources"
+    properties:
+      - key: "grails.resources.enabled"
+        description: "Whether serving static files from 
src/main/resources/public is enabled."
+        default: "`true`"
+      - key: "grails.resources.pattern"
+        description: "The URL path pattern for serving static resources."
+        default: "`/static/**`"
+      - key: "grails.resources.cachePeriod"
+        description: "The cache period (in seconds) for static resource HTTP 
responses."
+        default: "`0` _(no caching)_"
+  - name: "URL Mappings"
+    properties:
+      - key: "grails.urlmapping.cache.maxsize"
+        description: "The maximum size of the URL mapping cache."
+        default: "`1000`"
+  - name: "Scaffolding"
+    properties:
+      - key: "grails.scaffolding.templates.domainSuffix"
+        description: "The suffix appended to domain class names when 
generating scaffolding templates."
+        default: "`\"\"`"
+  - name: "Development & Reloading"
+    properties:
+      - key: "grails.reload.includes"
+        description: "List of fully qualified class names to include in 
development reloading, when set only these classes are reloaded."
+        default: "`[]` _(all project classes)_"
+      - key: "grails.reload.excludes"
+        description: "List of fully qualified class names to exclude from 
development reloading."
+        default: "`[]`"
+  - name: "Events"
+    properties:
+      - key: "grails.events.spring"
+        description: "Whether to bridge GORM/Grails events to the Spring 
ApplicationEventPublisher, allowing EventListener methods to receive domain 
events."
+        default: "`true`"
+  - name: "JSON & Converters"
+    properties:
+      - key: "grails.json.legacy.builder"
+        description: "Whether to use the legacy JSON builder."
+        default: "`false`"
+      - key: "grails.converters.json.domain.include.class"
+        description: "Whether to include the class property when marshalling 
domain objects to JSON."
+        default: "`false`"
+      - key: "grails.converters.xml.domain.include.class"
+        description: "Whether to include the class attribute when marshalling 
domain objects to XML."
+        default: "`false`"
+  - name: "GORM"
+    properties:
+      - key: "grails.gorm.failOnError"
+        description: "When true, save() throws ValidationException on 
validation failure instead of returning null and can also be a list of package 
names to apply selectively."
+        default: "`false`"
+      - key: "grails.gorm.autoFlush"
+        description: "Whether to automatically flush the Hibernate session 
between queries."
+        default: "`false`"
+      - key: "grails.gorm.flushMode"
+        description: "The default Hibernate flush mode (AUTO, COMMIT, MANUAL)."
+        default: "`AUTO`"
+      - key: "grails.gorm.markDirty"
+        description: "Whether to mark a domain instance as dirty on an 
explicit save() call."
+        default: "`true`"
+      - key: "grails.gorm.autowire"
+        description: "Whether to autowire Spring beans into domain class 
instances."
+        default: "`true`"
+      - key: "grails.gorm.default.mapping"
+        description: "A closure applied as the default mapping block for all 
domain classes."
+        default: "`{}`"
+      - key: "grails.gorm.default.constraints"
+        description: "A closure applied as the default constraints for all 
domain classes."
+        default: "`{}`"
+      - key: "grails.gorm.custom.types"
+        description: "Map of custom GORM types."
+        default: "`{}`"
+      - key: "grails.gorm.reactor.events"
+        description: "Whether to translate GORM events into Reactor events, 
which is disabled by default for performance."
+        default: "`false`"
+      - key: "grails.gorm.events.autoTimestampInsertOverwrite"
+        description: "Whether auto-timestamp (dateCreated) overwrites a 
user-provided value on insert."
+        default: "`true`"
+      - key: "grails.gorm.multiTenancy.mode"
+        description: "The multi-tenancy mode: DISCRIMINATOR, DATABASE, SCHEMA, 
or NONE."
+        default: "`NONE`"
+      - key: "grails.gorm.multiTenancy.tenantResolverClass"
+        description: "Fully qualified class name of the TenantResolver 
implementation."
+        default: "_(required when mode is not NONE)_"
+  - name: "DataSource"
+    properties:
+      - key: "dataSource.driverClassName"
+        description: "The JDBC driver class name."
+        default: "`org.h2.Driver`"
+      - key: "dataSource.username"
+        description: "The database username."
+        default: "`sa`"
+      - key: "dataSource.password"
+        description: "The database password."
+        default: "`''`"
+      - key: "dataSource.url"
+        description: "The JDBC connection URL."
+        default: "`jdbc:h2:mem:devDb` (dev)"
+      - key: "dataSource.dbCreate"
+        description: "The schema generation strategy: create-drop, create, 
update, validate, or none, use none in production with a migration tool."
+        default: "`create-drop` (dev), `none` (prod)"
+      - key: "dataSource.pooled"
+        description: "Whether to use a connection pool."
+        default: "`true`"
+      - key: "dataSource.logSql"
+        description: "Whether to log SQL statements to stdout."
+        default: "`false`"
+      - key: "dataSource.formatSql"
+        description: "Whether to format logged SQL for readability."
+        default: "`false`"
+      - key: "dataSource.dialect"
+        description: "The Hibernate dialect class name or class."
+        default: "_(auto-detected from driver)_"
+      - key: "dataSource.readOnly"
+        description: "Whether the DataSource is read-only (calls 
setReadOnly(true) on connections)."
+        default: "`false`"
+      - key: "dataSource.transactional"
+        description: "For additional datasources, whether to include in the 
chained transaction manager."
+        default: "`true`"
+      - key: "dataSource.persistenceInterceptor"
+        description: "For additional datasources, whether to wire up the 
persistence interceptor (the default datasource is always wired)."
+        default: "`false`"
+      - key: "dataSource.jmxExport"
+        description: "Whether to register JMX MBeans for the DataSource."
+        default: "`true`"
+      - key: "dataSource.type"
+        description: "The connection pool implementation class when multiple 
are on the classpath."
+        default: "`com.zaxxer.hikari.HikariDataSource`"
+  - name: "Hibernate"
+    properties:
+      - key: "hibernate.cache.queries"
+        description: "Whether to cache Hibernate queries."
+        default: "`false`"
+      - key: "hibernate.cache.use_second_level_cache"
+        description: "Whether to enable Hibernate's second-level cache."
+        default: "`false`"
+      - key: "hibernate.cache.use_query_cache"
+        description: "Whether to enable Hibernate's query cache."
+        default: "`false`"
+  - name: "Database Migration Plugin"
+    properties:
+      - key: "grails.plugin.databasemigration.updateOnStart"
+        description: "Whether to automatically apply pending migrations on 
application startup."
+        default: "`false`"
+      - key: "grails.plugin.databasemigration.updateAllOnStart"
+        description: "Whether to apply migrations for all datasources on 
startup (overrides per-datasource updateOnStart)."
+        default: "`false`"
+      - key: "grails.plugin.databasemigration.updateOnStartFileName"
+        description: "The changelog filename to use when applying migrations 
on startup."
+        default: "`changelog.groovy` (default ds), `changelog-<name>.groovy` 
(named ds)"
+      - key: "grails.plugin.databasemigration.dropOnStart"
+        description: "Whether to drop and recreate the schema before applying 
migrations on startup."
+        default: "`false`"
+      - key: "grails.plugin.databasemigration.updateOnStartContexts"
+        description: "List of Liquibase contexts to apply during startup 
migration."
+        default: "`[]` _(all contexts)_"
+      - key: "grails.plugin.databasemigration.updateOnStartLabels"
+        description: "List of Liquibase labels to apply during startup 
migration."
+        default: "`[]` _(all labels)_"
+      - key: "grails.plugin.databasemigration.updateOnStartDefaultSchema"
+        description: "The default schema to use when applying migrations on 
startup."
+        default: "_(database default schema)_"
+      - key: "grails.plugin.databasemigration.databaseChangeLogTableName"
+        description: "Custom name for the Liquibase changelog tracking table."
+        default: "`DATABASECHANGELOG`"
+      - key: "grails.plugin.databasemigration.databaseChangeLogLockTableName"
+        description: "Custom name for the Liquibase lock table."
+        default: "`DATABASECHANGELOGLOCK`"
+      - key: "grails.plugin.databasemigration.changelogLocation"
+        description: "The directory containing migration changelog files."
+        default: "`grails-app/migrations`"
+      - key: "grails.plugin.databasemigration.changelogFileName"
+        description: "The default changelog filename for CLI commands."
+        default: "`changelog.groovy` (default ds), `changelog-<name>.groovy` 
(named ds)"
+      - key: "grails.plugin.databasemigration.contexts"
+        description: "List of Liquibase contexts for CLI commands."
+        default: "`[]` _(all contexts)_"
+      - key: "grails.plugin.databasemigration.excludeObjects"
+        description: "Comma-separated list of database object names to exclude 
from dbm-gorm-diff and dbm-generate-changelog output, cannot be combined with 
includeObjects."
+        default: "`''`"
+      - key: "grails.plugin.databasemigration.includeObjects"
+        description: "Comma-separated list of database object names to include 
in diff and changelog output (all others excluded), cannot be combined with 
excludeObjects."
+        default: "`''`"
+      - key: "grails.plugin.databasemigration.skipUpdateOnStartMainClasses"
+        description: "List of main class names that should skip auto-migration 
on startup (e.g., CLI command runners)."
+        default: 
"`['grails.ui.command.GrailsApplicationContextCommandRunner']`"
+  - name: "Cache Plugin"
+    properties:
+      - key: "grails.cache.enabled"
+        description: "Whether the cache plugin is enabled."
+        default: "`true`"
+      - key: "grails.cache.cleanAtStartup"
+        description: "Whether to clear all caches on application startup."
+        default: "`false`"
+      - key: "grails.cache.cacheManager"
+        description: "Fully qualified class name of the CacheManager 
implementation."
+        default: "`grails.plugin.cache.GrailsConcurrentMapCacheManager`"
+      - key: "grails.cache.clearAtStartup"
+        description: "Alias for cleanAtStartup (both are supported)."
+        default: "`false`"
+      - key: "grails.cache.ehcache.ehcacheXmlLocation"
+        description: "Classpath location of the ehcache.xml configuration 
file."
+        default: "`classpath:ehcache.xml`"
+      - key: "grails.cache.ehcache.lockTimeout"
+        description: "Timeout (in milliseconds) for cache lock acquisition."
+        default: "`200`"
+  - name: "Asset Pipeline Plugin"
+    properties:
+      - key: "grails.assets.mapping"
+        description: "The URL path segment for serving assets (e.g., 
/assets/*) and must be one level deep where foo is valid and foo/bar is not."
+        default: "`assets`"
+      - key: "grails.assets.bundle"
+        description: "Whether assets are bundled in development mode where 
false loads individual files for easier debugging."
+        default: "`false`"
+      - key: "grails.assets.url"
+        description: "Base URL for assets, useful for CDN integration (e.g., 
https://cdn.example.com/) and can also be a Closure accepting 
HttpServletRequest for dynamic URL generation in application.groovy."
+        default: "_(derived from request)_"
+      - key: "grails.assets.storagePath"
+        description: "Directory path to copy compiled assets on application 
startup (e.g., for CDN upload)."
+        default: "_(none)_"
+      - key: "grails.assets.useManifest"
+        description: "Whether to use the manifest.properties file for asset 
resolution in production."
+        default: "`true`"
+      - key: "grails.assets.skipNotFound"
+        description: "If true, missing assets pass through to the next filter 
instead of returning 404."
+        default: "`false`"
+      - key: "grails.assets.allowDebugParam"
+        description: "If true, allows ?_debugAssets=y query parameter to force 
non-bundled mode in production for debugging."
+        default: "`false`"
+      - key: "grails.assets.cacheLocation"
+        description: "Directory for caching compiled assets during 
development."
+        default: "`build/assetCache`"
+      - key: "grails.assets.minifyJs"
+        description: "Whether to minify JavaScript using Google Closure 
Compiler."
+        default: "`true`"
+      - key: "grails.assets.minifyCss"
+        description: "Whether to minify CSS."
+        default: "`true`"
+      - key: "grails.assets.enableSourceMaps"
+        description: "Whether to generate source maps for minified JavaScript 
files (.js.map)."
+        default: "`true`"
+      - key: "grails.assets.enableDigests"
+        description: "Whether to generate digest or fingerprinted filenames 
(e.g., app-abc123.js)."
+        default: "`true`"
+      - key: "grails.assets.skipNonDigests"
+        description: "If true, only digested filenames are generated and 
non-digested names are served via manifest mapping, reducing storage by 50%."
+        default: "`true`"
+      - key: "grails.assets.enableGzip"
+        description: "Whether to generate gzipped versions of assets (.gz 
files)."
+        default: "`true`"
+      - key: "grails.assets.excludesGzip"
+        description: "List of GLOB patterns for files to exclude from gzip 
compression (e.g., ['**/*.png', '**/*.jpg'])."
+        default: "`['+++**+++/+++*+++.png', '+++**+++/+++*+++.jpg']`"
+      - key: "grails.assets.minifyOptions"
+        description: "Map of options passed to Google Closure Compiler with 
keys languageMode, targetLanguage, and optimizationLevel."
+        default: "`{languageMode: 'ES5', targetLanguage: 'ES5', 
optimizationLevel: 'SIMPLE'}`"
+      - key: "grails.assets.excludes"
+        description: "List of GLOB patterns (or regex: prefixed) for files to 
exclude from compilation, excluded files can still be included via require 
directives."
+        default: "`[]`"
+      - key: "grails.assets.includes"
+        description: "List of GLOB patterns to override excludes and allow 
specific files to be compiled even if they match an exclude pattern."
+        default: "`[]`"
+      - key: "grails.assets.enableES6"
+        description: "Enable ES6+ transpilation via Babel/SWC where if not set 
it auto-detects ES6 syntax."
+        default: "_(auto-detect)_"
+      - key: "grails.assets.commonJs"
+        description: "Whether to enable CommonJS module support (require() / 
module.exports)."
+        default: "`true`"
+      - key: "grails.assets.nodeEnv"
+        description: "Value injected as process.env.NODE_ENV in JavaScript 
files (used by libraries like React)."
+        default: "`development`"
+  - name: "Spring Security Plugin"
+    properties:
+      - key: "grails.plugin.springsecurity.active"
+        description: "Whether the security plugin is active."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.printStatusMessages"
+        description: "Whether to print startup and status messages to the 
console."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.rejectIfNoRule"
+        description: "Whether to reject requests if no matching security rule 
is found."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.securityConfigType"
+        description: "The security configuration strategy (Annotation, 
Requestmap, InterceptUrlMap)."
+        default: "`Annotation`"
+      - key: "grails.plugin.springsecurity.roleHierarchy"
+        description: "The role hierarchy definition string."
+        default: "`''`"
+      - key: "grails.plugin.springsecurity.cacheUsers"
+        description: "Whether to cache user details in the user details 
service."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.useHttpSessionEventPublisher"
+        description: "Whether to register a HttpSessionEventPublisher bean."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.useSecurityEventListener"
+        description: "Whether to publish security events to the Grails event 
system."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.userLookup.userDomainClassName"
+        description: "The fully qualified name of the user domain class."
+        default: "`null` _(must be set)_"
+      - key: "grails.plugin.springsecurity.userLookup.usernamePropertyName"
+        description: "The property name for the username in the user domain 
class."
+        default: "`username`"
+      - key: "grails.plugin.springsecurity.userLookup.enabledPropertyName"
+        description: "The property name for the enabled status in the user 
domain class."
+        default: "`enabled`"
+      - key: "grails.plugin.springsecurity.userLookup.passwordPropertyName"
+        description: "The property name for the password in the user domain 
class."
+        default: "`password`"
+      - key: "grails.plugin.springsecurity.userLookup.authoritiesPropertyName"
+        description: "The property name for the authorities collection in the 
user domain class."
+        default: "`authorities`"
+      - key: 
"grails.plugin.springsecurity.userLookup.accountExpiredPropertyName"
+        description: "The property name for the account expired status in the 
user domain class."
+        default: "`accountExpired`"
+      - key: 
"grails.plugin.springsecurity.userLookup.accountLockedPropertyName"
+        description: "The property name for the account locked status in the 
user domain class."
+        default: "`accountLocked`"
+      - key: 
"grails.plugin.springsecurity.userLookup.passwordExpiredPropertyName"
+        description: "The property name for the password expired status in the 
user domain class."
+        default: "`passwordExpired`"
+      - key: "grails.plugin.springsecurity.userLookup.authorityJoinClassName"
+        description: "The fully qualified name of the user-authority join 
domain class."
+        default: "`null` _(must be set)_"
+      - key: "grails.plugin.springsecurity.userLookup.usernameIgnoreCase"
+        description: "Whether to ignore case when looking up users by 
username."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.authority.className"
+        description: "The fully qualified name of the authority (role) domain 
class."
+        default: "`null` _(must be set)_"
+      - key: "grails.plugin.springsecurity.authority.nameField"
+        description: "The property name for the authority name in the 
authority domain class."
+        default: "`authority`"
+      - key: "grails.plugin.springsecurity.useRoleGroups"
+        description: "Whether to enable support for role groups."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.apf.filterProcessesUrl"
+        description: "The URL the authentication processing filter handles."
+        default: "`/login/authenticate`"
+      - key: "grails.plugin.springsecurity.apf.usernameParameter"
+        description: "The HTTP parameter name for the username in login 
requests."
+        default: "`username`"
+      - key: "grails.plugin.springsecurity.apf.passwordParameter"
+        description: "The HTTP parameter name for the password in login 
requests."
+        default: "`password`"
+      - key: "grails.plugin.springsecurity.apf.postOnly"
+        description: "Whether to restrict authentication requests to HTTP 
POST."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.apf.allowSessionCreation"
+        description: "Whether to allow the authentication filter to create a 
new HTTP session."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.apf.storeLastUsername"
+        description: "Whether to store the last used username in the session 
after a failed login."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.auth.loginFormUrl"
+        description: "The URL of the login form page."
+        default: "`/login/auth`"
+      - key: "grails.plugin.springsecurity.auth.forceHttps"
+        description: "Whether to force HTTPS for the login page."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.auth.ajaxLoginFormUrl"
+        description: "The URL of the AJAX login form."
+        default: "`/login/authAjax`"
+      - key: "grails.plugin.springsecurity.auth.useForward"
+        description: "Whether to use a forward instead of a redirect to the 
login page."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.successHandler.defaultTargetUrl"
+        description: "The default URL to redirect to after a successful login."
+        default: "`/`"
+      - key: "grails.plugin.springsecurity.successHandler.alwaysUseDefault"
+        description: "Whether to always redirect to the default target URL 
after login."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.successHandler.ajaxSuccessUrl"
+        description: "The URL used for AJAX success responses."
+        default: "`/login/ajaxSuccess`"
+      - key: "grails.plugin.springsecurity.successHandler.useReferer"
+        description: "Whether to redirect to the Referer header URL after 
login."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.failureHandler.defaultFailureUrl"
+        description: "The default URL to redirect to after a failed login."
+        default: "`/login/authfail?login_error=1`"
+      - key: "grails.plugin.springsecurity.failureHandler.ajaxAuthFailUrl"
+        description: "The URL used for AJAX failure responses."
+        default: "`/login/authfail?ajax=true`"
+      - key: "grails.plugin.springsecurity.failureHandler.useForward"
+        description: "Whether to use a forward for authentication failure."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.logout.afterLogoutUrl"
+        description: "The URL to redirect to after logging out."
+        default: "`/`"
+      - key: "grails.plugin.springsecurity.logout.filterProcessesUrl"
+        description: "The URL the logout filter handles."
+        default: "`/logoff`"
+      - key: "grails.plugin.springsecurity.logout.postOnly"
+        description: "Whether to restrict logout requests to HTTP POST."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.logout.invalidateHttpSession"
+        description: "Whether to invalidate the HTTP session on logout."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.logout.clearAuthentication"
+        description: "Whether to clear the authentication from the security 
context on logout."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.logout.redirectToReferer"
+        description: "Whether to redirect to the Referer header URL after 
logout."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.adh.errorPage"
+        description: "The URL of the access denied page."
+        default: "`/login/denied`"
+      - key: "grails.plugin.springsecurity.adh.ajaxErrorPage"
+        description: "The URL of the AJAX access denied page."
+        default: "`/login/ajaxDenied`"
+      - key: "grails.plugin.springsecurity.adh.useForward"
+        description: "Whether to use a forward for access denied errors."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.password.algorithm"
+        description: "The password hashing algorithm."
+        default: "`bcrypt`"
+      - key: "grails.plugin.springsecurity.password.encodeHashAsBase64"
+        description: "Whether to encode the hashed password as Base64."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.password.bcrypt.logrounds"
+        description: "The number of log rounds for the BCrypt algorithm."
+        default: "`10` _(4 in test)_"
+      - key: "grails.plugin.springsecurity.password.hash.iterations"
+        description: "The number of hash iterations for algorithms that 
support it."
+        default: "`10000` _(1 in test)_"
+      - key: "grails.plugin.springsecurity.rememberMe.cookieName"
+        description: "The name of the remember-me cookie."
+        default: "`grails_remember_me`"
+      - key: "grails.plugin.springsecurity.rememberMe.alwaysRemember"
+        description: "Whether to always remember the user, even if the 
checkbox is not checked."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.rememberMe.tokenValiditySeconds"
+        description: "The validity period (in seconds) of the remember-me 
token."
+        default: "`1209600` _(14 days)_"
+      - key: "grails.plugin.springsecurity.rememberMe.parameter"
+        description: "The HTTP parameter name for the remember-me checkbox."
+        default: "`remember-me`"
+      - key: "grails.plugin.springsecurity.rememberMe.key"
+        description: "The secret key used to sign remember-me cookies."
+        default: "`grailsRocks`"
+      - key: "grails.plugin.springsecurity.rememberMe.persistent"
+        description: "Whether to use persistent (database-backed) remember-me 
tokens."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.rememberMe.useSecureCookie"
+        description: "Whether to use the Secure flag on the remember-me 
cookie."
+        default: "`null`"
+      - key: 
"grails.plugin.springsecurity.rememberMe.persistentToken.domainClassName"
+        description: "The fully qualified name of the persistent token domain 
class."
+        default: "`null`"
+      - key: "grails.plugin.springsecurity.controllerAnnotations.staticRules"
+        description: "Map of static URL rules for controller-based security."
+        default: "`[]`"
+      - key: "grails.plugin.springsecurity.interceptUrlMap"
+        description: "Map of URL patterns to security rules."
+        default: "`[]`"
+      - key: "grails.plugin.springsecurity.requestMap.className"
+        description: "The fully qualified name of the Requestmap domain class."
+        default: "`null`"
+      - key: "grails.plugin.springsecurity.requestMap.urlField"
+        description: "The property name for the URL in the Requestmap domain 
class."
+        default: "`url`"
+      - key: "grails.plugin.springsecurity.requestMap.configAttributeField"
+        description: "The property name for the config attribute in the 
Requestmap domain class."
+        default: "`configAttribute`"
+      - key: "grails.plugin.springsecurity.fii.rejectPublicInvocations"
+        description: "Whether to reject invocations that do not match any 
security rule."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.fii.alwaysReauthenticate"
+        description: "Whether to always re-authenticate on every request."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.fii.validateConfigAttributes"
+        description: "Whether to validate configuration attributes at startup."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.fii.observeOncePerRequest"
+        description: "Whether to ensure security checks are performed only 
once per request."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.useSessionFixationPrevention"
+        description: "Whether to enable session fixation prevention."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.sessionFixationPrevention.migrate"
+        description: "Whether to migrate session attributes to the new session 
after login."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.scr.allowSessionCreation"
+        description: "Whether the security context repository is allowed to 
create a session."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.scr.disableUrlRewriting"
+        description: "Whether to disable URL rewriting for session IDs."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.scpf.forceEagerSessionCreation"
+        description: "Whether to force eager creation of the HTTP session."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.sch.strategyName"
+        description: "The security context holder strategy (MODE_THREADLOCAL, 
MODE_INHERITABLETHREADLOCAL)."
+        default: "`MODE_THREADLOCAL`"
+      - key: "grails.plugin.springsecurity.useBasicAuth"
+        description: "Whether to enable HTTP Basic authentication."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.basic.realmName"
+        description: "The realm name used in HTTP Basic authentication."
+        default: "`Grails Realm`"
+      - key: "grails.plugin.springsecurity.useSwitchUserFilter"
+        description: "Whether to enable the switch user filter for user 
impersonation."
+        default: "`false`"
+      - key: "grails.plugin.springsecurity.switchUser.switchUserUrl"
+        description: "The URL used to initiate a user switch."
+        default: "`/login/impersonate`"
+      - key: "grails.plugin.springsecurity.switchUser.exitUserUrl"
+        description: "The URL used to exit a user switch and return to the 
original user."
+        default: "`/logout/impersonate`"
+      - key: "grails.plugin.springsecurity.gsp.layoutAuth"
+        description: "The Sitemesh layout used for the authentication page."
+        default: "`main`"
+      - key: "grails.plugin.springsecurity.gsp.layoutDenied"
+        description: "The Sitemesh layout used for the access denied page."
+        default: "`main`"
+      - key: "grails.plugin.springsecurity.ajaxHeader"
+        description: "The HTTP header name used to identify AJAX requests."
+        default: "`X-Requested-With`"
+      - key: "grails.plugin.springsecurity.ipRestrictions"
+        description: "List of IP-based restriction rules."
+        default: "`[]`"
+      - key: "grails.plugin.springsecurity.portMapper.httpPort"
+        description: "The standard HTTP port used for redirecting between 
secure and insecure pages."
+        default: "`8080`"
+      - key: "grails.plugin.springsecurity.portMapper.httpsPort"
+        description: "The standard HTTPS port used for redirecting between 
secure and insecure pages."
+        default: "`8443`"
+      - key: "grails.plugin.springsecurity.dao.hideUserNotFoundExceptions"
+        description: "Whether to hide UsernameNotFoundException and instead 
throw BadCredentialsException."
+        default: "`true`"
+      - key: 
"grails.plugin.springsecurity.providerManager.eraseCredentialsAfterAuthentication"
+        description: "Whether to erase password credentials from the 
Authentication object after successful authentication."
+        default: "`true`"
+      - key: "grails.plugin.springsecurity.debug.useFilter"
+        description: "Whether to enable the Spring Security debug filter."
+        default: "`false`"
+  - name: "MongoDB GORM Plugin"
+    properties:
+      - key: "grails.mongodb.url"
+        description: "The MongoDB connection string."
+        default: "`mongodb://localhost/test`"
+      - key: "grails.mongodb.host"
+        description: "The MongoDB server host."
+        default: "`localhost`"
+      - key: "grails.mongodb.port"
+        description: "The MongoDB server port."
+        default: "`27017`"
+      - key: "grails.mongodb.databaseName"
+        description: "The name of the MongoDB database."
+        default: "_(application name)_"
+      - key: "grails.mongodb.username"
+        description: "The database username."
+        default: "_(none)_"
+      - key: "grails.mongodb.password"
+        description: "The database password."
+        default: "_(none)_"
+      - key: "grails.mongodb.stateless"
+        description: "Whether the GORM implementation is stateless (disabling 
persistence context)."
+        default: "`false`"
+      - key: "grails.mongodb.decimalType"
+        description: "Whether to use the Decimal128 type for BigDecimal 
properties."
+        default: "`false`"
+      - key: "grails.mongodb.codecs"
+        description: "List of custom MongoDB codec classes."
+        default: "`[]`"
+      - key: "grails.mongodb.default.mapping"
+        description: "A closure applied as the default mapping block for 
MongoDB domain classes."
+        default: "`{}`"
+      - key: "grails.mongodb.options.connectionPoolSettings.maxSize"
+        description: "The maximum number of connections in the pool."
+        default: "`100`"
+      - key: "grails.mongodb.options.connectionPoolSettings.minSize"
+        description: "The minimum number of connections in the pool."
+        default: "`0`"
+      - key: "grails.mongodb.options.connectionPoolSettings.maxWaitTime"
+        description: "The maximum time (in milliseconds) a thread will wait 
for a connection."
+        default: "`120000`"
+      - key: 
"grails.mongodb.options.connectionPoolSettings.maxConnectionLifeTime"
+        description: "The maximum life time (in milliseconds) of a pooled 
connection."
+        default: "`0` _(unlimited)_"
+      - key: 
"grails.mongodb.options.connectionPoolSettings.maxConnectionIdleTime"
+        description: "The maximum idle time (in milliseconds) of a pooled 
connection."
+        default: "`0` _(unlimited)_"
+      - key: "grails.mongodb.options.readPreference"
+        description: "The read preference strategy."
+        default: "_(none)_"
+      - key: "grails.mongodb.options.writeConcern"
+        description: "The write concern strategy."
+        default: "_(none)_"
+      - key: "grails.mongodb.options.readConcern"
+        description: "The read concern strategy."
+        default: "_(none)_"
+      - key: "grails.mongodb.options.retryWrites"
+        description: "Whether to retry write operations on failure."
+        default: "_(none)_"
+      - key: "grails.mongodb.options.retryReads"
+        description: "Whether to retry read operations on failure."
+        default: "_(none)_"
+      - key: "grails.mongodb.options.applicationName"
+        description: "The name of the application (used for logging and 
monitoring)."
+        default: "_(none)_"
+      - key: "grails.mongodb.options.sslSettings.enabled"
+        description: "Whether SSL is enabled for connections."
+        default: "`false`"
+      - key: "grails.mongodb.options.sslSettings.invalidHostNameAllowed"
+        description: "Whether invalid hostnames are allowed in SSL 
certificates."
+        default: "`false`"
diff --git 
a/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy
 
b/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy
index 4bd4f67370..3c05ddd910 100644
--- 
a/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy
+++ 
b/grails-core/src/test/groovy/grails/dev/commands/ConfigReportCommandSpec.groovy
@@ -86,16 +86,20 @@ class ConfigReportCommandSpec extends Specification {
         File reportFile = new File(executionContext.baseDir, 
ConfigReportCommand.DEFAULT_REPORT_FILE)
         reportFile.exists()
 
-        and:
+        and: "report has correct AsciiDoc structure"
         String content = reportFile.text
         content.contains('= Grails Application Configuration Report')
-        content.contains('== grails')
-        content.contains('== server')
-        content.contains('== spring')
+
+        and: "known Grails properties appear in their metadata category 
sections"
         content.contains('`grails.profile`')
-        content.contains('`web`')
+        content.contains('`grails.codegen.defaultPackage`')
+
+        and: "unknown runtime properties appear in the Other Properties 
section"
+        content.contains('== Other Properties')
         content.contains('`server.port`')
         content.contains('`8080`')
+        content.contains('`spring.main.banner-mode`')
+        content.contains('`off`')
 
         cleanup:
         reportFile?.delete()
@@ -178,50 +182,90 @@ class ConfigReportCommandSpec extends Specification {
         !result.containsKey('app.bad')
     }
 
-    def "writeReport groups properties by top-level namespace"() {
+    def "writeReport uses 3-column format with metadata categories"() {
         given:
-        Map<String, String> sorted = new TreeMap<String, String>()
-        sorted.put('grails.controllers.defaultScope', 'singleton')
-        sorted.put('grails.profile', 'web')
-        sorted.put('server.port', '8080')
+        Map<String, String> runtimeProperties = new TreeMap<String, String>()
+        runtimeProperties.put('grails.controllers.defaultScope', 'singleton')
+        runtimeProperties.put('grails.profile', 'web')
+        runtimeProperties.put('server.port', '8080')
 
         File reportFile = new File(tempDir, 'test-report.adoc')
 
         when:
-        command.writeReport(sorted, reportFile)
+        command.writeReport(runtimeProperties, reportFile)
 
         then:
         String content = reportFile.text
 
-        and: "report has correct AsciiDoc structure"
+        and: "report has correct AsciiDoc header"
         content.startsWith('= Grails Application Configuration Report')
         content.contains(':toc: left')
-        content.contains('[cols="2,3", options="header"]')
-        content.contains('| Property | Value')
 
-        and: "properties are grouped by namespace"
-        content.contains('== grails')
-        content.contains('== server')
+        and: "metadata categories are used as section headers"
+        content.contains('== Core Properties')
+        content.contains('== Web & Controllers')
 
-        and: "grails section appears before server section (alphabetical)"
-        content.indexOf('== grails') < content.indexOf('== server')
+        and: "3-column table format is used for known properties"
+        content.contains('[cols="2,5,2", options="header"]')
+        content.contains('| Property | Description | Default')
 
-        and: "properties are listed under correct sections"
+        and: "known properties appear with descriptions"
+        content.contains('`grails.profile`')
         content.contains('`grails.controllers.defaultScope`')
+
+        and: "runtime values override static defaults for known properties"
+        content.contains('`web`')
         content.contains('`singleton`')
+
+        and: "unknown runtime properties go to Other Properties section"
+        content.contains('== Other Properties')
         content.contains('`server.port`')
         content.contains('`8080`')
     }
 
+    def "writeReport shows static defaults when no runtime value exists"() {
+        given: "no runtime properties provided"
+        Map<String, String> runtimeProperties = new TreeMap<String, String>()
+        File reportFile = new File(tempDir, 'defaults-report.adoc')
+
+        when:
+        command.writeReport(runtimeProperties, reportFile)
+
+        then:
+        String content = reportFile.text
+
+        and: "metadata categories are still present with static defaults"
+        content.contains('== Core Properties')
+        content.contains('`grails.profile`')
+        content.contains('Set by project template')
+    }
+
+    def "writeReport runtime values override static defaults"() {
+        given:
+        Map<String, String> runtimeProperties = new TreeMap<String, String>()
+        runtimeProperties.put('grails.profile', 'rest-api')
+
+        File reportFile = new File(tempDir, 'override-report.adoc')
+
+        when:
+        command.writeReport(runtimeProperties, reportFile)
+
+        then:
+        String content = reportFile.text
+
+        and: "runtime value overrides the static default"
+        content.contains('`rest-api`')
+    }
+
     def "writeReport escapes pipe characters in values"() {
         given:
-        Map<String, String> sorted = new TreeMap<String, String>()
-        sorted.put('test.key', 'value|with|pipes')
+        Map<String, String> runtimeProperties = new TreeMap<String, String>()
+        runtimeProperties.put('test.key', 'value|with|pipes')
 
         File reportFile = new File(tempDir, 'escape-test.adoc')
 
         when:
-        command.writeReport(sorted, reportFile)
+        command.writeReport(runtimeProperties, reportFile)
 
         then:
         String content = reportFile.text
@@ -229,19 +273,70 @@ class ConfigReportCommandSpec extends Specification {
         !content.contains('value|with|pipes')
     }
 
-    def "writeReport handles empty configuration"() {
+    def "writeReport handles empty configuration with no Other Properties"() {
         given:
-        Map<String, String> sorted = new TreeMap<String, String>()
+        Map<String, String> runtimeProperties = new TreeMap<String, String>()
         File reportFile = new File(tempDir, 'empty-report.adoc')
 
         when:
-        command.writeReport(sorted, reportFile)
+        command.writeReport(runtimeProperties, reportFile)
 
         then:
         reportFile.exists()
         String content = reportFile.text
         content.contains('= Grails Application Configuration Report')
-        !content.contains('|===')
+
+        and: "metadata categories still appear from the YAML"
+        content.contains('== Core Properties')
+
+        and: "no Other Properties section when no unknown runtime properties"
+        !content.contains('== Other Properties')
+    }
+
+    def "writeReport puts only unknown runtime properties in Other 
Properties"() {
+        given:
+        Map<String, String> runtimeProperties = new TreeMap<String, String>()
+        runtimeProperties.put('custom.app.setting', 'myvalue')
+        runtimeProperties.put('grails.profile', 'web')
+
+        File reportFile = new File(tempDir, 'other-props-report.adoc')
+
+        when:
+        command.writeReport(runtimeProperties, reportFile)
+
+        then:
+        String content = reportFile.text
+
+        and: "known property is in its category, not in Other Properties"
+        content.contains('== Core Properties')
+        content.contains('`grails.profile`')
+
+        and: "unknown property appears in Other Properties"
+        content.contains('== Other Properties')
+        content.contains('`custom.app.setting`')
+        content.contains('`myvalue`')
+
+        and: "Other Properties uses 2-column format"
+        int otherIdx = content.indexOf('== Other Properties')
+        String otherSection = content.substring(otherIdx)
+        otherSection.contains('[cols="2,3", options="header"]')
+        otherSection.contains('| Property | Default')
+    }
+
+    def "loadPropertyMetadata returns properties from classpath YAML"() {
+        when:
+        Map<String, Map<String, String>> metadata = 
command.loadPropertyMetadata()
+
+        then: "metadata is loaded from the config-properties.yml on the 
classpath"
+        !metadata.isEmpty()
+        metadata.containsKey('grails.profile')
+
+        and: "each entry has the expected fields"
+        Map<String, String> profileEntry = metadata.get('grails.profile')
+        profileEntry.get('key') == 'grails.profile'
+        profileEntry.get('description') != null
+        profileEntry.get('description').length() > 0
+        profileEntry.get('category') == 'Core Properties'
     }
 
     def "escapeAsciidoc handles null and empty strings"() {
diff --git 
a/grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy
 
b/grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy
index fd85880a79..19a4362141 100644
--- 
a/grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy
+++ 
b/grails-test-examples/config-report/src/integration-test/groovy/configreport/ConfigReportCommandIntegrationSpec.groovy
@@ -37,9 +37,14 @@ import spock.lang.TempDir
  *   <li>{@code application.groovy} - Groovy-based configuration</li>
  *   <li>{@code @ConfigurationProperties} - Type-safe configuration beans</li>
  * </ul>
+ *
+ * <p>The hybrid report uses curated property metadata (from {@code 
config-properties.yml})
+ * to produce a 3-column AsciiDoc table (Property | Description | Default) for 
known
+ * Grails properties, with runtime values overriding static defaults. 
Properties not
+ * found in the metadata appear in a separate "Other Properties" section.
  */
 @Integration
-@Narrative('Verifies that ConfigReportCommand generates an AsciiDoc report 
containing properties from application.yml, application.groovy, and 
@ConfigurationProperties sources')
+@Narrative('Verifies that ConfigReportCommand generates a hybrid AsciiDoc 
report merging static property metadata with runtime-collected values')
 class ConfigReportCommandIntegrationSpec extends Specification {
 
     @Autowired
@@ -57,14 +62,7 @@ class ConfigReportCommandIntegrationSpec extends 
Specification {
         return command
     }
 
-    private File executeCommand(ConfigReportCommand command) {
-        ExecutionContext executionContext = new 
ExecutionContext(Mock(CommandLine))
-        File reportFile = new File(executionContext.baseDir, 
ConfigReportCommand.DEFAULT_REPORT_FILE)
-        command.handle(executionContext)
-        return reportFile
-    }
-
-    def "ConfigReportCommand generates a report file"() {
+    def "ConfigReportCommand generates a report file with hybrid format"() {
         given: 'a ConfigReportCommand wired to the live application context'
         ConfigReportCommand command = createCommand()
 
@@ -82,18 +80,18 @@ class ConfigReportCommandIntegrationSpec extends 
Specification {
         reportFile.exists()
         reportFile.length() > 0
 
-        and: 'the report has valid AsciiDoc structure'
+        and: 'the report has valid AsciiDoc structure with 3-column format'
         String content = reportFile.text
         content.startsWith('= Grails Application Configuration Report')
         content.contains(':toc: left')
-        content.contains('[cols="2,3", options="header"]')
-        content.contains('| Property | Value')
+        content.contains('[cols="2,5,2", options="header"]')
+        content.contains('| Property | Description | Default')
 
         cleanup:
         reportFile?.delete()
     }
 
-    def "report contains properties from application.yml"() {
+    def "report shows known Grails properties in metadata categories"() {
         given: 'a ConfigReportCommand wired to the live application context'
         ConfigReportCommand command = createCommand()
         ExecutionContext executionContext = new 
ExecutionContext(Mock(CommandLine))
@@ -103,29 +101,49 @@ class ConfigReportCommandIntegrationSpec extends 
Specification {
         command.handle(executionContext)
         String content = reportFile.text
 
-        then: 'YAML-defined properties are present in the report'
+        then: 'known metadata categories are present as section headers'
+        content.contains('== Core Properties')
+        content.contains('== Web & Controllers')
+        content.contains('== DataSource')
+
+        and: 'grails.profile appears in the Core Properties section with its 
description'
+        content.contains('`grails.profile`')
+
+        and: 'runtime value overrides the static default for grails.profile'
+        content.contains('`web`')
+
+        cleanup:
+        reportFile?.delete()
+    }
+
+    def "report puts custom application properties in Other Properties 
section"() {
+        given: 'a ConfigReportCommand wired to the live application context'
+        ConfigReportCommand command = createCommand()
+        ExecutionContext executionContext = new 
ExecutionContext(Mock(CommandLine))
+        File reportFile = new File(executionContext.baseDir, 
ConfigReportCommand.DEFAULT_REPORT_FILE)
+
+        when: 'the command is executed'
+        command.handle(executionContext)
+        String content = reportFile.text
+
+        then: 'YAML-defined custom properties appear in Other Properties'
+        content.contains('== Other Properties')
         content.contains('`myapp.yaml.greeting`')
         content.contains('`Hello from YAML`')
 
-        and: 'YAML numeric properties are present'
+        and: 'YAML numeric properties are in Other Properties'
         content.contains('`myapp.yaml.maxRetries`')
         content.contains('`5`')
 
-        and: 'YAML nested properties are present'
+        and: 'YAML nested properties are in Other Properties'
         content.contains('`myapp.yaml.feature.enabled`')
-        content.contains('`true`')
         content.contains('`myapp.yaml.feature.timeout`')
-        content.contains('`30000`')
-
-        and: 'standard Grails YAML properties are present'
-        content.contains('`grails.profile`')
-        content.contains('`web`')
 
         cleanup:
         reportFile?.delete()
     }
 
-    def "report contains properties from application.groovy"() {
+    def "report contains properties from application.groovy in Other 
Properties"() {
         given: 'a ConfigReportCommand wired to the live application context'
         ConfigReportCommand command = createCommand()
         ExecutionContext executionContext = new 
ExecutionContext(Mock(CommandLine))
@@ -135,7 +153,7 @@ class ConfigReportCommandIntegrationSpec extends 
Specification {
         command.handle(executionContext)
         String content = reportFile.text
 
-        then: 'Groovy config properties are present in the report'
+        then: 'Groovy config properties are present in Other Properties'
         content.contains('`myapp.groovy.appName`')
         content.contains('`Config Report Test App`')
 
@@ -166,7 +184,7 @@ class ConfigReportCommandIntegrationSpec extends 
Specification {
         reportFile?.delete()
     }
 
-    def "report contains properties bound via @ConfigurationProperties"() {
+    def "report contains properties bound via @ConfigurationProperties in 
Other Properties"() {
         given: 'a ConfigReportCommand wired to the live application context'
         ConfigReportCommand command = createCommand()
         ExecutionContext executionContext = new 
ExecutionContext(Mock(CommandLine))
@@ -181,7 +199,7 @@ class ConfigReportCommandIntegrationSpec extends 
Specification {
         appProperties.pageSize == 50
         appProperties.debugEnabled == true
 
-        and: 'the typed properties appear in the config report'
+        and: 'the typed properties appear in Other Properties'
         content.contains('`myapp.typed.name`')
         content.contains('`Configured App`')
         content.contains('`myapp.typed.pageSize`')
@@ -193,7 +211,7 @@ class ConfigReportCommandIntegrationSpec extends 
Specification {
         reportFile?.delete()
     }
 
-    def "report groups properties by top-level namespace"() {
+    def "report separates known metadata properties from custom properties"() {
         given: 'a ConfigReportCommand wired to the live application context'
         ConfigReportCommand command = createCommand()
         ExecutionContext executionContext = new 
ExecutionContext(Mock(CommandLine))
@@ -203,20 +221,27 @@ class ConfigReportCommandIntegrationSpec extends 
Specification {
         command.handle(executionContext)
         String content = reportFile.text
 
-        then: 'properties are organized into namespace sections'
-        content.contains('== grails')
-        content.contains('== myapp')
-        content.contains('== dataSource')
+        then: 'known Grails properties appear in categorized sections before 
Other Properties'
+        int coreIdx = content.indexOf('== Core Properties')
+        int otherIdx = content.indexOf('== Other Properties')
+        coreIdx >= 0
+        otherIdx >= 0
+        coreIdx < otherIdx
+
+        and: 'grails.profile is in the Core Properties section (not Other 
Properties)'
+        String otherSection = content.substring(otherIdx)
+        !otherSection.contains('`grails.profile`')
 
-        and: 'sections are in alphabetical order'
-        content.indexOf('== dataSource') < content.indexOf('== grails')
-        content.indexOf('== grails') < content.indexOf('== myapp')
+        and: 'custom myapp properties are in Other Properties (not in 
categorized sections)'
+        String beforeOther = content.substring(0, otherIdx)
+        !beforeOther.contains('`myapp.yaml.greeting`')
+        !beforeOther.contains('`myapp.groovy.appName`')
 
         cleanup:
         reportFile?.delete()
     }
 
-    def "report contains properties from all three config sources 
simultaneously"() {
+    def "report contains properties from all three config sources"() {
         given: 'a ConfigReportCommand wired to the live application context'
         ConfigReportCommand command = createCommand()
         ExecutionContext executionContext = new 
ExecutionContext(Mock(CommandLine))
@@ -235,17 +260,11 @@ class ConfigReportCommandIntegrationSpec extends 
Specification {
         and: 'typed @ConfigurationProperties are present'
         content.contains('`myapp.typed.name`')
 
-        and: 'all properties are in the same myapp section'
-        int myappSectionIndex = content.indexOf('== myapp')
-        myappSectionIndex >= 0
-
-        and: 'each table row has the correct AsciiDoc format'
-        content.contains('| `myapp.yaml.greeting`')
-        content.contains('| `Hello from YAML`')
-        content.contains('| `myapp.groovy.appName`')
-        content.contains('| `Config Report Test App`')
-        content.contains('| `myapp.typed.name`')
-        content.contains('| `Configured App`')
+        and: 'Other Properties section uses 2-column format'
+        int otherIdx = content.indexOf('== Other Properties')
+        String otherSection = content.substring(otherIdx)
+        otherSection.contains('[cols="2,3", options="header"]')
+        otherSection.contains('| Property | Default')
 
         cleanup:
         reportFile?.delete()

Reply via email to