You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/06/06 08:20:55 UTC

(causeway) 03/39: CAUSEWAY-4012: adds safe action command publishing

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

danhaywood pushed a commit to branch CAUSEWAY-4012
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 08fd941a6c993ebe3a270396e75a8ab255e13d05
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Fri Jun 5 08:38:52 2026 +0100

    CAUSEWAY-4012: adds safe action command publishing
---
 .../config/pages/sections/causeway.extensions.adoc | 12 ++++
 .../core/config/CausewayConfiguration.java         | 22 ++++++
 .../CommandPublishingFacetForActionAnnotation.java | 26 +++++--
 ...dPublishingFacetForActionFromConfiguration.java | 19 +++++
 .../integtest/CommandLog_IntegTestAbstract.java    | 83 ++++++++++++++++++++++
 .../commandlog/applib/integtest/model/Counter.java | 28 ++++++++
 .../log-safe-single-result-finders/design.md       | 57 ++++++++-------
 .../log-safe-single-result-finders/proposal.md     | 18 ++---
 .../specs/command-export-yaml-result/spec.md       | 16 ++---
 .../specs/command-replay-result-mapping/spec.md    | 24 +++----
 .../spec.md                                        | 67 ++++++++---------
 .../log-safe-single-result-finders/tasks.md        | 54 +++++++-------
 12 files changed, 300 insertions(+), 126 deletions(-)

diff --git a/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc b/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc
index d918d7aa97a..f2c7fca700d 100644
--- a/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc
+++ b/core/config/src/main/adoc/modules/config/pages/sections/causeway.extensions.adoc
@@ -33,6 +33,18 @@ persist
 One reason to use this option is if you wish to provide your own implementation that wraps or delegates to the default implementation of ``CommandSubscriber`` that is provided by the _commandlog_ extension. Because commands are published to _all_ subscribers on the class path, you can disable the default implementation from doing anything using this setting.
 
 
