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

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git


The following commit(s) were added to refs/heads/master by this push:
     new 4f075a46d CAY-2986 Cgen to run unconditionally
4f075a46d is described below

commit 4f075a46d79741aa3547e3898ab91600410b1c49
Author: Andrus Adamchik <[email protected]>
AuthorDate: Sat Aug 8 08:53:07 2026 -0400

    CAY-2986 Cgen to run unconditionally
    
    also updating MCP skill docs. There were discrepancies
---
 RELEASE-NOTES.txt                                  |   1 +
 UPGRADE.md                                         |   6 ++
 ai-plugin/references/mcp-tools.md                  |  18 ++--
 ai-plugin/skills/cayenne-cgen/SKILL.md             |  10 +-
 .../apache/cayenne/tools/CayenneGeneratorTask.java |  35 +++---
 .../org/apache/cayenne/gen/CgenConfiguration.java  |  26 ++---
 .../apache/cayenne/gen/ClassGenerationAction.java  | 119 ++++++++++++---------
 .../cayenne/gen/ClassGenerationActionTest.java     |  89 +++++++--------
 .../java/org/apache/cayenne/tools/CgenTask.java    |  27 +++--
 .../apache/cayenne/tools/CayenneGeneratorMojo.java |  30 +++---
 cayenne-mcp-server/PROTOCOL.md                     |   5 +-
 .../apache/cayenne/mcp/tools/cgen/CgenRunTool.java |  12 ---
 .../cgen/InstrumentedClassGenerationAction.java    |  40 ++-----
 .../apache/cayenne/mcp/tools/cgen/CgenRunIT.java   |  57 +++++-----
 .../_cayenne-guide/part6/maven-plugin.adoc         |   2 +-
 .../editor/datadomain/cgen/DataDomainCgenTab.java  |   1 -
 .../ui/project/editor/datamap/cgen/CgenPanel.java  |   2 -
 17 files changed, 243 insertions(+), 237 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index aa94aa8f3..117a6340e 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -32,6 +32,7 @@ CAY-2982 Modeler: when upgrading projects allow upgraders to 
send outcome messag
 CAY-2983 CayenneDataSource: Public-facing DataSource builder
 CAY-2984 Remove "DataChannel" injectable dependency
 CAY-2985 DataDomain init and structure cleanup
+CAY-2986 Cgen to run unconditionally
 
 Bug Fixes:
 
diff --git a/UPGRADE.md b/UPGRADE.md
index f6931d43a..15583e4a4 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -111,6 +111,12 @@ DataMaps, filters and listeners can still be added (and 
removed) after creation.
   - `setMaxIdQualifierSize(int)` — use the `cayenne.max_id_qualifier_size` 
property
     (`Constants.MAX_ID_QUALIFIER_SIZE_PROPERTY`).
 