+|
+[[causeway.extensions.command-log.safe-action-command-publishing]]
+causeway.extensions.command-log. +
+safe-action-command-publishing
+
+|  false
+| Whether safe action invocations should be command published so they can be persisted to the command log.
+
+This is disabled by default because safe actions can be frequent.
+It is intended for regression recording scenarios where finder commands need to be exported and replayed.
+
+
 |
 [[causeway.extensions.command-log.run-background-commands.batch-size]]
 causeway.extensions.command-log. +
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
index 7dcc74cac90..0d5091736eb 100644
--- a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
+++ b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
@@ -3979,6 +3979,28 @@ public enum PersistPolicy {
              */
             private PersistPolicy persist = PersistPolicy.ENABLED;
 
+            /**
+             * Whether safe action invocations should be command published so they can be persisted to the command log.
+             *
+             * <p>
+             *     This is disabled by default because safe actions can be frequent.
+             *     It is intended for regression recording scenarios where finder commands need to be exported and replayed.
+             * </p>
+             *
+             * <p>
+             *     External property: {@code causeway.extensions.command-log.safe-action-command-publishing}.
+             * </p>
+             */
+            private boolean safeActionCommandPublishing = false;
+
+            public boolean getSafeActionCommandPublishing() {
+                return safeActionCommandPublishing;
+            }
+
+            public void setSafeActionCommandPublishing(final boolean safeActionCommandPublishing) {
+                this.safeActionCommandPublishing = safeActionCommandPublishing;
+            }
+
             private final ReplayResultMapping replayResultMapping = new ReplayResultMapping();
             @Data
             public static class ReplayResultMapping {
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
index cf6690083e8..c64a1f4b3d4 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
@@ -63,9 +63,10 @@ public static Optional<CommandPublishingFacet> create(
 
         val publishingPolicy = ActionConfigOptions.actionCommandPublishingPolicy(configuration);
 
+        val safeSemantics = hasSafeSemantics(holder);
+
         return actionsIfAny
-                .filter(action -> action.commandPublishing() != Publishing.NOT_SPECIFIED)
-                .map(action -> {
+                .<CommandPublishingFacet>map(action -> {
                     Publishing publishing = action.commandPublishing();
 
                     final Class<? extends CommandDtoProcessor> processorClass = action.commandDtoProcessor();
@@ -76,14 +77,23 @@ public static Optional<CommandPublishingFacet> create(
                     }
 
                     switch (publishing) {
+                        case NOT_SPECIFIED:
+                            return safeSemantics
+                                    ? new CommandPublishingFacetForActionFromConfiguration.SafeEnabledByCommandLogProperty(
+                                            holder, servicesInjector, configuration)
+                                    : null;
                         case AS_CONFIGURED:
                             switch (publishingPolicy) {
                                 case NONE:
-                                    return new CommandPublishingFacetForActionAnnotationAsConfigured.None(holder, servicesInjector);
+                                    return safeSemantics
+                                            ? new CommandPublishingFacetForActionFromConfiguration.SafeEnabledByCommandLogProperty(
+                                                    holder, servicesInjector, configuration)
+                                            : new CommandPublishingFacetForActionAnnotationAsConfigured.None(holder, servicesInjector);
                                 case IGNORE_QUERY_ONLY:
                                 case IGNORE_SAFE:
-                                    return hasSafeSemantics(holder)
-                                            ? new CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafe(holder, servicesInjector)
+                                    return safeSemantics
+                                            ? new CommandPublishingFacetForActionFromConfiguration.SafeEnabledByCommandLogProperty(
+                                                    holder, servicesInjector, configuration)
                                             : new CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafeYetNot(holder, servicesInjector);
                                 case ALL:
                                     return new CommandPublishingFacetForActionAnnotationAsConfigured.All(holder, servicesInjector);
@@ -98,7 +108,11 @@ public static Optional<CommandPublishingFacet> create(
                         default:
                             throw new IllegalStateException(String.format("@Action#commandPublishing '%s' not recognised", publishing));
                     }
-                });
+                })
+                .or(() -> safeSemantics
+                        ? Optional.of((CommandPublishingFacet)new CommandPublishingFacetForActionFromConfiguration.SafeEnabledByCommandLogProperty(
+                                holder, servicesInjector, configuration))
+                        : Optional.empty());
     }
 
     static boolean hasSafeSemantics(final FacetHolder holder) {
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionFromConfiguration.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionFromConfiguration.java
index ca2cdf62efa..25e4cd66c85 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionFromConfiguration.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionFromConfiguration.java
@@ -19,6 +19,7 @@
 package org.apache.causeway.core.metamodel.facets.members.publish.command;
 
 import org.apache.causeway.applib.services.inject.ServiceInjector;
+import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 
 public abstract class CommandPublishingFacetForActionFromConfiguration extends CommandPublishingFacetAbstract {
@@ -59,6 +60,24 @@ public boolean isEnabled() {
         }
     }
 
+    static class SafeEnabledByCommandLogProperty extends CommandPublishingFacetForActionFromConfiguration {
+
+        private final CausewayConfiguration configuration;
+
+        SafeEnabledByCommandLogProperty(
+                FacetHolder holder,
+                ServiceInjector servicesInjector,
+                CausewayConfiguration configuration) {
+            super(holder, servicesInjector);
+            this.configuration = configuration;
+        }
+
+        @Override
+        public boolean isEnabled() {
+            return configuration.getExtensions().getCommandLog().getSafeActionCommandPublishing();
+        }
+    }
+
     static class IgnoreSafeYetNot extends CommandPublishingFacetForActionFromConfiguration {
 
         IgnoreSafeYetNot(FacetHolder holder, ServiceInjector servicesInjector) {
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
index ed271259221..0730df4e6aa 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/CommandLog_IntegTestAbstract.java
@@ -37,6 +37,8 @@
 import org.apache.causeway.applib.services.sudo.SudoService;
 import org.apache.causeway.applib.services.user.UserMemento;
 import org.apache.causeway.applib.services.wrapper.WrapperFactory;
+import org.apache.causeway.applib.util.schema.CommandDtoUtils;
+import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.config.beans.CausewayBeanTypeRegistry;
 import org.apache.causeway.core.config.presets.CausewayPresets;
 import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
@@ -77,6 +79,7 @@ void beforeEach() {
 
         counterRepository.removeAll();
         commandLogEntryRepository.removeAll();
+        causewayConfiguration.getExtensions().getCommandLog().setSafeActionCommandPublishing(false);
 
         assertThat(counterRepository.find()).isEmpty();
 
@@ -215,6 +218,85 @@ void invoke_direct_disabled() {
         assertThat(mostRecentCompleted).isEmpty();
     }
 
+    @Test
+    void safe_action_command_publishing_disabled_by_default() {
+
+        // when
+        wrapperFactory.wrap(counter1).findSelf();
+        interactionService.nextInteraction();
+
+        // then
+        Optional<? extends CommandLogEntry> mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted();
+        assertThat(mostRecentCompleted).isEmpty();
+    }
+
+    @Test
+    void safe_action_command_publishing_enabled() {
+
+        // given
+        causewayConfiguration.getExtensions().getCommandLog().setSafeActionCommandPublishing(true);
+
+        // when
+        wrapperFactory.wrap(counter1).findSelf();
+        interactionService.nextInteraction();
+
+        // then
+        Optional<? extends CommandLogEntry> mostRecentCompleted = commandLogEntryRepository.findMostRecentCompleted();
+        assertThat(mostRecentCompleted).isPresent();
+
+        CommandLogEntry commandLogEntry = mostRecentCompleted.get();
+        assertThat(commandLogEntry.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#findSelf");
+        assertThat(commandLogEntry.getCommandDto()).isNotNull();
+        assertThat(commandLogEntry.getCommandDto().getMember()).isInstanceOf(ActionDto.class);
+        assertThat(commandLogEntry.getResult()).isEqualTo(bookmarkService.bookmarkForElseFail(counter1));
+
+        val exportDto = CommandDtoUtils.CommandExportDto.of(
+                commandLogEntry.getCommandDto(),
+                commandLogEntry.getResult());
+        assertThat(exportDto.getReturnedObject().getLogicalTypeName()).isEqualTo("commandlog.test.Counter");
+    }
+
+    @Test
+    void safe_action_with_explicit_command_publishing_is_not_duplicated() {
+
+        // given
+        causewayConfiguration.getExtensions().getCommandLog().setSafeActionCommandPublishing(true);
+
+        // when
+        wrapperFactory.wrap(counter1).findSelfWithCommandPublishingEnabled();
+        interactionService.nextInteraction();
+
+        // then
+        List<? extends CommandLogEntry> entries = commandLogEntryRepository.findAll();
+        assertThat(entries).hasSize(1);
+        assertThat(entries.get(0).getLogicalMemberIdentifier())
+                .isEqualTo("commandlog.test.Counter#findSelfWithCommandPublishingEnabled");
+    }
+
+    @Test
+    void safe_action_command_publishing_logs_null_list_and_scalar_results() {
+
+        // given
+        causewayConfiguration.getExtensions().getCommandLog().setSafeActionCommandPublishing(true);
+
+        // when
+        wrapperFactory.wrap(counter1).findNull();
+        interactionService.nextInteraction();
+        wrapperFactory.wrap(counter1).findSelfAsList();
+        interactionService.nextInteraction();
+        wrapperFactory.wrap(counter1).findNameAsScalar();
+        interactionService.nextInteraction();
+
+        // then
+        List<? extends CommandLogEntry> entries = commandLogEntryRepository.findAll();
+        assertThat(entries)
+                .extracting(CommandLogEntry::getLogicalMemberIdentifier)
+                .contains(
+                        "commandlog.test.Counter#findNull",
+                        "commandlog.test.Counter#findSelfAsList",
+                        "commandlog.test.Counter#findNameAsScalar");
+    }
+
 
 
     @Test
@@ -519,6 +601,7 @@ void test_all_the_repository_methods() {
     @Inject CounterRepository counterRepository;
     @Inject WrapperFactory wrapperFactory;
     @Inject BookmarkService bookmarkService;
+    @Inject CausewayConfiguration causewayConfiguration;
     @Inject CausewayBeanTypeRegistry causewayBeanTypeRegistry;
 
 }
diff --git a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/model/Counter.java b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/model/Counter.java
index b76c915d84f..2a292852e93 100644
--- a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/model/Counter.java
+++ b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/model/Counter.java
@@ -20,6 +20,8 @@
 
 package org.apache.causeway.extensions.commandlog.applib.integtest.model;
 
+import java.util.List;
+
 import javax.inject.Named;
 
 import org.apache.causeway.applib.annotation.Action;
@@ -28,6 +30,7 @@
 import org.apache.causeway.applib.annotation.Nature;
 import org.apache.causeway.applib.annotation.Property;
 import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.SemanticsOf;
 
 @Named("commandlog.test.Counter")
 @DomainObject(nature = Nature.ENTITY)
@@ -60,6 +63,31 @@ public Counter bumpUsingDeclaredActionWithCommandPublishingDisabled() {
         return doBump();
     }
 
+    @Action(semantics = SemanticsOf.SAFE)
+    public Counter findSelf() {
+        return this;
+    }
+
+    @Action(semantics = SemanticsOf.SAFE, commandPublishing = Publishing.ENABLED)
+    public Counter findSelfWithCommandPublishingEnabled() {
+        return this;
+    }
+
+    @Action(semantics = SemanticsOf.SAFE)
+    public Counter findNull() {
+        return null;
+    }
+
+    @Action(semantics = SemanticsOf.SAFE)
+    public List<Counter> findSelfAsList() {
+        return List.of(this);
+    }
+
+    @Action(semantics = SemanticsOf.SAFE)
+    public String findNameAsScalar() {
+        return getName();
+    }
+
     Counter doBump() {
         if (getNum() == null) {
             setNum(1L);
diff --git a/openspec/changes/log-safe-single-result-finders/design.md b/openspec/changes/log-safe-single-result-finders/design.md
index e24f0f8890f..edd505f8f3c 100644
--- a/openspec/changes/log-safe-single-result-finders/design.md
+++ b/openspec/changes/log-safe-single-result-finders/design.md
@@ -5,54 +5,58 @@ Replay result mapping already lets applications map recorded returned object boo
 The missing link is that safe finder actions are normally not command logged, so a recording can show a later command against an object without also recording the safe action that found that object.
 
 The change crosses command publishing, command logging, export, and replay because the framework must capture the finder invocation as a command log entry and then allow the existing export and replay mapping flows to process it.
-The behavior must be opt-in because safe actions can be frequent and many safe actions return lists, scalar values, or other results that are not useful for replay result remapping.
+The behavior must be opt-in because safe actions can be frequent.
 
 ## Goals / Non-Goals
 
 **Goals:**
 
-- Provide a configuration property that enables command logging for safe actions that return exactly one domain object.
-- Preserve the default behavior where safe finder actions are not logged unless explicitly configured.
-- Reuse the existing `CommandDto`, `CommandLogEntry#getResult()`, command export YAML, and replay result mapping SPI concepts.
-- Limit the added logging to safe action invocations whose result can be represented as a single returned object bookmark.
-- Ensure replay can execute the logged safe finder command and notify the mapping SPI with recorded and actual finder result bookmarks.
+- Provide a configuration property that enables command publishing for safe actions.
+- Preserve the default behavior where safe actions are not command logged unless explicitly configured.
+- Reuse the existing `CommandPublishingFacet`, `CommandDto`, `CommandLogEntry#getResult()`, command export YAML, and replay result mapping SPI concepts.
+- Ensure replay can execute the logged safe action command and notify the mapping SPI when recorded and actual result bookmarks are available.
 
 **Non-Goals:**
 
-- Do not log all safe actions by default.
-- Do not add support for replay mapping list-valued finder results in this change.
-- Do not change command export file format beyond carrying safe finder entries through the existing command export shape.
+- Do not log safe actions by default.
+- Do not inspect or constrain the safe action result shape as part of deciding whether to command publish it.
+- Do not add support for replay mapping list-valued results in this change.
+- Do not change command export file format beyond carrying safe action entries through the existing command export shape.
 - Do not require applications to rewrite existing replay mapping listener implementations.
 
 ## Decisions
 
 ### Decision: Use an opt-in configuration property
 
-The new behavior will be guarded by a boolean configuration property in the Causeway command logging or command publishing configuration area.
+The new behavior will be guarded by `causeway.extensions.command-log.safe-action-command-publishing`.
 The default value will be `false` to avoid increasing command log volume or changing production behavior unexpectedly.
 
-Alternative considered: enable safe single-result finder logging automatically whenever command logging is enabled.
+Alternative considered: enable safe action command publishing automatically whenever command logging is enabled.
 That was rejected because many applications use safe actions heavily and may not want finder calls in the audit log outside regression recording contexts.
 
-### Decision: Classify eligibility by action semantics and bookmarkable result shape
+### Decision: Implement through command publishing facets
 
-The framework will consider an invocation eligible only when the action has safe semantics and the completed result resolves to a single bookmarkable domain object.
-List, collection, array, optional-empty, scalar, void, and other non-single-object results will not be logged by this feature.
+The implementation will adjust the action command publishing facet factory so safe actions receive a command publishing facet whose enabled state is controlled by the new command log property.
+This keeps command logging on the normal path: action execution prepares the command for publishing when its facet is enabled, and `CommandSubscriberForCommandLog` persists the command on the standard ready/start/completed notifications.
+Explicit `@Action(commandPublishing = DISABLED)` remains a local opt-out, and explicitly enabled actions are not duplicated because they already use the normal enabled facet.
 
-Alternative considered: use naming conventions such as `find*` to identify finders.
-That was rejected because Causeway already models safe semantics and result cardinality more reliably than action names.
+Alternative considered: create command log entries during `CommandSubscriberForCommandLog#onCompleted` for safe actions that were not published earlier.
+That was rejected because it still required special command-log subscriber logic instead of using the existing command publishing facet model.
 
-### Decision: Reuse command log entries and returned object bookmarks
+Alternative considered: force safe finder commands through ready/start publishing from `MemberExecutorServiceDefault` after the action result was known.
+That was rejected because it was a bolt-on around the normal logging boundary and duplicated command publishing concerns outside the facet and subscriber pipeline.
 
-Eligible safe finder invocations will be represented as normal command log entries with a command DTO and a returned object bookmark.
-This allows command export YAML and replay mapping notifications to use the existing returned object metadata pathway.
+### Decision: Do not use result shape for eligibility
 
-Alternative considered: create a separate finder recording artifact outside command log entries.
-That was rejected because regression replay already consumes command log entries and the replay mapping SPI already understands recorded and actual returned object bookmarks.
+The framework will consider safe action semantics and configuration only when deciding whether to command publish through this feature.
+Returned object bookmarks will still be captured when the existing command result handling can capture them, but null, list, scalar, or otherwise non-bookmarkable results do not prevent the command entry from being recorded.
+
+Alternative considered: log only safe actions that return a single domain object.
+That was rejected for this change because result shape may not be available at facet installation time and the command publishing decision belongs before invocation.
 
 ### Decision: Keep replay mapping listener contracts unchanged
 
-Replay of safe finder entries will use the existing result mapping notification when the recorded finder entry and replay result both have bookmarks.
+Replay of safe action entries will use the existing result mapping notification when the recorded entry and replay result both have bookmarks.
 No new listener method is required.
 
 Alternative considered: add a finder-specific replay mapping callback.
@@ -61,18 +65,17 @@ That was rejected because the existing callback already expresses the required m
 ## Risks / Trade-offs
 
 - Increased command log volume when enabled → Keep the property disabled by default and document that it is intended for regression recording environments.
-- Ambiguous result shapes such as `Optional<T>` or framework wrappers → Define tests for accepted and rejected shapes, and only log when the framework can produce one returned object bookmark unambiguously.
 - Safe action replay might have side effects in poorly behaved applications → Limit the feature to opt-in regression recording and rely on the existing semantic contract that safe actions do not mutate state.
-- Duplicate logging if a safe action is explicitly command-published already → Prefer existing explicit command publishing behavior and avoid creating an additional entry for the same interaction.
+- Safe actions without returned object bookmarks will be recorded but cannot contribute result mappings → Preserve them as replayable dotted-line steps and only notify mapping listeners when bookmark data is available.
+- Applications may want selected safe actions excluded → Continue to respect explicit command publishing annotations such as `DISABLED`.
 
 ## Migration Plan
 
 No data migration is required.
 Existing applications keep current behavior because the new property defaults to disabled.
 Regression test applications can enable the property in their test configuration before recording command exports.
-Rollback consists of disabling the property and ignoring any previously recorded safe finder entries in newly generated recordings.
+Rollback consists of disabling the property and ignoring any previously recorded safe action entries in newly generated recordings.
 
 ## Open Questions
 
-- Confirm the final property namespace and name during implementation to match existing Causeway configuration conventions.
-- Confirm whether `Optional<T>` with a present bookmarkable object should be treated as a single-result finder or excluded from the first implementation for simplicity.
+- Confirm whether the final property name `causeway.extensions.command-log.safe-action-command-publishing` matches existing Causeway configuration conventions.
diff --git a/openspec/changes/log-safe-single-result-finders/proposal.md b/openspec/changes/log-safe-single-result-finders/proposal.md
index 13592c8a9c9..488657c82cf 100644
--- a/openspec/changes/log-safe-single-result-finders/proposal.md
+++ b/openspec/changes/log-safe-single-result-finders/proposal.md
@@ -5,24 +5,24 @@ Today command logging normally captures only state-changing commands, so replay
 
 ## What Changes
 
-- Add an opt-in configuration property that enables command logging for safe actions that return a single domain object.
-- Treat enabled safe single-result finder invocations as command-log entries with command DTO and returned object bookmark metadata, so they can be exported and replayed alongside state-changing commands.
+- Add an opt-in configuration property that enables command publishing for safe actions through the normal command publishing facet model.
+- Treat enabled safe action invocations as command-log entries with command DTO metadata, so they can be exported and replayed alongside state-changing commands.
+- Store returned object bookmark metadata when the safe action result provides one.
 - Keep existing default behavior unchanged when the property is disabled.
-- Exclude safe actions that return lists, collections, void, scalar values, or other non-single-domain-object results from this additional logging path.
 - Preserve existing command export and replay behavior so recorded finder results can feed the replay mapping SPI in the same way as other commands with returned object metadata.
 
 ## Capabilities
 
 ### New Capabilities
-- `safe-single-result-finder-command-logging`: Opt-in command logging for safe actions that return a single domain object, enabling exported regression recordings to include finder steps and their returned object bookmarks.
+- `safe-single-result-finder-command-logging`: Opt-in command publishing for safe actions, enabling exported regression recordings to include finder steps and any returned object bookmarks that the framework can capture.
 
 ### Modified Capabilities
-- `command-export-yaml-result`: Exported command YAML includes returned object metadata for safe single-result finder command log entries when the opt-in logging property is enabled.
-- `command-replay-result-mapping`: Replay result mapping notifications include replayed safe single-result finder entries when both recorded and actual result bookmarks are available.
+- `command-export-yaml-result`: Exported command YAML includes returned object metadata for safe action command log entries when a result bookmark is available.
+- `command-replay-result-mapping`: Replay result mapping notifications include replayed safe action entries when both recorded and actual result bookmarks are available.
 
 ## Impact
 
-- Affects command creation, command publishing, and command log subscriber behavior for safe action invocations.
-- Adds framework configuration under the command logging or command publishing settings namespace.
+- Affects command publishing facet creation for safe action invocations.
+- Adds framework configuration under the command log settings namespace.
 - Affects command log export and replay regression tests by allowing finder steps to appear in exported command streams.
-- Requires unit and integration tests covering disabled-by-default behavior, enabled finder logging, excluded safe result shapes, export metadata, and replay mapping notifications.
+- Requires unit and integration tests covering disabled-by-default behavior, enabled safe action logging, explicit command publishing, export metadata, and replay mapping notifications.
diff --git a/openspec/changes/log-safe-single-result-finders/specs/command-export-yaml-result/spec.md b/openspec/changes/log-safe-single-result-finders/specs/command-export-yaml-result/spec.md
index 398d666ceb2..5dce36988d5 100644
--- a/openspec/changes/log-safe-single-result-finders/specs/command-export-yaml-result/spec.md
+++ b/openspec/changes/log-safe-single-result-finders/specs/command-export-yaml-result/spec.md
@@ -1,15 +1,15 @@
 ## ADDED Requirements
 
-### Requirement: Exported command YAML includes logged safe finder returned object metadata
-The system SHALL export safe single-result finder command log entries using the existing command export YAML shape when safe finder logging created those entries.
-When a logged safe finder entry has a non-null returned object bookmark, the exported YAML SHALL include returned object metadata containing the bookmark logical type name and identifier.
-The system SHALL preserve compatibility with existing command replay import for exported safe finder entries.
+### Requirement: Exported command YAML includes logged safe action returned object metadata
+The system SHALL export safe action command log entries using the existing command export YAML shape when safe action command publishing created those entries.
+When a logged safe action entry has a non-null returned object bookmark, the exported YAML SHALL include returned object metadata containing the bookmark logical type name and identifier.
+The system SHALL preserve compatibility with existing command replay import for exported safe action entries.
 
-#### Scenario: Export logged safe finder command with returned object
-- **GIVEN** safe single-result finder command logging is enabled
-- **AND** a safe finder command log entry has result bookmark `demoCustomer:1`
+#### Scenario: Export logged safe action command with returned object
+- **GIVEN** safe action command publishing is enabled
+- **AND** a safe action command log entry has result bookmark `demoCustomer:1`
 - **WHEN** the command is selected for export
-- **THEN** the generated YAML contains the embedded command DTO for the safe finder invocation
+- **THEN** the generated YAML contains the embedded command DTO for the safe action invocation
 - **AND** the generated YAML contains returned object metadata with logical type name `demoCustomer` and id `1`
 
 #### Scenario: Export command stream containing finder and state-changing commands
diff --git a/openspec/changes/log-safe-single-result-finders/specs/command-replay-result-mapping/spec.md b/openspec/changes/log-safe-single-result-finders/specs/command-replay-result-mapping/spec.md
index 138bcc75280..003626f6eca 100644
--- a/openspec/changes/log-safe-single-result-finders/specs/command-replay-result-mapping/spec.md
+++ b/openspec/changes/log-safe-single-result-finders/specs/command-replay-result-mapping/spec.md
@@ -1,24 +1,24 @@
 ## ADDED Requirements
 
-### Requirement: Replay result mapping includes logged safe finder results
-The system SHALL replay imported safe single-result finder command log entries as replayable commands when they are present in the imported command stream.
-After a logged safe finder replay succeeds, the system SHALL notify the command replay mapping SPI when the imported command log entry has a recorded returned object bookmark and replay execution returns an actual result bookmark.
-The notification SHALL use the existing replay result mapping SPI contract and SHALL include the recorded finder result bookmark and the actual replay finder result bookmark.
-The system MUST NOT notify the SPI for a logged safe finder replay when either the recorded or actual result bookmark is unavailable.
+### Requirement: Replay result mapping includes logged safe action results
+The system SHALL replay imported safe action command log entries as replayable commands when they are present in the imported command stream.
+After a logged safe action replay succeeds, the system SHALL notify the command replay mapping SPI when the imported command log entry has a recorded returned object bookmark and replay execution returns an actual result bookmark.
+The notification SHALL use the existing replay result mapping SPI contract and SHALL include the recorded result bookmark and the actual replay result bookmark.
+The system MUST NOT notify the SPI for a logged safe action replay when either the recorded or actual result bookmark is unavailable.
 
 #### Scenario: Replayed safe finder maps recorded result to actual result
 - **GIVEN** an imported safe finder command log entry has recorded result bookmark `demoCustomer:1`
 - **WHEN** command replay executes that safe finder and obtains actual result bookmark `demoCustomer:2`
 - **THEN** the system notifies the command replay mapping SPI with recorded bookmark `demoCustomer:1` and actual bookmark `demoCustomer:2`
 
-#### Scenario: Replayed safe finder result is unavailable
-- **GIVEN** an imported safe finder command log entry has recorded result bookmark `demoCustomer:1`
-- **WHEN** command replay executes that safe finder and obtains no actual result bookmark
-- **THEN** the system does not notify the command replay mapping SPI for that finder result
+#### Scenario: Replayed safe action result is unavailable
+- **GIVEN** an imported safe action command log entry has no recorded result bookmark
+- **WHEN** command replay executes that safe action
+- **THEN** the system does not notify the command replay mapping SPI for that action result
 
-### Requirement: Safe finder result mappings can remap later replay inputs
-The system SHALL make replay result mappings produced by logged safe finder commands available to the same replay input remapping flow used by other replayed commands.
-When a later replayed command target or reference parameter uses a recorded bookmark that was mapped by an earlier safe finder replay, the system SHALL allow the command replay mapping SPI to replace that recorded bookmark with the actual replay bookmark.
+### Requirement: Safe action result mappings can remap later replay inputs
+The system SHALL make replay result mappings produced by logged safe action commands available to the same replay input remapping flow used by other replayed commands.
+When a later replayed command target or reference parameter uses a recorded bookmark that was mapped by an earlier safe action replay, the system SHALL allow the command replay mapping SPI to replace that recorded bookmark with the actual replay bookmark.
 
 #### Scenario: Later replay command target is remapped from finder result
 - **GIVEN** replay of a safe finder command mapped recorded bookmark `demoCustomer:1` to actual bookmark `demoCustomer:2`
diff --git a/openspec/changes/log-safe-single-result-finders/specs/safe-single-result-finder-command-logging/spec.md b/openspec/changes/log-safe-single-result-finders/specs/safe-single-result-finder-command-logging/spec.md
index c68d4b953a6..cc16d8e064d 100644
--- a/openspec/changes/log-safe-single-result-finders/specs/safe-single-result-finder-command-logging/spec.md
+++ b/openspec/changes/log-safe-single-result-finders/specs/safe-single-result-finder-command-logging/spec.md
@@ -1,61 +1,54 @@
 ## ADDED Requirements
 
-### Requirement: Safe single-result finder command logging is configurable
-The system SHALL provide a configuration property that enables command logging for safe action invocations that return a single domain object.
+### Requirement: Safe action command publishing is configurable
+The system SHALL provide a configuration property that enables command publishing for safe action invocations.
 The system SHALL leave this property disabled by default.
-When the property is disabled, safe action invocations MUST retain the existing behavior and MUST NOT be command logged solely because they return a single domain object.
+When the property is disabled, safe action invocations MUST retain the existing behavior and MUST NOT be command logged solely because they are safe actions.
+The system SHALL apply this behavior through the command publishing facet model used by normal command logging.
 
-#### Scenario: Safe finder logging is disabled by default
-- **WHEN** an application does not configure safe single-result finder command logging
-- **AND** a user invokes a safe action that returns one domain object
+#### Scenario: Safe action command publishing is disabled by default
+- **WHEN** an application does not configure safe action command publishing
+- **AND** a user invokes a safe action
 - **THEN** the system does not create a command log entry solely for that safe action invocation
 
-#### Scenario: Safe finder logging is enabled by property
-- **GIVEN** safe single-result finder command logging is configured as enabled
-- **WHEN** a user invokes a safe action that returns one domain object
-- **THEN** the system creates a command log entry for that safe action invocation
+#### Scenario: Safe action command publishing is enabled by property
+- **GIVEN** safe action command publishing is configured as enabled
+- **WHEN** a user invokes a safe action
+- **THEN** the system creates a command log entry for that safe action invocation through the normal command publishing flow
 
-### Requirement: Logged safe finder entries include command DTO and result bookmark
-When safe single-result finder command logging is enabled, the system SHALL store a command DTO for each eligible safe finder invocation.
-When the eligible safe finder returns a bookmarkable domain object, the system SHALL store that returned object's bookmark as the command log entry result.
+### Requirement: Logged safe action entries include command DTO and available result bookmark
+When safe action command publishing is enabled, the system SHALL store a command DTO for each eligible safe action invocation.
+When the safe action returns a bookmarkable domain object and the framework captures a returned object bookmark, the system SHALL store that returned object's bookmark as the command log entry result.
+When the safe action result does not produce a returned object bookmark, the system SHALL still store the safe action command log entry without a result bookmark.
 The command log entry SHALL be suitable for command export and command replay.
 
-#### Scenario: Safe finder returns one bookmarkable object
-- **GIVEN** safe single-result finder command logging is enabled
+#### Scenario: Safe action returns one bookmarkable object
+- **GIVEN** safe action command publishing is enabled
 - **WHEN** a user invokes a safe action with command target and parameters that returns bookmark `demoCustomer:1`
 - **THEN** the system creates a command log entry containing a command DTO for that invocation
 - **AND** the command log entry result is bookmark `demoCustomer:1`
 
-#### Scenario: Safe finder returns null
-- **GIVEN** safe single-result finder command logging is enabled
-- **WHEN** a user invokes a safe action that returns null
-- **THEN** the system creates no command log entry for that invocation under the safe single-result finder logging feature
-
-### Requirement: Safe finder logging excludes non-single-domain-object results
-When safe single-result finder command logging is enabled, the system MUST NOT create safe finder command log entries for safe actions whose result is a list, collection, array, void, scalar value, or otherwise not representable as exactly one returned object bookmark.
-The system MUST NOT log unsafe, idempotent, or non-idempotent actions through this safe finder logging path because those actions continue to use existing command publishing behavior.
-
-#### Scenario: Safe action returns a list
-- **GIVEN** safe single-result finder command logging is enabled
-- **WHEN** a user invokes a safe action that returns a list of domain objects
-- **THEN** the system creates no command log entry for that invocation under the safe single-result finder logging feature
+#### Scenario: Safe action returns a result without a bookmark
+- **GIVEN** safe action command publishing is enabled
+- **WHEN** a user invokes a safe action whose result does not produce a returned object bookmark
+- **THEN** the system creates a command log entry containing a command DTO for that invocation
+- **AND** the command log entry has no result bookmark
 
-#### Scenario: Safe action returns a scalar value
-- **GIVEN** safe single-result finder command logging is enabled
-- **WHEN** a user invokes a safe action that returns a scalar value
-- **THEN** the system creates no command log entry for that invocation under the safe single-result finder logging feature
+### Requirement: Safe action command publishing does not affect state-changing action policy
+When safe action command publishing is enabled, the system MUST NOT use this property to determine whether idempotent or non-idempotent actions are command logged.
+Idempotent and non-idempotent actions SHALL continue to use existing command publishing behavior.
 
 #### Scenario: State-changing action is invoked
-- **GIVEN** safe single-result finder command logging is enabled
+- **GIVEN** safe action command publishing is enabled
 - **WHEN** a user invokes an idempotent or non-idempotent action
-- **THEN** the safe finder logging path does not determine whether the action is command logged
+- **THEN** the safe action command publishing property does not determine whether the action is command logged
 - **AND** the action continues to follow existing command publishing rules
 
 ### Requirement: Explicit command publishing is not duplicated
-When an action invocation is already command logged through existing command publishing rules, the system MUST NOT create a duplicate command log entry for the same invocation through safe single-result finder command logging.
+When an action invocation is already command logged through existing command publishing rules, the system MUST NOT create a duplicate command log entry for the same invocation through safe action command publishing.
 
 #### Scenario: Safe action is explicitly command-published
-- **GIVEN** safe single-result finder command logging is enabled
-- **AND** a safe single-result action is explicitly configured for command publishing
+- **GIVEN** safe action command publishing is enabled
+- **AND** a safe action is explicitly configured for command publishing
 - **WHEN** a user invokes that action
 - **THEN** the system stores at most one command log entry for that invocation
diff --git a/openspec/changes/log-safe-single-result-finders/tasks.md b/openspec/changes/log-safe-single-result-finders/tasks.md
index a4ec266ce8b..59b95136f75 100644
--- a/openspec/changes/log-safe-single-result-finders/tasks.md
+++ b/openspec/changes/log-safe-single-result-finders/tasks.md
@@ -1,44 +1,44 @@
 ## 1. Discovery and Configuration
 
-- [ ] 1.1 Locate the existing command publishing, command DTO creation, and command log subscriber paths for action invocations.
-- [ ] 1.2 Identify the existing Causeway configuration namespace that should own the safe single-result finder logging property.
-- [ ] 1.3 Add the disabled-by-default boolean configuration property and bind it into the relevant runtime service or subscriber.
-- [ ] 1.4 Add documentation metadata or configuration property descriptors if this repository maintains generated config docs.
+- [x] 1.1 Locate the existing command publishing, command DTO creation, and command log subscriber paths for action invocations.
+- [x] 1.2 Identify the existing Causeway configuration namespace that should own the safe action command publishing property.
+- [x] 1.3 Add the disabled-by-default boolean configuration property and bind it into the relevant runtime service or subscriber.
+- [x] 1.4 Add documentation metadata or configuration property descriptors if this repository maintains generated config docs.
 
 ## 2. Safe Finder Eligibility
 
-- [ ] 2.1 Implement eligibility detection for safe action invocations when the new property is enabled.
-- [ ] 2.2 Detect whether the completed result is exactly one bookmarkable domain object.
-- [ ] 2.3 Exclude null, list, collection, array, void, scalar, and otherwise non-single-bookmarkable results.
-- [ ] 2.4 Ensure idempotent and non-idempotent actions continue to follow existing command publishing behavior rather than the safe finder logging path.
-- [ ] 2.5 Prevent duplicate command log entries when a safe action is already explicitly command-published.
+- [x] 2.1 Implement command publishing facet eligibility for safe action invocations when the new property is enabled.
+- [x] 2.2 Avoid using result shape as part of safe action command publishing eligibility.
+- [x] 2.3 Allow null, list, collection, array, void, scalar, and otherwise non-bookmarkable results to be command logged when the safe action publishing property is enabled.
+- [x] 2.4 Ensure idempotent and non-idempotent actions continue to follow existing command publishing behavior rather than the safe action command publishing property.
+- [x] 2.5 Prevent duplicate command log entries when a safe action is already explicitly command-published.
 
 ## 3. Command Log Entry Creation
 
-- [ ] 3.1 Create or adapt the command DTO for eligible safe finder invocations so it can be persisted and replayed.
-- [ ] 3.2 Persist an eligible safe finder invocation as a command log entry when the property is enabled.
-- [ ] 3.3 Store the returned object's bookmark as the command log entry result.
-- [ ] 3.4 Keep disabled-by-default behavior unchanged for safe actions when the property is absent or false.
+- [x] 3.1 Create or adapt the command DTO for eligible safe action invocations so it can be persisted and replayed.
+- [x] 3.2 Persist an eligible safe action invocation as a command log entry when the property is enabled.
+- [x] 3.3 Store the returned object's bookmark as the command log entry result when available.
+- [x] 3.4 Keep disabled-by-default behavior unchanged for safe actions when the property is absent or false.
 
 ## 4. Export and Replay Integration
 
-- [ ] 4.1 Verify command export includes logged safe finder entries in replay order.
-- [ ] 4.2 Verify exported safe finder entries include returned object metadata when a result bookmark is present.
-- [ ] 4.3 Verify replay executes imported safe finder command entries.
-- [ ] 4.4 Verify replay result mapping notifications are emitted for safe finder entries with recorded and actual result bookmarks.
-- [ ] 4.5 Verify later replay command targets and reference parameters can be remapped using mappings learned from safe finder replay results.
+- [x] 4.1 Verify command export includes logged safe finder entries in replay order.
+- [x] 4.2 Verify exported safe finder entries include returned object metadata when a result bookmark is present.
+- [x] 4.3 Verify replay executes imported safe finder command entries.
+- [x] 4.4 Verify replay result mapping notifications are emitted for safe finder entries with recorded and actual result bookmarks.
+- [x] 4.5 Verify later replay command targets and reference parameters can be remapped using mappings learned from safe finder replay results.
 
 ## 5. Tests and Documentation
 
-- [ ] 5.1 Add unit tests for the new configuration property default and enabled values.
-- [ ] 5.2 Add unit tests for safe finder eligibility and excluded result shapes.
-- [ ] 5.3 Add tests ensuring explicitly command-published safe actions are not duplicated.
-- [ ] 5.4 Add export tests covering safe finder command entries and returned object metadata.
-- [ ] 5.5 Add replay tests covering finder result mapping notifications and subsequent input remapping.
-- [ ] 5.6 Update user-facing documentation for regression recording usage and the new configuration property.
+- [x] 5.1 Add unit tests for the new configuration property default and enabled values.
+- [x] 5.2 Add tests for safe action command publishing eligibility and result shapes without exclusion.
+- [x] 5.3 Add tests ensuring explicitly command-published safe actions are not duplicated.
+- [x] 5.4 Add export tests covering safe finder command entries and returned object metadata.
+- [x] 5.5 Add replay tests covering finder result mapping notifications and subsequent input remapping.
+- [x] 5.6 Update user-facing documentation for regression recording usage and the new configuration property.
 
 ## 6. Validation
 
-- [ ] 6.1 Run the relevant commandlog applib unit tests.
-- [ ] 6.2 Run the relevant commandlog persistence integration tests for JDO and JPA if affected.
-- [ ] 6.3 Run OpenSpec validation for `log-safe-single-result-finders`.
+- [x] 6.1 Run the relevant commandlog applib unit tests.
+- [x] 6.2 Run the relevant commandlog persistence integration tests for JDO and JPA if affected.
+- [x] 6.3 Run OpenSpec validation for `log-safe-single-result-finders`.