+* Per [CAY-2986](https://issues.apache.org/jira/browse/CAY-2986) cgen now runs 
unconditionally. Previously it compared
+  the DataMap file mtime against the mtime of the generated classes and 
skipped generation when the classes looked
+  newer. That optimization saved very little (cgen is idempotent and fast) 
while regularly producing stale classes
+  after project upgrades or when switching between machines and branches. 
Consequences:
+  - The `force` flag is now a deprecated no-op — its former behavior is the 
only behavior.
+
 ## Upgrading to 5.0-M2
 
 * Per [CAY-2947](https://issues.apache.org/jira/browse/CAY-2947) the 
`cayenne-commitlog` artifact has been removed. Commit log support is now part 
of the
diff --git a/ai-plugin/references/mcp-tools.md 
b/ai-plugin/references/mcp-tools.md
index cad3c0c8a..9b628bf87 100644
--- a/ai-plugin/references/mcp-tools.md
+++ b/ai-plugin/references/mcp-tools.md
@@ -45,18 +45,20 @@ Runs Cayenne's class generator for one DataMap. Uses the 
`<cgen>` config block e
 
 ```json
 {
-  "status": "ok" | "error",
-  "summary": { "writtenCount": 3, "skippedCount": 12, "errorCount": 0 },
-  "resolvedConfig": { "destDir": "..." },
-  "writtenFiles": [{ "path": "...", "size": 1234 }],
-  "skippedFiles": [{ "path": "...", "reason": "up-to-date" }],
-  "errors": []
+  "status": "generated" | "up_to_date" | "validation_failed" | "error",
+  "summary": { "filesConsidered": 15, "filesWritten": 3 },
+  "files": [{ "path": "...", "kind": "entity_super", "sourceEntity": "Artist" 
}],
+  "resolved": { "destDir": "..." },
+  "warnings": [],
+  "validation": { "projectFound": true, "dataMapFound": true, 
"cgenConfigPresent": true,
+                  "destDirSpecified": true, "destDirWritable": true },
+  "error": null
 }
 ```
 
-Surface the `summary` to the user verbatim. List the first few `writtenFiles` 
paths; full list is informational. If `errors` is non-empty, those are blocking 
— show them.
+Surface the `summary` to the user verbatim. List the first few `files` paths; 
full list is informational.
 
-When the DataMap had no `<cgen>` block, the tool generated from a synthesized 
default config. The default destination is derived from the Maven layout 
(`src/main/resources` → `src/main/java`); for non-Maven layouts it falls back 
to the DataMap's own directory, so check `resolvedConfig.destDir` and confirm 
with the user if it looks wrong. Offer to persist a `<cgen>` block (via 
`cayenne-modeling` or the Modeler) if they want to customize destination, 
templates, or filtering.
+When the DataMap had no `<cgen>` block, the tool generated from a synthesized 
default config. The default destination is derived from the Maven layout 
(`src/main/resources` → `src/main/java`); for non-Maven layouts it falls back 
to the DataMap's own directory, so check `resolved.destDir` and confirm with 
the user if it looks wrong. Offer to persist a `<cgen>` block (via 
`cayenne-modeling` or the Modeler) if they want to customize destination, 
templates, or filtering.
 
 **Failure modes:**
 
diff --git a/ai-plugin/skills/cayenne-cgen/SKILL.md 
b/ai-plugin/skills/cayenne-cgen/SKILL.md
index 73b833ee9..0997708da 100644
--- a/ai-plugin/skills/cayenne-cgen/SKILL.md
+++ b/ai-plugin/skills/cayenne-cgen/SKILL.md
@@ -59,12 +59,12 @@ If the tool is not available (MCP server not registered), 
surface `cayenne-mcp-s
 
 The tool returns structured JSON. Report:
 
-- `summary.writtenCount`, `summary.skippedCount`, `summary.errorCount` 
verbatim — these are the headline.
-- The first few entries in `writtenFiles` (relative paths). Full list is 
informational; offer to dump it if the user asks.
-- Any entries in `errors` — these are blocking. Read the messages and explain 
in user terms (a missing entity class name, a bad template path, an invalid 
`<destDir>`, etc.).
-- `resolvedConfig.destDir` — the absolute output directory. If the DataMap had 
no `<cgen>` block, the tool generated from a synthesized default; report this 
destination so the user can confirm it's right, and offer to persist a `<cgen>` 
block (via `cayenne-modeling`) if they want to customize destination, 
templates, or entity filtering.
+- `summary.filesConsidered` and `summary.filesWritten` verbatim — these are 
the headline.
+- The first few entries in `files` (relative paths). Full list is 
informational; offer to dump it if the user asks.
+- `error`, when non-null — this is blocking. Read the message and explain in 
user terms (a missing entity class name, a bad template path, an invalid 
`<destDir>`, etc.).
+- `resolved.destDir` — the absolute output directory. If the DataMap had no 
`<cgen>` block, the tool generated from a synthesized default; report this 
destination so the user can confirm it's right, and offer to persist a `<cgen>` 
block (via `cayenne-modeling`) if they want to customize destination, 
templates, or entity filtering.
 
-If `writtenCount` is 0 and `skippedCount` covers everything, say so — it means 
everything is already up-to-date and no work was needed.
+cgen always runs in full, but rewrites a file only when the generated contents 
differ from what is on disk. So `files` is exactly what changed, and a `status` 
of `up_to_date` means the Java was already in sync with the model — say so 
plainly rather than implying work was skipped.
 
 ## Step 4 — Next steps
 
diff --git 
a/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java 
b/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
index be63d2ce2..cc063ab60 100644
--- 
a/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
+++ 
b/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
@@ -18,27 +18,27 @@
  ****************************************************************/
 package org.apache.cayenne.tools;
 
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
 import org.apache.cayenne.configuration.xml.DataChannelMetaData;
 import org.apache.cayenne.dbsync.filter.NamePatternMatcher;
 import org.apache.cayenne.dbsync.reverse.configuration.ToolsModule;
 import org.apache.cayenne.di.Injector;
 import org.apache.cayenne.gen.ArtifactsGenerationMode;
-import org.apache.cayenne.gen.CgenConfiguration;
 import org.apache.cayenne.gen.CgenConfigList;
+import org.apache.cayenne.gen.CgenConfiguration;
+import org.apache.cayenne.gen.CgenTemplate;
 import org.apache.cayenne.gen.ClassGenerationAction;
 import org.apache.cayenne.gen.ClassGenerationActionFactory;
-import org.apache.cayenne.gen.CgenTemplate;
 import org.apache.cayenne.gen.TemplateType;
 import org.apache.cayenne.map.DataMap;
 import org.apache.tools.ant.BuildException;
 import org.apache.tools.ant.types.Path;
 import org.slf4j.LoggerFactory;
 
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 /**
  * An Ant task to perform class generation based on CayenneDataMap.
  *
@@ -73,11 +73,6 @@ public class CayenneGeneratorTask extends CayenneTask {
     protected Boolean usepkgpath;
     protected Boolean createpropertynames;
 
-    /**
-     * @since 4.1
-     */
-    private boolean force;
-
     private boolean useConfigFromDataMap;
 
     private transient Injector injector;
@@ -127,11 +122,6 @@ public class CayenneGeneratorTask extends CayenneTask {
                 CayenneGeneratorEmbeddableFilterAction filterEmbeddableAction 
= new CayenneGeneratorEmbeddableFilterAction();
                 
filterEmbeddableAction.setNameFilter(NamePatternMatcher.build(logger, null, 
excludeEmbeddablesPattern));
                 generatorAction.setLogger(logger);
-                if (force) {
-                    // will (re-)generate all files
-                    generatorAction.getCgenConfiguration().setForce(true);
-                }
-                
generatorAction.getCgenConfiguration().setTimestamp(map.lastModified());
                 if (!hasConfig() && useConfigFromDataMap) {
                     generatorAction.prepareArtifacts();
                 } else {
@@ -161,7 +151,7 @@ public class CayenneGeneratorTask extends CayenneTask {
                 makepairs != null || mode != null || outputPattern != null || 
overwrite != null || superpkg != null ||
                 supertemplate != null || template != null || 
embeddabletemplate != null || embeddablesupertemplate != null ||
                 usepkgpath != null || createpropertynames != null || 
datamaptemplate != null ||
-                datamapsupertemplate != null || createpkproperties != null || 
force || externaltoolconfig != null;
+                datamapsupertemplate != null || createpkproperties != null || 
externaltoolconfig != null;
     }
 
     List<CgenConfiguration> buildConfigurations(DataMap dataMap) {
@@ -390,8 +380,15 @@ public class CayenneGeneratorTask extends CayenneTask {
         this.createpkproperties = createpkproperties;
     }
 
+    /**
+     * does nothing
+     *
+     * @param force not used
+     * @deprecated cgen runs unconditionally
+     */
+    @Deprecated(since = "5.0", forRemoval = true)
     public void setForce(boolean force) {
-        this.force = force;
+        log("'force' is deprecated and ignored. cgen always regenerates 
classes.");
     }
 
     /**
diff --git 
a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java 
b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
index 6d2fd810e..b19121edd 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
@@ -86,11 +86,9 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
      * @since 5.0 renamed from querySuperTemplate
      */
     private CgenTemplate dataMapSuperTemplate;
-    private long timestamp;
     private String outputPattern;
     private String encoding;
     private boolean createPropertyNames;
-    private boolean force; // force run generator
     /**
      * @since 4.1
      */
@@ -106,7 +104,6 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
 
         // isDefault() method should be in sync with the following values
         this.outputPattern = "*.java";
-        this.timestamp = 0L;
         this.usePkgPath = true;
         this.makePairs = true;
         this.createPKProperties = true;
@@ -357,14 +354,6 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
         this.dataMapSuperTemplate = dataMapSuperTemplate;
     }
 
-    public long getTimestamp() {
-        return timestamp;
-    }
-
-    public void setTimestamp(long timestamp) {
-        this.timestamp = timestamp;
-    }
-
     public String getOutputPattern() {
         return outputPattern;
     }
@@ -389,12 +378,23 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
         this.createPropertyNames = createPropertyNames;
     }
 
+    /**
+     * @return false
+     * @deprecated cgen always regenerates classes, so there is nothing left 
to force
+     */
+    @Deprecated(since = "5.0", forRemoval = true)
     public boolean isForce() {
-        return force;
+        return true;
     }
 
+    /**
+     * does nothing
+     *
+     * @param force not used
+     * @deprecated cgen always regenerates classes, so there is nothing left 
to force
+     */
+    @Deprecated(since = "5.0", forRemoval = true)
     public void setForce(boolean force) {
-        this.force = force;
     }
 
     public boolean isCreatePKProperties() {
diff --git 
a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java 
b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
index 93b081f82..450e92a33 100644
--- 
a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
+++ 
b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
@@ -37,12 +37,12 @@ import 
org.apache.velocity.tools.config.FactoryConfiguration;
 import org.slf4j.Logger;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.OutputStreamWriter;
 import java.io.Writer;
+import java.nio.charset.Charset;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
@@ -308,34 +308,33 @@ public class ClassGenerationAction {
             return null;
         }
 
-        if (logger != null) {
-            String label = templateType.isSuperclass() ? "superclass" : 
"class";
-            logger.info("Generating {} file: {}", label, 
outFile.getCanonicalPath());
-        }
-
-        // return writer with specified encoding
-        FileOutputStream out = new FileOutputStream(outFile);
+        return new GeneratedFileWriter(outFile, templateType);
+    }
 
-        return (cgenConfiguration.getEncoding() != null) ? new 
OutputStreamWriter(out, cgenConfiguration.getEncoding()) : new 
OutputStreamWriter(out);
+    /**
+     * A callback invoked after a generated file was written to disk. Files 
whose generated contents match
+     * what is already on disk are left alone, so this is only called for 
files that actually changed.
+     *
+     * @since 5.0
+     */
+    protected void fileWritten(File file, TemplateType templateType) {
     }
 
     /**
-     * Returns a target file where a generated superclass must be saved. If 
null
-     * is returned, class shouldn't be generated.
+     * Returns a target file where a generated superclass must be saved. 
Superclasses are always
+     * regenerated, overwriting any previous version.
      */
     private File fileForSuperclass() throws Exception {
 
         String packageName = (String) context.get(Artifact.SUPER_PACKAGE_KEY);
         String className = (String) context.get(Artifact.SUPER_CLASS_KEY);
 
-        String filename = 
StringUtils.getInstance().replaceWildcardInStringWithString(WILDCARD, 
cgenConfiguration.getOutputPattern(), className);
-        File dest = new 
File(mkpath(cgenConfiguration.buildOutputPath().toFile(), packageName), 
filename);
-
-        if (dest.exists() && !fileNeedUpdate(dest, 
cgenConfiguration.getSuperTemplate().getData())) {
-            return null;
-        }
+        File dir = mkpath(cgenConfiguration.buildOutputPath().toFile(), 
packageName);
+        String fileName = StringUtils
+                .getInstance()
+                .replaceWildcardInStringWithString(WILDCARD, 
cgenConfiguration.getOutputPattern(), className);
 
-        return dest;
+        return new File(dir, fileName);
     }
 
     /**
@@ -360,43 +359,11 @@ public class ClassGenerationAction {
             if (!cgenConfiguration.isOverwrite()) {
                 return null;
             }
-
-            if (!fileNeedUpdate(dest, 
cgenConfiguration.getTemplate().getData())) {
-                return null;
-            }
         }
 
         return dest;
     }
 
-    /**
-     * Ignore if the destination is newer than the map
-     * (internal timestamp), i.e. has been generated after the map was
-     * last saved AND the template is older than the destination file
-     */
-    protected boolean fileNeedUpdate(File dest, String templateFileName) {
-        if (cgenConfiguration.isForce()) {
-            return true;
-        }
-
-        if (isOld(dest)) {
-            if (templateFileName == null) {
-                return false;
-            }
-
-            File templateFile = new File(templateFileName);
-            return templateFile.lastModified() >= dest.lastModified();
-        }
-        return true;
-    }
-
-    /**
-     * Is file modified after internal timestamp (usually equal to mtime of 
datamap file)
-     */
-    protected boolean isOld(File file) {
-        return file.lastModified() > cgenConfiguration.getTimestamp();
-    }
-
     /**
      * Returns a File object corresponding to a directory where files that
      * belong to <code>pkgName</code> package should reside. Creates any 
missing
@@ -462,4 +429,54 @@ public class ClassGenerationAction {
     public MetadataUtils getMetadataUtils() {
         return metadataUtils;
     }
+
+    /**
+     * Collects generated class text in memory, and on close writes it to the 
target file, but only if it
+     * differs from what the file already contains.
+     */
+    private class GeneratedFileWriter extends Writer {
+
+        private final File file;
+        private final TemplateType templateType;
+        private final StringBuilder buffer;
+
+        GeneratedFileWriter(File file, TemplateType templateType) {
+            this.file = file;
+            this.templateType = templateType;
+            this.buffer = new StringBuilder();
+        }
+
+        @Override
+        public void write(char[] chars, int offset, int length) {
+            buffer.append(chars, offset, length);
+        }
+
+        @Override
+        public void flush() {
+            // nothing to flush - the file is written on close
+        }
+
+        @Override
+        public void close() throws IOException {
+
+            String encoding = cgenConfiguration.getEncoding();
+            Charset charset = encoding != null ? Charset.forName(encoding) : 
Charset.defaultCharset();
+            byte[] generated = buffer.toString().getBytes(charset);
+
+            if (file.isFile()
+                    && file.length() == generated.length
+                    && Arrays.equals(generated, 
Files.readAllBytes(file.toPath()))) {
+                return;
+            }
+
+            Files.write(file.toPath(), generated);
+
+            if (logger != null) {
+                String label = templateType.isSuperclass() ? "superclass" : 
"class";
+                logger.info("Generating {} file: {}", label, 
file.getCanonicalPath());
+            }
+
+            fileWritten(file, templateType);
+        }
+    }
 }
diff --git 
a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
 
b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
index 39e96cee1..963a40622 100644
--- 
a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
+++ 
b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
@@ -20,6 +20,7 @@
 package org.apache.cayenne.gen;
 
 import java.io.*;
+import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -38,8 +39,6 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
 public class ClassGenerationActionTest extends CgenCase {
 
@@ -225,64 +224,62 @@ public class ClassGenerationActionTest extends CgenCase {
        }
 
        @Test
-       public void isOld() {
-               File file = mock(File.class);
-               when(file.lastModified()).thenReturn(1000L);
-
-               cgenConfiguration.setTimestamp(0);
-               assertTrue(action.isOld(file));
-
-               cgenConfiguration.setTimestamp(2000L);
-               assertFalse(action.isOld(file));
-       }
-
-       @Test
-       public void fileNeedUpdate() {
-               File file = mock(File.class);
-               when(file.lastModified()).thenReturn(1000L);
-
-               cgenConfiguration.setTimestamp(0);
-               cgenConfiguration.setForce(false);
-
-               assertFalse(action.fileNeedUpdate(file, null));
-
-               cgenConfiguration.setTimestamp(2000L);
-               cgenConfiguration.setForce(false);
+       public void fileForSuperclass() throws Exception {
 
-               assertTrue(action.fileNeedUpdate(file, null));
+               TemplateType templateType = TemplateType.DATAMAP_SUPERCLASS;
 
-               cgenConfiguration.setTimestamp(0);
-               cgenConfiguration.setForce(true);
+               cgenConfiguration.setRootPath(tempFolder.toPath());
+               cgenConfiguration.updateOutputPath(Paths.get("."));
+               action = new ClassGenerationAction(cgenConfiguration);
+               ObjEntity testEntity1 = new ObjEntity("TEST");
+               testEntity1.setClassName("TestClass1");
+               action.context.put(Artifact.SUPER_PACKAGE_KEY, "");
+               action.context.put(Artifact.SUPER_CLASS_KEY, "TestClass1");
 
-               assertTrue(action.fileNeedUpdate(file, null));
+               File outFile = new File(tempFolder + "/TestClass1.java");
+               assertFalse(outFile.exists());
 
-               cgenConfiguration.setTimestamp(2000L);
-               cgenConfiguration.setForce(true);
+               try(Writer out = action.openWriter(templateType)) {
+                       out.write("// generated");
+                       // the file is written on close, not on open
+                       assertFalse(outFile.exists());
+               }
+               assertEquals("// generated", 
Files.readString(outFile.toPath()));
 
-               assertTrue(action.fileNeedUpdate(file, null));
+               // superclasses are regenerated unconditionally
+               try(Writer out = action.openWriter(templateType)) {
+                       assertNotNull(out);
+                       out.write("// regenerated");
+               }
+               assertEquals("// regenerated", 
Files.readString(outFile.toPath()));
        }
 
        @Test
-       public void fileForSuperclass() throws Exception {
+       public void unchangedFileLeftAlone() throws Exception {
 
                TemplateType templateType = TemplateType.DATAMAP_SUPERCLASS;
 
                cgenConfiguration.setRootPath(tempFolder.toPath());
                cgenConfiguration.updateOutputPath(Paths.get("."));
                action = new ClassGenerationAction(cgenConfiguration);
-               ObjEntity testEntity1 = new ObjEntity("TEST");
-               testEntity1.setClassName("TestClass1");
                action.context.put(Artifact.SUPER_PACKAGE_KEY, "");
                action.context.put(Artifact.SUPER_CLASS_KEY, "TestClass1");
 
                File outFile = new File(tempFolder + "/TestClass1.java");
-               assertFalse(outFile.exists());
+               try(Writer out = action.openWriter(templateType)) {
+                       out.write("// generated");
+               }
+
+               // backdate the file, so that an actual rewrite would be 
detectable
+               assertTrue(outFile.setLastModified(outFile.lastModified() - 
5_000L));
+               long backdated = outFile.lastModified();
 
-               try(Writer ignored = action.openWriter(templateType)) {
-                       assertTrue(outFile.exists());
+               try(Writer out = action.openWriter(templateType)) {
+                       out.write("// generated");
                }
 
-               assertNull(action.openWriter(templateType));
+               assertEquals(backdated, outFile.lastModified(), "Identical file 
must not be rewritten");
+               assertEquals("// generated", 
Files.readString(outFile.toPath()));
        }
 
        @Test
@@ -301,16 +298,24 @@ public class ClassGenerationActionTest extends CgenCase {
                File outFile = new File(tempFolder + "/TestClass1.java");
                assertFalse(outFile.exists());
 
-               try(Writer ignored = action.openWriter(templateType)) {
-                       assertTrue(outFile.exists());
+               try(Writer out = action.openWriter(templateType)) {
+                       out.write("// generated");
                }
+               assertTrue(outFile.exists());
 
+               // existing subclasses are preserved when generating class 
pairs...
                assertNull(action.openWriter(templateType));
 
+               // ... and when generating single classes without "overwrite"
                cgenConfiguration.setMakePairs(false);
                assertNull(action.openWriter(templateType));
 
+               // "overwrite" regenerates the subclass unconditionally
                cgenConfiguration.setOverwrite(true);
-               assertNull(action.openWriter(templateType));
+               try(Writer out = action.openWriter(templateType)) {
+                       assertNotNull(out);
+                       out.write("// regenerated");
+               }
+               assertEquals("// regenerated", 
Files.readString(outFile.toPath()));
        }
 }
diff --git 
a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java 
b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
index b5100bf00..227834b57 100644
--- a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
+++ b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
@@ -198,10 +198,12 @@ public class CgenTask extends BaseCayenneTask {
     private Boolean createPropertyNames;
 
     /**
-     * Force run (skip check for files modification time)
+     * No longer has any effect - cgen always regenerates classes.
      *
      * @since 4.1
+     * @deprecated cgen runs unconditionally
      */
+    @Deprecated(since = "5.0", forRemoval = true)
     @Input
     private boolean force;
 
@@ -260,6 +262,10 @@ public class CgenTask extends BaseCayenneTask {
     public void generate() {
         File dataMapFile = getDataMapFile();
 
+        if (force || getProject().hasProperty("force")) {
+            getLogger().warn("'force' is deprecated and ignored. cgen always 
regenerates classes.");
+        }
+
         injector = new ToolsInjectorBuilder()
                 .addModule(new 
ToolsModule(LoggerFactory.getLogger(CgenTask.class)))
                 .create();
@@ -280,11 +286,6 @@ public class CgenTask extends BaseCayenneTask {
                 
filterEmbeddableAction.setNameFilter(NamePatternMatcher.build(getLogger(), 
null, excludeEmbeddables));
                 generator.setLogger(getLogger());
 
-                if (this.force || getProject().hasProperty("force")) {
-                    generator.getCgenConfiguration().setForce(true);
-                }
-                
generator.getCgenConfiguration().setTimestamp(dataMapFile.lastModified());
-
                 if (!hasConfig() && useConfigFromDataMap) {
                     generator.prepareArtifacts();
                 } else {
@@ -399,7 +400,7 @@ public class CgenTask extends BaseCayenneTask {
         return destDir != null || destDirName != null || encoding != null || 
excludeEntities != null || excludeEmbeddables != null || includeEntities != 
null ||
                 makePairs != null || mode != null || outputPattern != null || 
overwrite != null || superPkg != null ||
                 superTemplate != null || template != null || 
embeddableTemplate != null || embeddableSuperTemplate != null ||
-                usePkgPath != null || createPropertyNames != null || force || 
dataMapTemplate != null ||
+                usePkgPath != null || createPropertyNames != null || 
dataMapTemplate != null ||
                 dataMapSuperTemplate != null || createPKProperties != null || 
externalToolConfig != null;
     }
 
@@ -723,14 +724,26 @@ public class CgenTask extends BaseCayenneTask {
         setCreatePropertyNames(createPropertyNames);
     }
 
+    /**
+     * @deprecated cgen runs unconditionally
+     */
+    @Deprecated(since = "5.0", forRemoval = true)
     public boolean isForce() {
         return force;
     }
 
+    /**
+     * @deprecated cgen runs unconditionally
+     */
+    @Deprecated(since = "5.0", forRemoval = true)
     public void setForce(boolean force) {
         this.force = force;
     }
 
+    /**
+     * @deprecated cgen runs unconditionally
+     */
+    @Deprecated(since = "5.0", forRemoval = true)
     public void force(boolean force) {
         setForce(force);
     }
diff --git 
a/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
 
b/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
index bd6bf2326..b7a6576cc 100644
--- 
a/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
+++ 
b/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
@@ -19,21 +19,16 @@
 
 package org.apache.cayenne.tools;
 
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
 import org.apache.cayenne.configuration.xml.DataChannelMetaData;
 import org.apache.cayenne.dbsync.filter.NamePatternMatcher;
 import org.apache.cayenne.dbsync.reverse.configuration.ToolsModule;
 import org.apache.cayenne.di.Injector;
 import org.apache.cayenne.gen.ArtifactsGenerationMode;
-import org.apache.cayenne.gen.CgenConfiguration;
 import org.apache.cayenne.gen.CgenConfigList;
+import org.apache.cayenne.gen.CgenConfiguration;
+import org.apache.cayenne.gen.CgenTemplate;
 import org.apache.cayenne.gen.ClassGenerationAction;
 import org.apache.cayenne.gen.ClassGenerationActionFactory;
-import org.apache.cayenne.gen.CgenTemplate;
 import org.apache.cayenne.gen.TemplateType;
 import org.apache.cayenne.map.DataMap;
 import org.apache.maven.plugin.AbstractMojo;
@@ -45,6 +40,11 @@ import org.apache.maven.plugins.annotations.Parameter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 /**
  * Maven mojo to perform class generation from data cgenConfiguration. This 
class is an Maven
  * adapter to DefaultClassGenerator class.
@@ -201,11 +201,12 @@ public class CayenneGeneratorMojo extends AbstractMojo {
     private Boolean createPropertyNames;
 
     /**
-     * If set to <code>true</code>, will skip file modification time 
validation and regenerate all.
-     * Default is <code>false</code>.
+     * No longer has any effect - cgen always regenerates classes.
      *
      * @since 4.1
+     * @deprecated cgen runs unconditionally
      */
+    @Deprecated(since = "5.0", forRemoval = true)
     @Parameter(defaultValue = "false", property = "force")
     private boolean force;
 
@@ -264,6 +265,10 @@ public class CayenneGeneratorMojo extends AbstractMojo {
                 .create();
 
         Logger logger = new MavenLogger(this);
+        if (force) {
+            logger.warn("'force' is deprecated and ignored. cgen always 
regenerates classes.");
+        }
+
         CayenneGeneratorMapLoaderAction loaderAction = new 
CayenneGeneratorMapLoaderAction(injector);
         loaderAction.setMainDataMapFile(map);
 
@@ -278,11 +283,6 @@ public class CayenneGeneratorMojo extends AbstractMojo {
                 
filterEmbeddableAction.setNameFilter(NamePatternMatcher.build(logger, null, 
excludeEmbeddables));
                 generator.setLogger(logger);
 
-                if (force) {
-                    // will (re-)generate all files
-                    generator.getCgenConfiguration().setForce(true);
-                }
-                
generator.getCgenConfiguration().setTimestamp(map.lastModified());
                 if (!hasConfig() && useConfigFromDataMap) {
                     generator.prepareArtifacts();
                 } else {
@@ -319,7 +319,7 @@ public class CayenneGeneratorMojo extends AbstractMojo {
         return destDir != null || encoding != null || excludeEntities != null 
|| excludeEmbeddables != null || includeEntities != null ||
                 makePairs != null || mode != null || outputPattern != null || 
overwrite != null || superPkg != null ||
                 superTemplate != null || template != null || 
embeddableTemplate != null || embeddableSuperTemplate != null ||
-                usePkgPath != null || createPropertyNames != null || force || 
dataMapTemplate != null ||
+                usePkgPath != null || createPropertyNames != null || 
dataMapTemplate != null ||
                 dataMapSuperTemplate != null || createPKProperties != null || 
externalToolConfig != null;
     }
 
diff --git a/cayenne-mcp-server/PROTOCOL.md b/cayenne-mcp-server/PROTOCOL.md
index 3d3e7b4e9..6aea9a33d 100644
--- a/cayenne-mcp-server/PROTOCOL.md
+++ b/cayenne-mcp-server/PROTOCOL.md
@@ -57,8 +57,9 @@ Both fields are required.
 ```
 
 `status` semantics:
-- `generated` — at least one file was written; `files` is non-empty
-- `up_to_date` — all candidate files were already current; `files` is empty
+- `generated` — at least one file changed on disk; `files` lists exactly those 
files
+- `up_to_date` — cgen ran in full, but every generated file was byte-identical 
to what was already there,
+  so nothing was touched; `files` is empty
 - `validation_failed` — a pre-flight check failed; the failing slot in 
`validation` is `false`, later slots are `null`
 - `error` — cgen started but threw mid-run; `files` lists what was written 
before the failure
 
diff --git 
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java
 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java
index 56017064f..26a73ad45 100644
--- 
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java
+++ 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java
@@ -167,18 +167,6 @@ public class CgenRunTool {
                 ? CgenConfiguration.createDefault(dataMap, 
CgenConfiguration.defaultOutputDir(dataMap))
                 : configList.getAll().getFirst();
 
-        // Set the DataMap file's mtime as the timestamp so fileNeedUpdate() 
can detect DataMap changes correctly.
-        if (dataMap.getConfigurationSource() != null) {
-            try {
-                Path dataMapFile = 
Path.of(dataMap.getConfigurationSource().getURL().toURI());
-                
cgenConfig.setTimestamp(Files.getLastModifiedTime(dataMapFile).toMillis());
-            } catch (Exception e) {
-                // URI conversion failed (e.g. non-file: URL) or some problems 
with file mtime read,
-                // better to regen all, than silently fail.
-                cgenConfig.setForce(true);
-            }
-        }
-
         // Step 5 — destDir specified?
         Path destDir = cgenConfig.buildOutputPath();
         if (destDir == null) {
diff --git 
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/InstrumentedClassGenerationAction.java
 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/InstrumentedClassGenerationAction.java
index d37cc0064..e8c3b9845 100644
--- 
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/InstrumentedClassGenerationAction.java
+++ 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/InstrumentedClassGenerationAction.java
@@ -22,7 +22,6 @@ import org.apache.cayenne.gen.Artifact;
 import org.apache.cayenne.gen.ArtifactGenerationMode;
 import org.apache.cayenne.gen.CgenConfiguration;
 import org.apache.cayenne.gen.ClassGenerationAction;
-import org.apache.cayenne.gen.StringUtils;
 import org.apache.cayenne.gen.TemplateType;
 import org.apache.cayenne.map.Embeddable;
 import org.apache.cayenne.map.ObjEntity;
@@ -30,15 +29,13 @@ import 
org.apache.cayenne.mcp.tools.cgen.protocol.CgenFileEntry;
 import org.apache.cayenne.mcp.tools.cgen.protocol.CgenFileKind;
 
 import java.io.File;
-import java.io.Writer;
-import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
 /**
- * Extends {@link ClassGenerationAction} to record which files cgen wrote and 
how many
- * were considered. Placed in {@code org.apache.cayenne.gen} so it shares the 
same Java
- * package as {@link ClassGenerationAction} and can read its protected fields.
+ * Extends {@link ClassGenerationAction} to record which files cgen changed 
and how many were
+ * considered. Cgen rewrites a file only when its generated contents differ 
from what is on disk,
+ * so the recorded files are the ones that actually changed.
  *
  * @since 5.0
  */
@@ -63,12 +60,11 @@ class InstrumentedClassGenerationAction extends 
ClassGenerationAction {
     }
 
     @Override
-    protected Writer openWriter(TemplateType templateType) throws Exception {
-        Writer w = super.openWriter(templateType);
-        if (w != null && currentArtifact != null) {
-            writtenFiles.add(buildFileEntry(currentArtifact, templateType));
+    protected void fileWritten(File file, TemplateType templateType) {
+        if (currentArtifact != null) {
+            writtenFiles.add(new CgenFileEntry(
+                    file.getAbsolutePath(), toKind(templateType), 
sourceName(currentArtifact)));
         }
-        return w;
     }
 
     public List<CgenFileEntry> getWrittenFiles() {
@@ -90,28 +86,6 @@ class InstrumentedClassGenerationAction extends 
ClassGenerationAction {
         return count;
     }
 
-    private CgenFileEntry buildFileEntry(Artifact artifact, TemplateType 
templateType) {
-        String packageName;
-        String className;
-        if (templateType.isSuperclass()) {
-            packageName = (String) context.get(Artifact.SUPER_PACKAGE_KEY);
-            className = (String) context.get(Artifact.SUPER_CLASS_KEY);
-        } else {
-            packageName = (String) context.get(Artifact.SUB_PACKAGE_KEY);
-            className = (String) context.get(Artifact.SUB_CLASS_KEY);
-        }
-
-        String filename = 
StringUtils.getInstance().replaceWildcardInStringWithString(
-                "*", cgenConfiguration.getOutputPattern(), className);
-        Path dir = cgenConfiguration.buildOutputPath();
-        if (cgenConfiguration.isUsePkgPath() && packageName != null) {
-            dir = dir.resolve(packageName.replace('.', File.separatorChar));
-        }
-        Path filePath = dir.resolve(filename).toAbsolutePath();
-
-        return new CgenFileEntry(filePath.toString(), toKind(templateType), 
sourceName(artifact));
-    }
-
     private static CgenFileKind toKind(TemplateType type) {
         return switch (type) {
             case ENTITY_SUPERCLASS -> CgenFileKind.entity_super;
diff --git 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java
 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java
index 3401918c2..30a00281f 100644
--- 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java
+++ 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java
@@ -28,7 +28,6 @@ import org.junit.jupiter.api.io.TempDir;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.attribute.FileTime;
 import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -88,7 +87,10 @@ public class CgenRunIT {
 
     @Test
     public void upToDateOnSecondRun() {
-        tool.run(projectFile.toString(), "PersonMap");
+        CgenRunResult first = tool.run(projectFile.toString(), "PersonMap");
+
+        // cgen re-runs in full, but the regenerated content is identical, so 
nothing is touched on disk
+        long[] mtimes = first.files().stream().mapToLong(e -> 
Path.of(e.path()).toFile().lastModified()).toArray();
 
         CgenRunResult second = tool.run(projectFile.toString(), "PersonMap");
 
@@ -97,6 +99,33 @@ public class CgenRunIT {
         assertTrue(second.files().isEmpty());
         assertTrue(second.summary().filesConsidered() > 0);
         assertNull(second.error());
+
+        for (int i = 0; i < mtimes.length; i++) {
+            assertEquals(mtimes[i], 
Path.of(first.files().get(i).path()).toFile().lastModified(),
+                    "Unchanged file must not be rewritten: " + 
first.files().get(i).path());
+        }
+    }
+
+    @Test
+    public void regeneratesAfterDataMapChange() throws IOException {
+        CgenRunResult first = tool.run(projectFile.toString(), "PersonMap");
+        assertEquals("generated", first.status());
+
+        // add an attribute to the entity - the superclass must pick it up
+        Path dataMapFile = tempDir.resolve("PersonMap.map.xml");
+        Files.writeString(dataMapFile, Files.readString(dataMapFile).replace(
+                "<obj-entity name=\"Person\" 
className=\"com.example.Person\"/>",
+                """
+                <obj-entity name="Person" className="com.example.Person">
+                        <obj-attribute name="nickname" 
type="java.lang.String"/>
+                    </obj-entity>"""));
+
+        CgenRunResult second = tool.run(projectFile.toString(), "PersonMap");
+
+        assertEquals("generated", second.status());
+        assertEquals(List.of(CgenFileKind.entity_super),
+                second.files().stream().map(CgenFileEntry::kind).toList());
+        
assertTrue(Files.readString(Path.of(second.files().getFirst().path())).contains("nickname"));
     }
 
     @Test
@@ -118,30 +147,6 @@ public class CgenRunIT {
         assertTrue(skipped >= 1, "At least one file (the existing subclass) 
should have been skipped");
     }
 
-    @Test
-    public void regeneratesAfterDataMapChange() throws IOException {
-        // First run — generates files
-        CgenRunResult first = tool.run(projectFile.toString(), "PersonMap");
-        assertEquals("generated", first.status());
-
-        // Bump the DataMap's mtime to be clearly newer than the generated 
files.
-        // Use setLastModifiedTime rather than a wall-clock sleep to avoid
-        // filesystem mtime granularity issues (Windows has 1-second 
resolution).
-        long maxGeneratedMtime = first.files().stream()
-                .mapToLong(e -> Path.of(e.path()).toFile().lastModified())
-                .max()
-                .orElseThrow();
-        Path dataMapFile = tempDir.resolve("PersonMap.map.xml");
-        Files.setLastModifiedTime(dataMapFile, 
FileTime.fromMillis(maxGeneratedMtime + 5_000L));
-
-        // Second run — must detect that the DataMap is newer than the 
generated files
-        // and regenerate the superclass(es).
-        CgenRunResult second = tool.run(projectFile.toString(), "PersonMap");
-        assertEquals("generated", second.status(),
-                "Expected regeneration after DataMap mtime was bumped past 
generated files");
-        assertTrue(second.summary().filesWritten() > 0);
-    }
-
     @Test
     public void generatesWithDefaultConfigWhenNoCgenBlock() throws IOException 
{
         // A DataMap with no <cgen> block, laid out in a standard Maven 
structure. The tool must
diff --git 
a/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part6/maven-plugin.adoc
 
b/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part6/maven-plugin.adoc
index 5d8b2674f..56be88a8e 100644
--- 
a/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part6/maven-plugin.adoc
+++ 
b/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part6/maven-plugin.adoc
@@ -113,7 +113,7 @@ ${project.basedir}/src/main/resources/my.map.xml
 
 .^|force
 .^|boolean
-.^|If set to "true", will force run from maven/gradle.
+.^|_Deprecated and ignored since 5.0._ cgen always regenerates classes, so 
there is nothing left to force.
 
 .^|createPKProperties
 .^|boolean
diff --git 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/cgen/DataDomainCgenTab.java
 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/cgen/DataDomainCgenTab.java
index 01f37b8aa..c8e5bf755 100644
--- 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/cgen/DataDomainCgenTab.java
+++ 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/cgen/DataDomainCgenTab.java
@@ -64,7 +64,6 @@ public class DataDomainCgenTab extends 
DataDomainGeneratorsTab<CgenConfiguration
                     cgenConfigList.add(createConfiguration(dataMap));
                 }
                 for (CgenConfiguration cgenConfiguration : 
cgenConfigList.getAll()) {
-                    cgenConfiguration.setForce(true);
                     ClassGenerationAction action = 
actionFactory.createAction(cgenConfiguration);
                     action.prepareArtifacts();
                     action.execute();
diff --git 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datamap/cgen/CgenPanel.java
 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datamap/cgen/CgenPanel.java
index 3e360faaa..f153b2fb7 100644
--- 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datamap/cgen/CgenPanel.java
+++ 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datamap/cgen/CgenPanel.java
@@ -249,7 +249,6 @@ public class CgenPanel extends ProjectPanel implements 
ObjEntityListener, Embedd
             cgenConfiguration.resolveExcludedEmbeddables();
             addToSelectedEntities(cgenConfiguration.getEntities());
             addToSelectedEmbeddables(cgenConfiguration.getEmbeddables());
-            cgenConfiguration.setForce(true);
             return;
         }
 
@@ -438,7 +437,6 @@ public class CgenPanel extends ProjectPanel implements 
ObjEntityListener, Embedd
     private CgenConfiguration createDefaultCgenConfiguration(DataMap map) {
         Path basePath = map.getLocation() != null ? CgenOps.baseDir(session) : 
null;
         CgenConfiguration configuration = CgenConfiguration.createDefault(map, 
basePath);
-        configuration.setForce(true);
         configuration.setEncoding(new 
GeneralPrefs(app.getPrefsLocator().appNode(GeneralPrefs.NODE)).getEncoding());
         return configuration;
     }

Reply via email to