*Modified areas:* rules/ (40 TRPL files, incl. new stable_naming.xml), com.goldencode.p2j.pattern (17 files: AstProcessorJob, AstProcessorThread, BaseRuleContainer, FileStatus new), com.goldencode.p2j.convert + convert.db (18), com.goldencode.expr (7, Resettable new), com.goldencode.p2j.uast (8), com.goldencode.p2j.schema (7), com.goldencode.p2j.report (5), com.goldencode.ast (4), com.goldencode.p2j.persist (4), com.goldencode.p2j.security (2), com.goldencode.p2j.preproc (2), com.goldencode.artifacts (1)

*Domains reviewed:* fwd-conversion-pipeline, fwd-persistence-layer, fwd-server-infrastructure, fwd-web-services-admin, fwd-i18n-translation

h2. Critical

* *[CRITICAL]* _functional_ @PatternEngine@.@run@: the multi-threaded branch is selected purely on @!singleThreadedMode@ without checking that worker threads exist. @singleThreadedMode@ is a static defaulting to @false@ and @startThreads()@ has exactly two call sites — @TransformDriver.front()@ (TransformDriver.java:340) and @ReportDriver.patternEngine()@ (ReportDriver.java:217) — so any other entry point enqueues an @AstProcessorJob@ per artifact onto the static @AstProcessorThread.queue@ that has no consumer and then blocks on @latch.await()@ (PatternEngine.java:1122-1125) forever. The earlier @threadLatch@ at line 1044 is sized by @threads.size() == 0@ and returns immediately, so nothing detects the empty pool. Concrete triggers: @ConversionDriver@ run modes @M0@/@M1@/@MX@/@MY@/@CB@ (all built with @front=false@ at ConversionDriver.java:381-385, so @front()@ is skipped while @middle()@/@back()@ still call @processTrees@ → @engine.run(rules)@), @SearchTrees.main@ line 292, and @ReportApi.runSearch@ line 1721, which additionally hangs inside @try (Connection conn = dbs.openConnection())@ holding a @DatabaseService@ pool connection.
* *[CRITICAL]* _functional_ @AstProcessorJob@.@run@: @setCurrentArtifact()@, @engine.setCurrentState()@, @AstManager.get().loadTree(artifact)@, @ast.getArtifact().getRelativePath()@ and the @getAnnotation("source-file").toString()@ dereference all execute *before* the @try@ whose @finally@ owns @latch.countDown()@ (lines 150-188 vs 190-208). @AstException extends RuntimeException@ and @XmlFilePlugin.loadTree@ throws it on any parse failure (plus @IllegalArgumentException@ when the file is absent), so a single unreadable/corrupt persisted @.ast@ — e.g. one left truncated by a previously aborted run — aborts the job, is swallowed by @AstProcessorThread.run@'s @catch (Exception)@, and @latch.await()@ never returns. The single-threaded path has the same statements outside its @try@, but there the exception propagates out of @run(String)@ and aborts loudly instead of hanging.
* *[CRITICAL]* _functional_ @AstProcessorJob@.@run@: the parent-class ordering guard (lines 173-188) re-adds the job to @AstProcessorThread.queue@ with no attempt counter, no timeout and no give-up path whenever @fileStatusMap.get(parentPath) != FileStatus.COMPLETED@, but @fileStatusMap@ is only ever populated (lines 187/205) for files that are themselves dispatched as jobs, i.e. in @targetPaths@. Concrete trigger: an incremental conversion (@TransformDriver.executeJob@ narrows @job.artifacts@ to @filteredFileArtifacts@ at lines 1192-1193, and the dependency expansion driven by @ConversionData.clean()@ follows only @tmpTabNodes@ — it does not pull in a changed @.cls@'s unchanged super-class) or a single-file/explicit-filelist conversion of a @.cls@ whose parent is unchanged. @sortLegacyClasses@ drops such an orphan from the BFS list but @convertSourceArtifactsToAstArtifacts@ appends it back (TransformDriver.java:929-931), so the job runs, finds no @COMPLETED@ parent, and the @annotations/annotations@ phase live-locks: all worker threads spin at 100% CPU re-running a full DOM parse per iteration and the latch never reaches zero. A secondary trigger exists on full conversions if @oo-skeleton-path@ points outside a @"/skeleton/"@ directory, defeating the only escape clause at line 175.
* *[CRITICAL]* _functional_ @AstProcessorThread@.@run@: @catch (Exception e) { System.err.println("Job execution failed: " + e.getMessage()); }@ (lines 150-153) swallows every unchecked exception thrown by a job, including the fail-fast @AstException@ that @PatternEngine.handleRunThrowable()@ deliberately raises at PatternEngine.java:1981-1986 when @throwFailed@ is set. @throwFailed@ defaults to @true@ (line 559) and @TransformDriver.processTrees@ sets it from @job.rollbackFailed@, which defaults to @false@ — i.e. fail-fast is on for a default conversion. In the old single-threaded flow that exception propagated out of @run(String)@ and aborted the conversion. Now @AstProcessorJob.run@'s @finally@ still counts the latch down, the exception dies as one stack-trace-less stderr line, post-rules and @finish()@ run, and the driver reports success while emitting incomplete generated Java. @ReportDriver@ (which never calls @setThrowFailed@) has the same defect: the failing file is silently dropped from the report.
* *[CRITICAL]* _functional_ @SchemaWorker@.@persistSchema@: the @joinCollisions@ filter was made dead by the concurrency refactor. Trunk shadowed the field with a local of the same name (@Map<String, TableRelation> joins = new Hashtable<>(this.joins); joins.keySet().removeAll(joinCollisions);@) and the loop iterated that local. The copy is now renamed @joinsCopy@ (@new ConcurrentHashMap<>(this.joins)@, filtered under @synchronized (joinCollisions)@) but the loop below was not updated and still reads @for (TableRelation join : joins.values())@ — the unfiltered *field* (SchemaWorker.java:425-436). @joinsCopy@ is written and never read. Trigger: @SchemaSupport.checkJoin@ (line 932) adds to @joinCollisions@ whenever a second @RelationAnalyzer.createJoin@ for the same table pair yields a @TableRelation@ differing from the cached one; those pairs were previously excluded from @attachRelationData@ and are now persisted into the @.schema@ AST, so p2o/DMO generation emits @Relation@ metadata for relations the analyzer explicitly rejected.
* *[CRITICAL]* _functional_ @SymbolResolver@.@setLocal@ (uast): @PatternEngine.run()@ builds a *single* @EngineState@ on the main thread (line 1051, capturing @SymbolResolver.locate()@) and hands that same object to every @AstProcessorJob@ (line 1116); each job calls @engine.setCurrentState(state)@ → @SymbolResolver.setLocal(state.sr)@ (line 2572). The *same* @WorkArea@ is therefore bound into every worker thread's @ContextLocal@, yet @classCache@ and @fname2qname@ are plain @HashMap@s. Concrete trigger: @rules/annotations/oo_references.rules@ lines 294/333/366/386/520/531/577/690/750/1016 call @loadConvertedClass@, which does @wa.fname2qname.put(...)@ (SymbolResolver.java:2400) with no lock at all; @annotations.xml@:592 pulls @oo_references@ into @annotations/annotations@, which @AstProcessorJob@ runs multi-threaded. Result: lost entries and corrupted bucket chains in a map that decides class resolution.
* *[CRITICAL]* _functional_ @SymbolResolver@.@loadClassDefinition@ (uast): the new @CLASS_LOCKS@ (line 783) is keyed by @qname.toLowerCase()@ (line 2491), so two threads loading *different* classes acquire *different* locks while concurrently mutating the same shared @wa.pendingLoading@ @HashSet@ (add 2507 / remove 2516) and @wa.classCache@ @HashMap@ (put 2518). The lock serializes nothing that needs serializing. @getClassDefinition()@ (2536-2539) reads @wa.classCache@ with no lock and is itself called *outside* the lock from @loadClassDefinition@'s fast path at line 2476, so a concurrent @put@ mid-resize can return null or traverse a corrupt chain. Trigger: @rules/convert/expressions.rules@:1608/1662 and @rules/convert/oo_calls.rules@:484 during the multi-threaded core conversion pass. The fix must guard the WorkArea collections themselves, not the class name.
* *[CRITICAL]* _functional_ @Variable@.@deepCloneObject@: for any value that is not a primitive/wrapper/String, not @Cloneable@, and not a @Collection@/@Map@, the method prints to @System.err@ and returns @null@, and @Variable(Variable other)@ assigns that @null@ to the per-thread copy. Fully constructed trigger: @rules/callgraph/generate_call_graph.xml@ declares the *global* variable @scope@ of type @ScopedDictionary@ with no @multiThreadAccess@ (line 95) and assigns it @create("com.goldencode.p2j.util.ScopedDictionary")@ in the profile's *global* @<init-rules>@ (line 102), which runs under @isInitializingGlobals == true@ so no thread copy is recorded. The first per-artifact reference — @scope.addScope(null)@ at line 130, in the rule-set @<init-rules>@ with @isInitializingGlobals == false@ — reaches @Compiler.instantiate@ (Compiler.java:1252-1262) → @Variable.clone()@ → @new Variable(this)@ → @deepCloneObject(ScopedDictionary)@. @ScopedDictionary@ implements neither @Cloneable@ nor @Collection@/@Map@, so the value becomes @null@ and @scope.addScope(null)@ throws NPE. This is deterministic and *not* thread-count dependent — @Variable.clone()@ does not consult @singleThreadedMode@, and @CallGraphGenerator@ explicitly sets it true. Same shape for @scope@ in @generate_call_graph_post.xml@ and @scope2@ in @load_code_set_post.xml@.
* *[CRITICAL]* _performance_ @ReportWorker@.@prepareDatabaseStatements@: it calls @prepareStatements(null, incremental)@ on the calling thread *and* @PatternEngine.addThreadJob(() -> prepareStatements(null, incremental))@. Because @ReportWorker.context@ is a @ContextLocal@ falling back to a @ThreadLocal@, each of the N @AstProcessorThread@s materializes its own @WorkArea@ with its own H2 @Connection@ plus ~24 @PreparedStatement@s. No matching @addThreadJob(this::closeConnection)@ exists — the only @closeConnection()@ is the @finally@ in @postprocessPipeline@ (line 1479) on the calling thread — and @shutdownThreads()@ joins and discards the threads without closing their connections. Worst trigger: @ReportApi@ line 1649 calls @ReportDriver.patternEngine(...)@ inside the long-lived analytics web-server JVM for every user-initiated incremental report, with @numThreads == -1@ spawning up to 16 threads — leaking 16 connections / ~384 prepared statements *per report run*, unbounded over server lifetime (@DB_CLOSE_DELAY=-1@ keeps the database open too).
* *[CRITICAL]* _functional_ @naming@.init-rules: the nested @<action>classname = result.substring(fullpkg.length() + 1)</action>@ placed inside @<action on="false">not result.equals(classfile)</action>@ is silently discarded — @ConfigLoader.action()@ (ConfigLoader.java:1410) loads only @XmlHelper.getText(element)@ and never calls @processChildElements@, and @XmlHelper.getText()@ (XmlHelper.java:1127) walks only TEXT/CDATA children, so the outer element compiles to the side-effect-free boolean @not result.equals(classfile)@ and the child is never visited — with no error raised. The assignment is needed only on the @ensureEntry@ *cache-hit* path, because on a miss @computeClassName@ sets @classname@/@classfile@ as a side effect. Concrete trigger: @naming.rules@ populates @convertedClasses@ but never @classNames@, so for any artifact where the DB-backed @convertedClasses@ has an entry but @classNames@ does not — i.e. any second/incremental conversion of a file first named by @naming.rules@ — @ensureEntry@ (CommonAstSupport.java:6830) returns the cached value without running @computeClassName@, and @putNote("classname", classname)@ (naming.rules:275) records either @null@ or the leftover value from the preceding @usingClasses@ loop, which is a fully-qualified imported class name. The element must be @<rule on="false">@.
* *[CRITICAL]* _functional_ @legacy_services@: the rewritten @annotations/naming@ init-rules now reference a @classNames@ variable (naming.rules:265-266, newly introduced here — trunk read only @convertedClasses@), but @rules/annotations/legacy_services.xml@, which loads @<rule-set name="annotations/naming" input="tree"/>@ at line 111 and deliberately declares the stand-ins @convertedNames@, @methodJavaNames@, @usingClasses@, @convertedClasses@, @ovrdNames@, @ftypesByName@ "because [they are] used by annotations/naming.rules", was not updated. @classNames@ is declared nowhere in @naming.rules@, nowhere in @rules/include/*@, and only at cfg level in @annotations_prep.xml@:127 and @stable_naming.xml@:88 — neither in the proxy pipeline. With no matching variable, no @getClassNames()@ on any registered worker and no library target, @SymbolResolver.resolveFunction@ returns @null@ and @expression.g@:1323 throws @UnresolvedSymbolException@, aborting the first tree of the "Proxy Client Programs Annotations" phase driven by @GenerateLegacyProxyOpenClient.back()@.
* *[CRITICAL]* _functional_ @H2MapToSet@.@get@ / @H2MapToMap@.@get@: only the mutators (@put@/@putAll@/@remove@/@clear@) were made @synchronized@; @get@/@containsKey@/@keySet@/@size@/@values@ are not overridden at all and remain bare inherited @HashMap@ reads with no happens-before edge — unlike @H2Map@, which did get a @synchronized get@. All the collections involved are declared @multiThreadAccess="true"@ and @Variable.clone()@ returns @this@ for those, so every worker shares one instance. Concrete trigger: @rules/fixups/collect_procedures.rules@:131-136 calls @ensureEntry(intProcBodies, name, "java.util.TreeSet", true)@ — which does its @containsKey@/@put@ inside @synchronized (map)@ — then discards the return value and re-reads @procIds = intProcBodies.get(name)@ *outside* the monitor before @procIds.add(cref.id)@. A concurrent @put@ mid-resize makes the unlocked @get@ return @null@ (NPE) or a stale entry (dropped procedure ID). Same shape at @collect_oo.rules@:100-103, @functions_procedures.rules@:597-601, @collect_procedures.rules@:142-153, and @rules/adm/adm_windows.xml@ (synchronized @put@/@remove@ at 362/414/425/436/550 vs unlocked @get@ at 389/439), where a lost read silently drops a file as an embedded ADM program. The cheap fix is to use @ensureEntry@'s already-locked return value.
* *[CRITICAL]* _style_ @AstManager@: copyright header was regressed from "2009-2025" to "2009-2024" in this diff, instead of being advanced to 2026.
* *[CRITICAL]* _style_ @FileStatus@: the new file's header "Module   :" line reads "AstProcessorJob.java" (copy-pasted from the sibling new file) instead of "FileStatus.java".

h2. Major

* *[MAJOR]* _functional_ @AstSymbolResolver@.@execute@: the @wa.resolvingContainers.push@/@wa.funcScopes.push@ calls were moved *inside* the @try@ while the @finally@ still pops unconditionally, and a new @catch (Exception e) { throw new RuntimeException(e); }@ was added. Concrete trigger: a TRPL named-function call whose alias cannot be resolved — @wa.ruleScope.getFunctionContainer(text, null)@ returns @null@ (lines 825-830) and @ExpressionException("Cannot resolve named function: …")@ is thrown *before* either push. At the top call level (@wa.funcScopes@ empty) the @finally@'s @pop()@ throws @NoSuchElementException@, which *replaces* the real exception, so @handleRunThrowable@ logs a bare message-less @NoSuchElementException@ and the developer never learns which function was missing. In the nested case the deques are non-empty, so the @finally@ pops the *caller's* frames before the exception unwinds. Push both frames before entering the @try@.
* *[MAJOR]* _performance_ @AstProcessorThread@.@run@: @job.run()@ is guarded only by @catch (InterruptedException)@/@catch (Exception)@, so an @Error@ escapes the catches, the @while@ loop and @run()@ itself, killing the worker permanently. Reachable triggers: @XmlFilePlugin.loadTree@ is a top-down recursive parse invoked at AstProcessorJob.java:154 *outside* that job's own @catch (Throwable)@, so a @StackOverflowError@ on a deep AST propagates; likewise @OutOfMemoryError@, materially more likely now that up to 16 threads each hold a DOM plus an AST. Consequences: the in-flight job's @latch.countDown()@ never runs; the dead thread is never pruned from @PatternEngine.threads@, so the *next* profile's per-thread init job hangs on @threadLatch.await()@ (line 1072); and @getCombinedResults@ blocks indefinitely on @future.get()@ (line 2411, no timeout).
* *[MAJOR]* _performance_ @ConversionData@.@connect@: ~25 static methods were made @synchronized@, so all conversion-DB access from all worker threads serializes on the single @ConversionData.class@ monitor, each holding it across a full H2 round-trip. Concrete hot paths on worker threads: @ClassDefinition@ calls @getTempIdx(mdat.astId)@ once *per method* (line 647) and once *per property/variable* (line 817) of every loaded class definition; @SymbolResolver.loadClassDefinition@ (2482) and @loadConvertedClass@ (2394) call @findClassFilename@/@findClassQName@ per class resolution; @naming.rules@:243 calls @getConvertedSimpleClassName@ from a walk rule; @stable_naming.xml@:479/565/580 call @nextSequenceValue@ per temp-table. The serialization is not gratuitous — @DBHelper@ holds one @Connection@ and one unsynchronized statement cache, so some mutual exclusion is mandatory — but a single global monitor around per-member SELECTs caps the achievable speedup of the whole parallelization effort. The fix is per-thread connections or a small pool, not removing @synchronized@.
* *[MAJOR]* _performance_ @CommonAstSupport.Library@.@create@: @classForName@, @create@, @openStream@, @closeStream@, @fprintf@, @getNumBytesWritten@, @openStoredStream@, @nextSequenceValue@, @processClassVariables@ and @processClassBuffers@ are all @synchronized@ on the *single* @Library@ instance created once per @PatternEngine@ (@registerDefaultWorkers()@ → @new CommonAstSupport(this)@ → @setLibrary(new Library())@) and shared by every @AstProcessorThread@. Concrete trigger: @rules/convert/brew.xml@ opens one stream per generated Java file (@openStream@/@openStoredStream@ at lines 506/513) and then emits essentially every token of that file through @fprintf@ (lines 526-753+); @ByteCountOutputStream@ is a @FilterOutputStream@ directly over @FileOutputStream@ with no buffering, so each @fprintf@ is a write syscall executed while holding the global monitor. The "Generate Java Source" phase therefore runs effectively single-threaded no matter how many threads were started.
* *[MAJOR]* _performance_ @Scope@.@hashCode@/@equals@: @Scope@ — and therefore every @RuleContainer@, @BaseRuleContainer@ and @NamedFunction@, all of which extend it — now has value-based @equals@/@hashCode@ deep-hashing @singleState@. These scopes are the keys of the static @WeakHashMap@ @SymbolResolver.instCache@ (SymbolResolver.java:343), so every @getCachedInstance@/@putCachedInstance@ costs a full walk of the variable pool, @libraries@, @libraryClasses@, @libsByClass@ and @variableList@ instead of an identity hash; on the @exec()@ path (@consolidated_reports.xml@:302/312/330/339 inside @walk-rules@) this deep hash runs once per report definition per AST node. Worse, the key mutates after insertion: @registerVariable@ obtains the pool via @scope.getVariables(this, true)@, constructs the @Variable@ — whose @reset()@ calls @putCachedInstance(scope, key, ce)@ — and only *then* executes @pool.put(name, var)@ (line 602), filing the entry under a hash the next statement invalidates. @Scope.getVariableList@ lazily materialises @variableList@, changing a live key's hash again. And because @Variable.equals@ ignores @value@, a per-thread @Scope@ clone compares equal to its master and @RuleContainer.apply@ installs that clone as the current scope (line 667), so @instCache.get(clone)@ returns the master's map — defeating the thread isolation the clone exists for.
* *[MAJOR]* _performance_ @Expression@.@getCompiledInstance@: the *cache-hit* branch no longer short-circuits. Where trunk did @compiled = instance@ unless @RT_CLONE@ was set, it now unconditionally does @instance.clone(sr)@ followed by @getCompiler().instantiate(compiled, resolver.get())@, re-allocating the @libs@/@vars@ arrays, re-walking the enclosing-scope chain per symbol, and calling @Variable.clone()@ for every global variable. The method-entry short-circuit @if (!dirty && compiled != null)@ is retained, so this is once per @Expression@ object — but @Rule.clone()@ (Rule.java:633-634) creates a fresh @Expression@ per rule for every worker thread, multiplying the whole rule base by thread count, and @AstSymbolResolver.execute(String)@ constructs a brand-new @Expression@ on *every* call, so for @exec()@ in @consolidated_reports.xml@ walk-rules it really is per report definition per AST node.
* *[MAJOR]* _functional_ @Variable@.@clone@: any *global* TRPL variable not marked @multiThreadAccess="true"@ is silently given a per-thread copy at @Compiler.instantiate@ time (Compiler.java:1252-1262), so values accumulated by per-file rules on worker threads are invisible to the main thread's global post-rules. The only guard is manual annotation across ~40 rules files, with no validation. Concrete instance: @rules/reports/simple_search.xml@ declares the global @rows@ (line 87) with no @multiThreadAccess@, creates it in global init-rules (line 93, under @isInitializingGlobals@, so the master holds the list), appends to it from the @<rule-set input="tree">@ walk-rules (line 116) on worker threads, and reads it from the *global* post-rules on the main thread (@storeObject("results", rows)@, line 126) — the main thread's own copy is a deep copy of the still-empty master, so the search result set is empty. The symptom is currently masked in both callers by the missing-@startThreads@ hang above; fixing that exposes this.
* *[MAJOR]* _functional_ @BaseRuleContainer@.@ruleList@: the @threadSpecificRules@/@threadSpecificFunctions@/@threadSpecificIncludes@/@threadSpecificWinners@ maps are keyed by @Thread.currentThread().getId()@ and are only bypassed when @PatternEngine.isInitializingGlobals@ is true, which is set *only* inside @PatternEngine.run(String)@ (lines 1046/1074). The runtime-server path reaches these through @DynamicQueryHelper@/@DynamicTablesHelper@/@DynamicValidationHelper@ → @ConversionPool.runTask@ → @popEngine@ → @PatternEngine.run()@ (no-arg, line 950), which neither sets the flag nor calls @finish()@ — @finish()@ is invoked from exactly one place, the @run(String)@ finally at line 1138 — and @runTask@'s finally only does @clearStoredObjects()@ + @pushEngine@. @BaseRuleContainer.cleanup()@, the only thing that clears these maps, therefore never runs for pooled runtime engines: every distinct server thread performing a dynamic query/temp-table conversion permanently adds a deep clone of the entire profile ruleset to pool-lifetime maps that nothing evicts — unbounded heap growth plus a deep-clone latency spike per new session thread. The runtime constructor @PatternEngine(String, boolean)@ has the same gap.
* *[MAJOR]* _functional_ @AstSymbolResolver@.@workers@: changing the registry from @LinkedHashMap@ to @ConcurrentHashMap@ (line 289) breaks the invariant the surrounding code exists to enforce — @workers()@'s own javadoc states "Iteration is in the order of registration" and it builds a custom key-set-driven iterator solely "because the order of iteration is important". Concrete trigger: @rules/annotations/preselect_prep.rules@ lines 123-124 register @IndexSelectionWorker@ (@isw@) then @P2OAccessWorker@ (@p2o@); both implement @visitAst@ and both call @P2OLookup.loadTemporarySchema@, a last-writer-wins operation guarded by @wa.lastTempFilename@ (P2OLookup.java:585-605), and they pass *different* artifacts (@ast.getArtifact()@ vs @ArtifactManager.getOriginalArtifact(artifact)@). Under @LinkedHashMap@ @P2OAccessWorker@ ran last and its schema load won; under @ConcurrentHashMap@ the spread hashes of the two FQCNs place @P2OAccessWorker@ in a lower bucket for every plausible table size, so @IndexSelectionWorker@ now runs last and a different temporary schema is left loaded. @PatternEngine.finish()@ (line 1245) is reordered the same way.
* *[MAJOR]* _performance_ @Configuration@.@withFileProfile@: @PatternEngine.run@ calls @config.withFileProfile(artifact)@ on the main thread inside the enqueue loop (line 1084), but the corresponding @AstProcessorJob@ runs asynchronously later (lines 1116-1118). In trunk this call immediately preceded @processAst(artifact)@ on the same thread; the diff kept the call in place while moving the work off-thread. @withFileProfile@ ends in @setDefaultProfile@ (Configuration.java:938) → @loadProfile@, which does @this.parmMap.clear()@ then @putAll@/@putParameter@ on the *singleton's* @LinkedHashMap@, plus @includeRemap.clear()@, @fileSet@ and @cvtpath@ resets, and finally mutates @activeProfile@. In a project with per-file conversion profiles (@activeProfiles != null@) every job therefore executes under whatever profile the main thread most recently installed, and the unsynchronized @clear()@/@putAll()@ races with concurrent @Configuration.getParameter@ reads on worker threads — corrupt reads, not merely stale ones. Effectively total breakage of the multi-profile feature under multi-threaded conversion.
* *[MAJOR]* _performance_ @CommonAstSupport.Library@.@getBaseArtifactsIterator@: iterates the engine's entire @getTargetPaths()@ and calls @AstManager.get().loadTree(artifact)@ for each purely to read @getArtifact()@, discarding the tree. @XmlFilePlugin.loadTree@ has no cache, so each call is a full @XmlHelper.parse@ DOM parse plus a full recursive @readAst@/@readAnnotations@ reconstruction. This is entirely gratuitous: @AnnotatedAst.getArtifact()@ (line 2936) is nothing but @AstManager.get().getTreeArtifact(id)@, a registry map lookup, so the same result is obtainable via @getTreeArtifact(getTreeId(artifact))@ with no I/O. Invoked from @rules/convert/proxy_programs.xml@:1026 in the global @post-rules@, serially on the main thread, and re-executed for every proxy pass.
* *[MAJOR]* _functional_ @CallGraphGenerator@.@generateGraphs@: calls @PatternEngine.setSingleThreadedMode(true)@ (line 177, also @updateGraphs@ at 140) and never restores the previous value. @TransformDriver.front()@ starts the worker threads at line 340 and then calls @runCallGraphGenerator()@ at line 412 when @mode.callgraph@ is set, so from that point every remaining phase (@middle()@, @back()@, brew) runs single-threaded on the main thread while the already-started @AstProcessorThread@s idle-poll @queue@ until @shutdownThreads()@ at line 1231. Mode @F3@ (ConversionDriver.java:380) sets @callgraph=true@, so the usual full-conversion mode string @F3+M1+CB@ hits this on every run — the concurrency feature is silently disabled for every call-graph-enabled project.
* *[MAJOR]* _functional_ @GenerateLegacyProxyOpenClient@.@main@: passes @numThreads = -1@ into the new @JobDefinition@ parameter (line 290), so @TransformDriver.front()@ calls @PatternEngine.startThreads(-1, source.size())@ and @calculateOptimalThreads@ selects @min(fileCount, 3)@ for 4-12 files and up to 16 above — contradicting the javadoc this same change added to the class ("This process is executed sequentially on a single thread", line 103) and the identical claim added to @rules/annotations/legacy_services.rules@ (lines 40, 98). Trigger: running the proxy generator over 4 or more configured @.xpxg@ files; to honour the documented contract the value must be @1@.
* *[MAJOR]* _functional_ @DatabaseService@.@DatabaseService@: flipping @MV_STORE=FALSE@ to @MV_STORE=TRUE@ in the @jdbc:h2:...rptdb/rptdb@ URL (matched in @ReportWorker.openConnection@) is *silently ignored* on any installation that already has a @rptdb.h2.db@: fwd-h2's @Database.open()@ contains @if (existsPage && !existsMv) { dbSettings.setMvStore(false); }@, so such installs keep running on PageStore forever with no migration path and no warning. The consequence is not data loss but that the premise of this branch's report multi-threading is defeated exactly where it matters: under PageStore @RegularTable@ takes an exclusive @lockExclusiveSession@ held for the whole transaction, and the transaction now spans a whole file (commit in @postprocessFile@), so the 16 worker connections serialize on @source_line@/@match@ and are liable to hit @DEFAULT_LOCK_TIMEOUT=10000@ "Timeout trying to lock table" failures — which, per the @AstProcessorThread@ swallow above, are logged and skipped rather than failing the run. The retained @LOCK_MODE=3@ is a no-op under MVStore. A documented delete/rebuild (or export/import) step for @rptdb.h2.db@ is needed.
* *[MAJOR]* _functional_ @proxy_programs@: the new global @post-rules@ transplant loop drains the field bag with @temp.getImmediateChildren(java.method_def)@ (line 1060), but @sharedFldBag@ is only ever populated with @java.assign@ nodes — @fldAnchor@ (set to @sharedFldBag@ at line 341, the non-persistent AppObject path) is used as a parent exactly once, at line 711 @pref = createJavaAst(java.assign, "=", fldAnchor)@. @AnnotatedAst.getImmediateChildren(int)@ filters strictly on exact type, so the iterator is always empty and the @public static final String@ constants stay in the throwaway @<relpath>.bag@ file, which is never persisted. The removed @fldAnchor = ref.getImmediateChild(java.cs_constants, null)@ binding put them into the emitted class. Trigger: any non-persistent legacy proxy program declaring a TEMP-TABLE, WORK-TABLE or DATASET — the generated AppObject silently loses the name constants it previously published to OpenClient consumers. The persistent path (line 424) is unaffected.
* *[MAJOR]* _functional_ @cross_namespace_conflicts@: moving the inline loop into @CommonAstSupport.Library.processClassVariables@ dropped the project-folder mapping — the removed rule persisted with @art.getPathToProjectFolder(aref.artifact.getRelativePath(), ".ast")@ (the "DDF 20260108 .ast file path was not using the project folder" fix, whose header entry is still in the file) while the Java helper uses @String.format("%s.ast", aref.getArtifact().getRelativePath())@ (CommonAstSupport.java:6891) and then @persist(aref, filename, true, false)@. That overload (line 3739) only calls @ArtifactManager.addArtifact(filename)@, which does @normalizeFilename@/@absoluteFilename@ and no project-folder rewrite; the @projectRelative@ flag is ignored entirely by the terminal overload. After the refactor the rules file contains no @persist@ call at all, so the Java helper is the sole writer: the super-class AST of every class with inherited variables is written next to the 4GL source instead of into the project folder.
* *[MAJOR]* _functional_ @consolidated_reports@.@addRowByDir@: the "Lines of Code Analysis By Directory" report no longer aggregates by directory — it emits one row per source file. @addRowByDir@ was moved into the per-file @post-rules@ (lines 432-435) while @pathLOC@/@pathIncl@/@pathDir@/@pathFiles@ are reset to 0 unconditionally in the per-file @init-rules@ (lines 285-288) and are non-@multiThreadAccess@ (hence per-thread); the @lastPath@ carry-over that used to accumulate across files in a directory was deleted, as was the flush in the global post-rules. Every file produces a row labelled @"<dir> (1 files)"@ carrying only that file's LOC, and a directory with N files appears N times. This regression is *not* thread-dependent — it reproduces single-threaded too. Trigger: any project with 2+ source files in one directory.
* *[MAJOR]* _functional_ @consolidated_reports@.@post-rules@: report row order is no longer deterministic. @rowsByFile@ and @rowsByDir@ are now @createSynchronizedList()@ appended from the per-file post-rules of parallel workers, so append order is thread-completion order; @ReportWorker.generateCustomReport@ assigns @report_cell.rowpos@ from the list index and @ReportApi@ serves rows with @"order by cell.rowpos, col.idx"@ (line 252), so both LOC reports render in a different row order on every conversion run and can no longer be diffed between runs.
* *[MAJOR]* _functional_ @SoapConfig@.@addOperation@: unsynchronized mutation of a shared @ArrayList@ concurrent with a synchronized iteration of the same list. @ServiceSupport.getSoapOperation@ (line 1629) is now @synchronized@ and iterates @SoapConfig.operations@ (a plain @ArrayList@, SoapConfig.java:92/170) for every configured @.wsm@, while @ServiceSupport.registerSoapOperation@ (line 1657) is *not* synchronized and calls @child.soap.addOperation(child)@ → @operations.add(op)@ (line 150) after the monitor is released; it also mutates shared @SoapOperation.service@ fields outside the monitor. Both are reached from @Library.getSoapOperations@, which @rules/annotations/legacy_services.rules@ walk-rules invoke for *every* @procedure@/@function@/@method_def@/@constructor@ of every artifact (wired in at @annotations.xml@:664), so two workers processing different programs iterate and mutate the same list concurrently: @ConcurrentModificationException@ (swallowed by @AstProcessorThread@, taking the file's remaining annotations with it) or an operation missing from the generated WSDL. Trigger needs only one @.wsm@ with a persistent @Procedure@.
* *[MAJOR]* _functional_ @stable_naming@.post-rules: the suffix counters were downgraded from H2-persisted maps to run-local @HashMap@s — Section B uses @ifaceSuffixMap = create("java.util.HashMap")@ (line 426) in place of the deleted @createString2IntMap('pre_nextTTSuffixMap')@ and Section D uses @frameSuffixMap = create("java.util.HashMap")@ (line 594) in place of the deleted @createString2IntMap("nextFrameSuffixMap")@, while @preTmpTabNames@ and @frameInterfaces@ stay H2-persisted. Concrete trigger: an incremental run (no @cleanAll()@) restarts the counters at 1 against a database still holding @Tt1_1@/@FrameFoo_1@, so a newly added temp-table or shared frame is handed a name already owned by an unchanged file — two distinct @AstKey@s/@FrameAstKey@s then resolve to the same Java interface name. Neither section validates the generated name against the persisted map's existing *values*; contrast Section A, which guards via @while convertedClasses.values().contains(classfile)@. Compounded by the @fileId == -1@ defect below, the stale owners survive indefinitely.
* *[MAJOR]* _functional_ @stable_naming@: rule-set 2 registers temp-table interface candidates under the composite key @sprintf("%s|%s", artifact.getRelativePath(), text.toLowerCase())@ (line 230) into @pendingTTIface@/@pendingTTIfaceBaseName@ with a plain @put@ (last writer wins), so two temp/work-table definitions with the same 4GL name but different structures inside one source file collapse to one entry. @SchemaDictionary.addTableEntry@ does @database.addChild(ast)@ unconditionally for *every* @TEMP_TABLE@/@WORK_TABLE@, so a file defining e.g. @tt1@ with different fields in two internal procedures yields two @TEMP_TABLE@ children with the same lowercased text. The losing @AstKey@ never gets an entry in @preTmpTabNames@, so @p2o_pre.xml@ annotates @tt_interface = null@, Section C's @tt_interface != null@ guard drops it from @grouped@, and @p2o.xml@ computes @key = sprintf('%s_%s', path, null)@ and stores a null DMO implementation name into @tmpTabNames@. The replaced @p2o_pre.xml@ logic keyed on the @AstKey@ alone and produced @Tt1_1@/@Tt1_2@.
* *[MAJOR]* _functional_ @stable_naming@ rule-set 4: the mock-frame key is produced from @biName = names.convert(ref.getText(), names.class, names.standard, false, null)@ where @ref@ is the @SYMBOL@ child of the @DEFINE SHARED FRAME@ — the raw 4GL source text with its original case — but @frame_generator_pre.processForm@ builds the same key from @biName = name.convert(fName, …)@ after the newly added @fName = fName.toLowerCase()@ applied to the @frame_alloc@ @name@ annotation, which @frame_scoping.rules@:1205 already stores lowercased. @NameConverter.preprocess()@ splits on camel-case boundaries, so @"custFrame"@ → @CustFrame@ whereas @"custframe"@ → @Custframe@. Concrete trigger: @DEFINE SHARED FRAME custFrame.@ (or any NEW SHARED FRAME left in @unFormedFrames@) — producer key @…|CustFrame@ vs consumer key @…|Custframe@, so @mockInterfaceNames.get(key)@ returns @null@, @fAlloc.putAnnotation("fr_interface", null)@ is emitted, and @masterFrames.contains(null)@/@orphanFrames.add(null)@ then run on a null name. Trunk had a single producer/consumer so no mismatch was possible.
* *[MAJOR]* _functional_ @stable_naming@.post-rules: Section A writes @convertedClasses.put(key, classfile)@/@classNames.put(key, classname)@ — and Sections B-D likewise write @preTmpTabNames@, @generationMap@, @DMONamesMap@, @frameInterfaces@, @mockInterfaceNames@ — from the cfg-level @<post-rules>@, which @PatternEngine.run()@ executes immediately after @resolver.setAsts(null, null)@ (line 1128). @H2Map.logChange@ reads @resolver.getSourceAst()@, finds null, and records every one of these work-table rows with @fileId = -1, astId = -1@ (H2Map.java:561-569); @ConversionData.clean(artifact)@ (line 271) only issues @delete from <table>__WORK where fileid = ?@ for the artifact's real tree ids, so the stale mappings survive an incremental re-conversion — trunk put the class-name entry from @naming.rules@ per-file init-rules, where @setAsts(source, copy)@ had already run and the row carried the file's real @fileId@. Combined with Section A's dedup loop not excluding the artifact's own previously-registered value, two source files whose converted class names collide are re-suffixed on *every* incremental run in which both are in the change set (@Foo@/@Foo_1@ → @Foo_2@/@Foo@ → …), and @annotations/i18n.rules@ (lines 185/189/223/227 build @<fdir>/<classname>[_<lang>].po@ from the @classname@ note) emits fresh empty @.po@ files under each new name, orphaning the existing hand-translated catalogs and @<Class>_<Language>@ bundles. Deleted sources leave permanently-orphaned rows for the same reason, keeping their names reserved and forcing spurious suffixes onto unrelated new files.
* *[MAJOR]* _functional_ @p2o@: the new @AstKey@ lock is acquired at @lock = tmpTabKey.getLock(); lock.lock()@ (p2o.xml:834-836) and released ~500 lines later at @<rule>lock != null <action>lock.unlock()</action>@ (1332-1334), with no try/finally equivalent. Trigger: the explicit @throwException("Prototype AST doesn't match the interface of TEMP-TABLE")@ at line 928 (and its sibling at 938) sits between the two and fires whenever a temp-table's prototype AST does not match the cached one. The exception unwinds the walk; @PatternEngine.apply@'s @finally@ only calls @resolver.resetVariables(ruleSet, true)@, which nulls the @lock@ variable *without unlocking* (see the @AstKey.reset@ finding), and @AstProcessorJob.run@ catches it via @handleRunThrowable@. With @throwFailed == false@ the engine keeps dispatching jobs, so the @ReentrantLock@ cached in @AstKey.LOCKS_CACHE@ stays held forever and the next worker reaching @lock.lock()@ for an equal (or hash-colliding) key blocks permanently — the conversion hangs on @latch.await()@. Reachable because @ConversionDriver.middle()@ runs @schema/p2o@ over @sschemas@ — one @.schema@ artifact per 4GL source — so many threads concurrently build @AstKey@s for identical temp-table definitions and share the cached lock.
* *[MAJOR]* _functional_ @AstKey@.@reset@: the @Resettable@ auto-release safety net is dead code — @SymbolResolver.resetVariables@ (expr/SymbolResolver.java:683) invokes @((Resettable) var.getValue()).reset()@ only when @scope.isAutoRelease()@, and the only implementation returning @true@ is @BaseRuleContainer.isAutoRelease()@ (line 232), which is never passed to @resetVariables@. Exhaustive grep finds exactly four call sites: @PatternEngine.apply@ (1749, 1808) with a @RuleSet@, which inherits @RuleContainer.isAutoRelease() == false@ (RuleContainer.java:298), and @NamedFunction@ (442, 490) with @this@, which returns @false@ (NamedFunction.java:199). The @p2o.xml@ temp-table lock therefore has no recovery path on any abnormal exit.
* *[MAJOR]* _functional_ @collect_procedures@.init-rules: the duplicate-basename check was inverted from @extProcBasenames.contains(basename)@ (with @<action on="false">…add(basename)</action>@) to @extProcBasenames.add(basename)@ as the rule condition. @extProcBasenames@ is @createStringSet("extProcBasenames")@ → @H2Set@, which @extends TreeSet@ and whose @add()@ (H2Set.java:257) is @return super.add(key)@ — plain @TreeSet@ semantics, @true@ on *first* insertion (the method's javadoc "true if the key already exists" is wrong and is the likely source of the mistake). So the "WARNING: multiple external procedures share basename" message now fires for every external procedure in the project and never for a real collision, and the collisions consumed by @run_stmt_targeting.rules@:141 go unreported. It needs @not extProcBasenames.add(basename)@.
* *[MAJOR]* _functional_ @H2Map@.@put@: per-method @synchronized@ gives callers no way to make the get-then-put idiom atomic, which the rules rely on. Concrete trigger: @rules/fixups/functions_procedures.rules@:186-199 (walk-rules in the @fixups/post_parse_fixups@ profile, which runs multi-threaded; the file uses no @getLock@) does @prevcls = funcRetTypes.get(fname)@ … @dupfuncRetTypes.add(fname)@ … @funcRetTypes.put(fname, cls)@ with the read and write in separate monitor acquisitions. @funcRetTypes@/@dupfuncRetTypes@ are declared @multiThreadAccess="true"@ (post_parse_fixups.xml:215-216) so they are one shared instance: two workers defining the same function name with different return types in different files both observe @prevcls == null@, the duplicate is never recorded, @post_parse_fixups.xml@:957 (@funcRetTypes.keySet().removeAll(dupfuncRetTypes)@) does not evict the key, and @annotations/functions.rules@:167 emits the return type of whichever worker won the race.
* *[MAJOR]* _functional_ @H2Map@.@persist@: change-log replay order is no longer stable across runs. @LoggedCollection.changes@ is a @LinkedHashMap<AstKey, Object>@ ordered by the order workers first called @logChange@; @LoggedCollection.persist@ (lines 229-240) feeds @changes.keySet()@ straight to @DBHelper.persistCollection@, so rows land in @<table>__work@ (@pos BIGINT AUTO_INCREMENT@) in that order; the @H2Map@ constructor replays with @select … order by pos@ doing @super.put(key, value)@ (line 240), so the last row for a duplicate key wins. The work table's unique index is @(fileId, astId, key)@, so two files writing the same map key produce two surviving rows and the winner is decided purely by @pos@. With parallel processing the @logChange@ interleaving differs between runs, so the state restored by the next incremental conversion differs; under the previous single-threaded engine the order was the deterministic @targetPaths@ order. @funcRetTypes@ is a concrete duplicate-key case.
* *[MAJOR]* _performance_ @SchemaWorker.SchemaSupport@.@areMetaTables@ (SchemaWorker.java:1351-1360 — the changed method; there is no @isMetaJoin@): @metaNames@ was made an immutable @Collections.unmodifiableSet@, yet the two @contains@ calls were wrapped in @synchronized (SchemaWorker.this)@ — a global monitor for a pure read of an immutable set on the hot join-analysis path. @areMetaTables@ is called from @recordJoin@ (887), @checkJoin@ (970) and @where_clause_prep.rules@:776, all driven per record/join reference by the multi-threaded @schema/annotations@ and @annotations@ profiles. The same @SchemaWorker.this@ monitor is held by @loadNonDefaults@ (694-700) across @SchemaLoader@ file I/O, so every worker's join analysis stalls behind any thread loading non-default schemas. @getLegacyTableName@ (668) was also made @synchronized@ — on the shared @SchemaSupport@ instance, a *different* monitor — although its body now reads only per-thread state (@context.get().dictionary@), so that monitor guards nothing while serializing @collect_names.rules@:478/542, @method_definitions.rules@:530/603 and @proxy_programs.xml@:172.
* *[MAJOR]* _performance_ @P2OLookup@.@getLegacyFieldNames@: made @synchronized@ (line 1591) on a @P2OLookup@ instance shared across all worker threads via the @private static final permSchemaMap@ (line 326; only the temp-table lookup is per-@WorkArea@), while the body scans the *entire* @javaNameMap@ keySet with @startsWith@ and allocates a new @LinkedHashSet@ on every call, with no memoization. @getProperties@ (1672) does the same over @propsMap.values()@ with an @Aast.getParent().getAnnotation("historical")@ per entry. Trigger: @rules/annotations/record_field_expansion.rules@:137 calls @p2o.legacyFieldNames(sname, false)@ per expanded record reference across every AST in the multi-threaded @annotations@ profile; @proxy_programs.xml@:777 calls @p2o.properties(...)@ per proxied table. Every worker serializes on one monitor while doing O(schema-size) work per reference.
* *[MAJOR]* _functional_ @ReportWorker@.@storeSourceFile@: the new batch flush calls @wa.connection.commit()@ every 1000 source lines (and again after the loop) on the same connection used for the whole file, breaking the per-file atomicity that @postprocessFile()@ implements and that the @catch@ block's @wa.connection.rollback()@ relies on. Trigger: any @IOException@/@SQLException@ in the *second* loop — @new FileReader(readerArtifact.getRelativePath())@ on a missing @.cache@ artifact, or an H2 lock timeout now that N threads insert into @source_line@ concurrently — after the base-source loop has already committed. The rollback then undoes nothing: a @file@ row, an @ast_map@ row and a complete base @source_line@ set stay committed for a file whose preprocessed lines are absent. The damage survives the run, since @AstProcessorThread@ swallows the resulting @AstException@ and the run finishes "successfully" with a corrupt partial file in the report DB. @addMatchCategory@'s new @commit()@ has the same effect mid-walk.
* *[MAJOR]* _performance_ @TransformDriver@.@executeJob@: @PatternEngine.shutdownThreads()@ is the last statement of the @try@ block (line 1231) while the @finally@ (1238-1244) only calls @ConversionData.disconnect()@ and @printElapsed@. @AstProcessorThread@ never calls @setDaemon(true)@, so the workers are non-daemon. The @catch (Exception)@ at 1233 does @System.exit(1)@, so the checked path is safe, but an @Error@ — @OutOfMemoryError@, or the @StackOverflowError@ deep AST/TRPL recursion routinely produces on the main thread during @middle()@/@back()@/brew — is not intercepted, skips line 1231, unwinds through @ConversionDriver.main@ (which has no @System.exit@ after @executeJob@), and leaves the workers alive so the JVM never exits. @shutdownThreads()@ belongs in the @finally@.
* *[MAJOR]* _style_ @ServiceSupport@: file gained substantial new functionality in this diff (new @getContext@/@setContext@ methods, @isAutoRelease@ override, @synchronized@ added to many methods, @registerVariable@ signature change) but no new history entry was added; last entry remains "015 ... DDF 20251104".
* *[MAJOR]* _style_ @NameConverter@: header copyright still reads "Copyright (c) 2005-2025" although the diff modifies the file as of 2026-06-18; should be advanced to include 2026.
* *[MAJOR]* _style_ @NameMappingWorker@: header copyright still reads "Copyright (c) 2006-2025" although the diff modifies the file as of 2026-02-18; should be advanced to include 2026.
* *[MAJOR]* _style_ @ImportWorker@: file content was modified (trailing blank line removed at EOF) but no new header history entry was added, unlike every other modified file in this diff.
* *[MAJOR]* _style_ @proxy_programs@: new history continuation line "**         20260630" blanks out both the sequence number AND the author, unlike this file's own convention of blanking only the number while keeping the author.
* *[MAJOR]* _style_ @fixups@ (schema): the new continuation history line "Added resetIds flag to brainwash method call." is missing the leading "**" comment-block prefix used by every other line in the header history block.
* *[MAJOR]* _style_ @PatternEngine@: header history entry "AOG 20260220" continuation line uses a single "*" instead of "**" like every other continuation line in the block, breaking the header comment format.
* *[MAJOR]* _style_ @SymbolResolver@ (expr): @instCache@ field declaration is 129 characters, exceeding the 110-char limit.
* *[MAJOR]* _style_ @SymbolResolver@.@isAutoRelease@ (expr): anonymous @Scope@ implementation in @TestResolver@ places the opening brace on the same line as the method signature, violating the file's Allman brace style.
* *[MAJOR]* _style_ @BufferScopeWorker@.@getSuperTables@: a blank line was inserted between the method's javadoc block and its signature, violating the "no blank line between comment and subject" rule.
* *[MAJOR]* _style_ @ConversionDriver@: added @import com.goldencode.artifacts.ExplicitFileList;@ is redundant given the existing @com.goldencode.artifacts.*@ wildcard, with no conflict justifying the explicit import.
* *[MAJOR]* _style_ @ConversionDriver@.@front@: new @processTrees("Stable naming (File and Frame Names)", ...)@ call's wrapped parameters are not aligned under the call's opening parenthesis column, unlike a correctly-aligned nested call a few lines below.
* *[MAJOR]* _style_ @NameConverter@: new import @com.goldencode.p2j.security.ContextLocal@ is inserted out of the file's java.*/org.reflections.*/com.goldencode.* grouping order and uses an explicit single-class import instead of the wildcard used for every other FWD package in this file.
* *[MAJOR]* _style_ @NameConverter@.@WorkArea@: new public no-arg constructor @public WorkArea() { }@ has no javadoc, unlike the equivalent constructor added in @NameConverterWorker@ which carries a "Default constructor." javadoc.
* *[MAJOR]* _style_ @NameMappingWorker@.@Library@: wrapped @XmlPatternWorker.createAst(...)@ parameters in @storeMappings@/@restoreMappings@ were re-indented to a small fixed indent instead of aligning under the opening parenthesis column as before this change.
* *[MAJOR]* _style_ @I18nWorker@.@resolveTMTranslation@: adding the @synchronized@ modifier pushed the method signature to 114 characters, exceeding the 110-character limit.
* *[MAJOR]* _style_ @ConversionData@: newly modified static methods consistently use modifier order @public synchronized static@ (e.g. @clean@, @cleanAll@, @connect@, @getTempIdx@, @createMapToMap@), contradicting the prevailing codebase convention of @public static synchronized@.
* *[MAJOR]* _style_ @ConversionData@.@createMapToMap@: wrapped-parameter continuation lines were left at their old column after @synchronized@ was inserted into the signature, no longer aligning under the first parameter.
* *[MAJOR]* _style_ @ConversionData@: several public static methods touching the same shared static state (@connected@, @saveServiceHash@, @getArtifactData@, @cleanupArtifacts@, @cleanupDeletedArtifacts@, @isTableSchemaModified@, @containsModifiedSchemaTable@) were left without @synchronized@ while ~25 sibling static methods were made synchronized in this same pass, applying the new thread-safety pattern inconsistently.
* *[MAJOR]* _style_ @H2ChildMap@: no-op @persist()@ override was not made @synchronized@, unlike the equivalent methods in sibling classes @H2Map@, @H2Set@, @H2MapToMap@, @H2MapToSet@ that were synchronized in this same rework.
* *[MAJOR]* _style_ @H2ChildSet@: no-op @persist()@ override was not made @synchronized@, same inconsistency as @H2ChildMap@.
* *[MAJOR]* _style_ @AstProcessorJob@.@AstProcessorJob@: constructor parameter list line is 121 characters, exceeding the 110-char limit; should wrap one parameter per line.
* *[MAJOR]* _style_ @BaseRuleContainer@.@ruleList@: public method javadoc omits the required @@return@ tag for the returned @ArrayList<RuleListElement>@.
* *[MAJOR]* _style_ @CommonAstSupport@.@ensureEntry@: public method javadoc is missing both @@return@ (returns @Object@) and @@throws@ (declares @throws ReflectiveOperationException@).
* *[MAJOR]* _style_ @CommonAstSupport@.@setConvertedClassName@: javadoc has a duplicated @@param legacyName@ tag with no description on the first occurrence.
* *[MAJOR]* _style_ @RuleContainer@.@getIncludes@: javadoc says "Returns the set of included rule containers" and references @{@link Set}@, but the field/return type was changed from @Set@ to @List@ in this same diff.
* *[MAJOR]* _style_ @PatternEngine@.@getConvertedFilenameWithoutExtension@: javadoc is malformed (stray "<p>    *" line artifact) and documents @@param path@ before @@param converter@, though the signature is @(NameConverter converter, String path)@.
* *[MAJOR]* _style_ @ReportWorker@.@initialValue@: missing @@Override@ annotation on the anonymous @ContextLocal@'s @initialValue()@ method, inconsistent with the identical pattern added in @DDLGeneratorWorker@ (which has @@Override@).
* *[MAJOR]* _style_ @SchemaWorker@.@context@: new anonymous @ContextLocal<WorkArea>@ field initializer and its @initialValue()@ override place the opening brace on the same line, violating Allman style used by the identical pattern in @JavaPatternWorker@ and @SymbolResolver@ (uast) in this same diff.
* *[MAJOR]* _style_ @SymbolResolver@.@loadClass@ (uast): re-indented @String.format(...)@ continuation lines no longer align under the opening parenthesis column, breaking the wrap-by-column rule that was correctly followed before this edit.
* *[MAJOR]* _style_ @CallGraphGenerator@.@main@: wrapped @RuleContainer.MSG_NONE@/@MSG_TRACE@ continuation lines in the @syntax(...)@ call are no longer aligned with the call's opening-paren column.

h2. Minor

* *[MINOR]* _functional_ @PatternEngine@.@getCombinedResults@ (lines 2387-2425): in multi-threaded mode it broadcasts @disableNewFileTracking@ only to the worker threads and aggregates only their @JavaPatternWorker.getPersistedArtifacts()@; @JavaPatternWorker.context@ is a @ContextLocal@ (only @autoload@ is shared through @EngineState@), so the main thread's @partifacts@ is neither returned nor disabled. @ConversionDriver.generateFrames@ (1060), @generateDMOs@ (1172), @generatePOJOs@ (1228) and @generateMenus@ (1279) each call @enableNewFileTracking()@ on the main thread and nothing ever closes that session — @WorkArea.track@ stays @true@ for the rest of the JVM run and its collection is retained but never read. No artifact is actually lost today (no cfg-level init/post rule in @frame_generator.xml@, @menu_generator.xml@, @java_dmo.xml@ or @java_pojo.xml@ persists a JAST), but the enable/disable pairing is broken for anything that later does. Merge order is also thread-partition order, making @frames@/@dmoList@/@menus@ ordering non-reproducible.
* *[MINOR]* _functional_ @PatternEngine@.@getConvertedFilenameWithoutExtension@ (lines 1392-1396): @path.substring(2).indexOf('.') + 2@ truncates at the first @'.'@ at or after index 2, not at the extension. Its only caller, @ArtifactManager.filterConflictingArtifacts@ (line 1499), uses the result as the grouping key for name-conflict detection, feeding @isConflicting()@ → @isCurrentArtifactConflicting()@ in @stable_naming.xml@:173/181. Three concrete mis-groupings: a dotted first path component (@app.core/foo.p.ast@ and @app.core/bar.p.ast@ both truncate to @"app"@, collapsing the whole directory into one conflict group); an embedded dot in the filename (@d/cust.upd.p.ast@ and @d/cust.p.ast@ both truncate to @"d/cust"@ — false positive); and a one-character basename at the project root (@f.p.ast@ → @"f.p"@, @f.w.ast@ → @"f.w"@ — a genuine collision *not* detected). The magic @2@ only behaves for paths carrying a @"./"@ prefix, which artifact relative paths do not have.
* *[MINOR]* _functional_ @PatternEngine@.@finish@ (lines 1263-1266): @addThreadJob(AstSymbolResolver::reset)@ is queued after the main thread already ran @AstSymbolResolver.reset()@ at line 1261, and the newly-added body dereferences the context-local unconditionally (@resolver.get().clearASTs()@ — AstSymbolResolver.java:503-507; trunk only did @context.set(null)@). Each worker's context-local resolver is left @null@ by the previous profile's queued reset and is only re-populated by the @setCurrentState@ job at line 1062. Concrete trigger: any failure between @initialize(profile)@ (1047) and @threadLatch.await()@ (1072) — e.g. a @ConfigurationException@ from @ConfigLoader.load@ or a TRPL error in a cfg-level @<init-rules>@ — takes the @finally@ path to @finish()@ while the per-thread @setCurrentState@ never ran, so the queued reset NPEs on every worker, is swallowed, and prints the misleading @"Job execution failed: null"@, masking the real diagnostic.
* *[MINOR]* _functional_ @NameConverter@.@resolvePossibleKeywordConflict@: the @TYPE_CLASS@ branch now calls @isDialectKeyword(true, lcPossible)@ (line 2159), which ignores @wa.currentDialects@ and tests the union of *every* dialect ever registered in the static @reservedSQL@ map; trunk tested only the set left by the most recent @resetDialects(schema)@. Concrete trigger: @ConversionDriver.middle()@ runs @schema/p2o@ over the permanent DB schemas first (p2o.xml:771 → @setCurrentDialects@, registering e.g. @P2JPostgreSQLDialect@), then over the temp-table @.p.schema@ files whose @_temp@ database falls through to the @h2@ default and is therefore the *last* reset; program class names are computed later in @ConversionDriver.back()@ at @annotations/annotations@ via @naming.rules@:203 → @convert_classname@. So in trunk the effective set was ANSI ∪ h2 and @analyse.p@ → @Analyse@; now the PostgreSQL-only keyword @"analyse"@ (@P2JPostgreSQLDialect.java:534@, absent from H2's and the ANSI @global@ list) matches and the name becomes @Analyse_@. Downstream, @i18n.rules@:176-190 names catalogs @translations/<pkg>/<classname>[_<lang>].po@ and @TranslationManager.resolveBundle@ uses @referentClass.getName()@ as the bundle base (line 572), so the existing @Analyse.po@ is silently orphaned. The widening looks deliberate (revision "049 AOG 20250922 … we check all dialects") but no migration of @translations/@ is performed and the rename is not reported.
* *[MINOR]* _performance_ @BaseRuleContainer@.@ruleList@/@getIncludes@/@getFunctionLibrary@/@getWinners@: each of the four overrides boxes @Thread.currentThread().getId()@ into a @Long@ (thread ids are almost always > 127, outside the @Long@ cache — a fresh allocation per call) and then performs a @containsKey@ followed by a separate @get@ on a @ConcurrentHashMap@ — two hash lookups plus an allocation where a @ThreadLocal@ field, or a single @get@ with a null check, would suffice. When the super value is @null@ nothing is stored, so the miss path is re-walked on every call. Reached once per AST from @PatternEngine.processAst@ and on every named-function lookup that misses the calling container's @winners@ cache and walks up to the base container via @RuleContainer.getFunctionContainer@ (three boxed double-lookups per root visit).
* *[MINOR]* _performance_ @RuleContainer@.@getFunctionContainer@: the recursion guard became a per-container @ThreadLocal<Boolean>@ (line 231) with @set(true)@/@set(false)@ on every invocation, even though @BaseRuleContainer@ already hands each thread its own cloned container tree, so a plain instance field on the clone would do. The @ThreadLocal@ is never @remove@d, and the AST processor threads are started once (TransformDriver.java:340) and shut down only at the end (1231), spanning all ~30 profiles — so each worker's @ThreadLocalMap@ accumulates one entry per distinct *master* @RuleContainer@ it ever touched (@clone()@ shallow-copies the field, so all per-thread clones share the master's @ThreadLocal@). Since the masters are discarded at each @finish()@, those entries become stale weak-key slots expunged only heuristically, bloating every subsequent @ThreadLocal@ get/set on those threads.
* *[MINOR]* _performance_ @NamedFunction@.@clone@: clones the expression list by reflectively resolving @clone@ per element (@expr.getClass().getMethod("clone")@ + @invoke@, lines 537-542). @Class.getMethod@ scans the public-method table and returns a *copy* of the @Method@ each call, roughly a microsecond per element on the per-thread deep-clone path — @BaseRuleContainer.getFunctionLibrary()@/@RuleContainer.clone()@ clone every named function of every container, per worker thread, per profile, and @rules/include/common-progress.rules@ alone declares 429 functions and is included in 154 places. The raw @ArrayList expr@ holds a heterogeneous mix of @Expression@ and @Rule@, so the fix is an @instanceof@ dispatch (or a shared @Cloneable@ interface) rather than a direct @Expression.clone()@ call; both types already expose a public @clone()@.
* *[MINOR]* _performance_ @Expression@.@getCompiler@: @getCompiler()@ (line 761), @putCompiler()@ (749) and @getCompiledInstance()@ (722) each allocate a fresh @CacheKey(this)@ even though the key is fully determined by the immutable @infix@/@flags@ pair and could be cached in a field — @getCompiledInstance@ alone allocates two, and three when it compiles. The static @locks@ @ConcurrentHashMap@ (line 216) retains one @ReentrantLock@ plus one @CacheKey@ per distinct expression text for the whole run, dropped only by @cleanup()@ at @PatternEngine.finish()@.
* *[MINOR]* _performance_ @AstKey@.@<init>@: @AstKey.initialize@ (lines 670-672, reached from the @AstKey(Aast, criteria)@ constructor) does @LOCKS_CACHE.computeIfAbsent(hash, key -> new ReentrantLock())@ on *every* construction, allocating and retaining a @ReentrantLock@ per distinct hash value in a static unbounded @ConcurrentHashMap@ even though only @rules/schema/p2o.xml@:834 ever calls @getLock()@. The constructing sites are walk-rules firing per matching node per file — @annotations/accumulate.rules@:203/280/477/611/642 and @annotations/presort.rules@:191/368 — none of which use the lock. The map is dropped only by the three explicit @cleanAstKeyCache()@ calls. Keying on @hashCode@ rather than the key itself also makes unrelated, non-equal keys with colliding hashes contend on one lock, which matters because @p2o.xml@ holds that lock across ~500 lines. Latent companion defect: @readExternal@ (576-582) restores @hash@ but never sets @lock@, so any @AstKey@ deserialized from the conversion DB would NPE in @getLock().lock()@ — currently unreachable only because @p2o.xml@ always constructs @tmpTabKey@ fresh.
* *[MINOR]* _performance_ @UastHintsWorker@.@loadHints@: the new @synchronized(UastHintsWorker.this)@ protects nothing shared while serializing all worker threads over file I/O. Everything written inside is per-thread — @hints@ is a @ContextLocal<UastHints>@ and @getResolver()@ is itself context-local — but the worker instance is a shared singleton, so the monitor is global. The guarded region is @getUastHints(...)@ → @new UastHints(path)@, which walks the whole directory tree and re-parses every directory-level @.hints@ plus the file-level @.hints@ on *each* call (no caching on this constructor path). Since @visitAst@ resets @hints@ to @null@ per AST, this full load runs once per artifact per thread and is globally serialized. @PreprocessorHintsWorker.loadHints@ has the identical pattern around @new PreprocessorHints(artifact)@.
* *[MINOR]* _performance_ @ServiceSupport@.@getProxyConfig@: lookups are serialized on two unrelated monitors. @getProxyConfig@, @getProxyProgramConfig@ and @getSoapOperation@ are @synchronized@ on the shared singleton @ServiceSupport@ instance, while @isRestService@, @hasRestSubServices@, @resolveRestAnnotation@, @getAppObjectJast@ and @registerProxyClass@ are @synchronized@ on the inner @Library@ instance — so a reader and a writer of the same @WorkArea@ field can hold different locks. @proxyConfigs@ and @operations@ are populated once by @initializeProxyConfigurations@/@initializeRestConfigurations@ (lines 310-336) and shared by reference through @setContext@, so those lookups are genuinely read-only and the locking buys nothing. Concrete cost: @legacy_services.rules@ (run for every artifact) calls @srv.getSoapOperations(...)@ for every @procedure@/@function@/@method_def@/@constructor@, each acquiring the single global @ServiceSupport@ monitor four times — including on projects with no @soap-cfg@ at all, where the method returns immediately on @wa.soapConfigs == null@ *inside* the lock.
* *[MINOR]* _functional_ @SoapWsdl@.@addService@: the generated WSDL is not reproducible across runs. @ServiceSupport.registerSoapOperation@ (line 1682, reached from @legacy_services.rules@:405 and @soap_services.xml@:153 on worker threads) calls @child.soap.getWsdl().addService(child.service, null)@ once per *persistent* operation. @addService@ derives @ServiceConfig.ns = "S" + (services.size() + 2)@ and the @ServiceConfig@ constructor does @elTypes.appendChild(elSchema)@, so both the @xmlns:Sn@→service bindings emitted by @generate()@ and the child order of @<wsdl:types>@ follow the order in which worker threads reach @registerSoapOperation@. Every @Sn:@ qualified reference in the document shifts accordingly, so two runs emit textually different WSDL for a @.wsm@ with 2+ persistent operations defined in different source files. The @HashMap@→@TreeMap@ change fixed iteration order but not this insertion-order-dependent numbering. Note @boolean first@, @root.setAttribute("name", …)@, @defaultService@ and the @FaultDetailMessage@ owner are *not* affected — @createSoapConfig@ (single-threaded, from @initializeSoapConfigurations@ before threads start) always performs the first @addService@, so @first@ is consumed deterministically at init.
* *[MINOR]* _functional_ @SoapWsdl@.@generate@: @service.messages.sort(Comparator.comparing(Node::getNodeName))@ is a no-op. Every element in @ServiceConfig.messages@ is created by @dom.createElementNS(NS_WSDL, "wsdl:message")@ (line 310 for @FaultDetailMessage@, line 640 in @createMessage@), so @getNodeName()@ returns the identical @"wsdl:message"@ for all of them and the stable sort reorders nothing; the intended key is the @name@ attribute set at line 641. The revision note claims messages are sorted per service, but only the @operations@ sort is effective — message order is deterministic today only as a by-product of that sort.
* *[MINOR]* _functional_ @AstProcessorThread@.@run@: the catch-all reports only @e.getMessage()@ — frequently @null@, e.g. for the NPE paths in @AstProcessorJob.run@ — with no stack trace and no artifact identification, and writes to @System.err@ rather than the @ConversionStatus@/@CentralLogger@ used everywhere else in the pipeline (@ServiceSupport@, @UastHintsWorker@, @PreprocessorHintsWorker@ all hold a @ConversionStatus LOG@). The operator sees the single line @"Job execution failed: null"@ while the driver hangs forever on @latch.await()@, with nothing naming the offending file.
* *[MINOR]* _functional_ @DDLGeneratorWorker@.@generateWordTableDDL@: the word-table registry changed from @wordTables.computeIfAbsent(dbName, db -> new TreeMap<>())@ to @wa.wordTables.computeIfAbsent(dbName, db -> new HashMap<>())@ (line 1540) while @generateWordTablesDDLs@ still passes @wa.wordTables.getOrDefault(dbName, Collections.emptyMap()).values()@ straight to @dialect.generateWordTablesDDLImpl@ (line 917), which emits them in iteration order. Trigger: any schema with word indexes on more than one table in a database — the @CREATE TABLE@ statements in @ddl/schema_word_tables_<schema>_<dialect>.sql@ are no longer in word-table-name order, so the file is not byte-reproducible across runs. The sibling @wordTablesKeys@ map on the very next line (1578) deliberately kept @TreeMap@, so the FK/index block below stays sorted while the block above does not.
* *[MINOR]* _functional_ @ReportDriver@.@processOptions@: the @args.length >= 2@ → @>= 3@ relaxation makes a two-token deprecated invocation such as @ReportDriver -D2 reports/profile@ leave @opt.newSyntax@ at its @true@ default (line 714), so @main@ dispatches to @newSyntax(opt)@, which ignores the trailing @<rptdef>@ argument and silently runs the full two-pass report over the whole project (and now also @SHUTDOWN COMPACT@s the database) instead of failing with "Missing file list parameter" (exit -6, line 577). A mistyped command becomes a multi-hour full run. Separately, @Integer.parseInt(opts.substring(11))@ for @-numthreads=@ throws an unhandled @NumberFormatException@ out of @main@ for @-numthreads=abc@ or a bare @-numthreads=@, instead of calling @syntax(...)@ like every other malformed option; the parsed value is also never range-checked.
* *[MINOR]* _functional_ @stable_naming@ rule-set 4: for the @<rule>type == prog.define_frame and frName.isEmpty()@ branch the key is built as @sprintf("%s|%s|%s", artifact.getRelativePath(), biName, frName)@ with @frName@ empty, while the sibling @on="false"@ branch correctly uses @biName|biName@. Section D extracts the base name with @compositeKey.substring(compositeKey.lastIndexOf('|') + 1)@ — yielding the empty string for the first branch, so @DEFINE [NEW] SHARED FRAME cust-frame fld1 fld2.@ produces @Frame_1@, @Frame_2@, … instead of @FrameCustFrame_N@. This is not a regression (trunk's @processForm@ also emitted @Frame_N@), but the new code deliberately computes the correct @biName@ from the @SYMBOL@ child and then discards it, so the intent is clearly to carry the base name and the fix is one token away.
* *[MINOR]* _functional_ @stable_naming@: @ConversionDriver.middle()@ registers both stable-naming passes with @matches == null, db == false@ (lines 483-503), whereas every phase whose name computation it takes over runs with a match list: @p2o_pre@/@p2o@ use @datanames, db == true@ and @annotations/annotations@ (containing @naming.rules@ and @frame_generator_pre.xml@) uses @codenames, db == false@. @TransformDriver.processTrees@ calls @NameConverter.load(matches, db)@, which replaces the static @defaults@ dictionary for the whole pass, so @load(null, false)@ skips the @"date" -> "dateValue"@/@"id" -> "identifier"@ database defaults (@DEFINE TEMP-TABLE date@ now yields @Date@ instead of @DateValue@) and ignores both configured match lists. Worse, the class-name path is now *split*: conflicting artifacts get their name from stable_naming (no match list) while every other artifact still gets it from @naming.rules@'s @computeClassName@ (with @codenames@), so one project can produce two different conversions of the same 4GL identifier.
* *[MINOR]* _functional_ @stable_naming@: the second registration (ConversionDriver.java:497-503) passes @FileExtensions.AST_POSTFIX@ as the cleanup extension for @sschemas@, a collection built by @convertSourceArtifactsToDictArtifacts(FileExtensions.SCHEMA_POSTFIX)@ whose members are @<source>.schema@ paths (the adjacent @p2o_pre@/@p2o@ calls over the same collection correctly pass @SCHEMA_POSTFIX@). @cleanSources()@ therefore calls @ArtifactManager.getOriginalArtifact(artifact, ".ast")@, whose @!artifact.endsWith(extensionToRemove)@ branch logs a warning and returns the *unmodified* @.schema@ artifact; @sources.removeAll(originalSources)@ then removes nothing, @getArtifactData@/@cleanupArtifacts@ operate on the wrong artifact, and @allFailedSources@ reports the @.schema@ path. A source whose stable-naming pass over its temp-table schema fails is neither removed from the project sources nor rolled back, and conversion continues with a half-processed file.
* *[MINOR]* _style_ @collect_procedures@: history entry "005" uses author initials "A0G" (digit zero) instead of "AOG" used by every other entry from this author.
* *[MINOR]* _style_ @java_dmo@: header gains a new closing "*/" line right before "-->" with no matching opening "/*", leaving a stray unmatched comment marker; the same pattern was introduced in @java_pojo@.
* *[MINOR]* _style_ @java_dmo@: two new merged @<rule>@ boolean conditions are emitted as single lines of 145 and 156 characters, abandoning the file's convention of wrapping long AND-chains across multiple aligned lines.
* *[MINOR]* _style_ @adm_windows@: the @toolbarFrameName@ variable declaration drops its @type="java.lang.String"@ attribute while adding @multiThreadAccess="true"@, and loses the space before the self-closing @/>@ that sibling declarations keep.
* *[MINOR]* _style_ @core_conversion@: new @multiThreadAccess="true"@ attributes omit the space before the self-closing @/>@, inconsistent with sibling lines in the same block.
* *[MINOR]* _style_ @gap_analysis_marking@: the @frameUiOpts@ variable declaration omits the space before the self-closing @/>@ while sibling variables in the same block keep it.
* *[MINOR]* _style_ @proxy_programs@: two @<action>@ elements are placed on a single line (occurring twice in the new post-rules block), breaking the one-action-per-line convention used elsewhere in the file.
* *[MINOR]* _style_ @naming@: trailing whitespace left on blank lines inside the newly added @computeClassName@ function and the refactored classname-lookup rule block.
* *[MINOR]* _style_ @annotations@ (rules/annotations): trailing whitespace left on blank separator lines within the newly reformatted @multiThreadAccess@ variable block.
* *[MINOR]* _style_ @stable_naming@: inconsistent column alignment where @pendingTTCustomIface@/@pendingTTIfaceCustomBaseName@ are padded to their own wider column instead of continuing the surrounding group's alignment; also a stray extra space after @<action on="false">@ before @suffix = 0@ (appears twice).
* *[MINOR]* _style_ @word_reindex@: @multiThreadAccess="true"@ attributes are padded to align with an unrelated, much longer type name from another block, pushing several lines to ~167 characters, well past the line-length guideline.
* *[MINOR]* _style_ @Scope@.@equals@: brace-less single-line @if@ statement used (and likewise in nested @ScopeState@.@equals@), violating the "always use block delimiters" rule.
* *[MINOR]* _style_ @SymbolResolver@ (expr): pre-existing @java.util.concurrent.locks.*@ import is now dead code since this diff removed its only usage (the @instCacheLock@ @ReentrantLock@) without removing the import.
* *[MINOR]* _style_ @Variable@: two consecutive blank lines left between the import block and the class javadoc (should be exactly one).
* *[MINOR]* _style_ @Variable@: newly added import @java.util.concurrent.locks.*@ is unused (no @Lock@/@ReentrantLock@ reference exists in the file).
* *[MINOR]* _style_ @Variable@.@Variable@ (copy constructor): javadoc omits the blank comment line between description and @@param@, inconsistent with the file's convention.
* *[MINOR]* _style_ @Variable@.@deepCloneObject@: @@throws RuntimeException@ tag places the full description on the same line as the tag instead of continuing on indented following lines per file convention.
* *[MINOR]* _style_ @Variable@.@isPrimitiveOrWrapper@: wrapped @return@ continuation lines indented 14 spaces, not a multiple of 3 and misaligned from the first line.
* *[MINOR]* _style_ @Resettable@: new file is missing a trailing newline, unlike every sibling file in @com.goldencode.expr@.
* *[MINOR]* _style_ @Compiler@.@instantiate@: javadoc still documents @@throws InstantiationException@/@IllegalAccessException@ and references storing into @#compiled@, both stale since the throwing instantiation logic moved to @process()@.
* *[MINOR]* _style_ @CompiledExpression@: new public no-arg constructor has only a plain comment, not a javadoc block.
* *[MINOR]* _style_ @ArtifactManager@.@filterConflictingArtifacts@: javadoc is missing @@param@ for its parameter.
* *[MINOR]* _style_ @ArtifactManager@: new import of @ContextLocal@ uses an explicit class import rather than the package wildcard.
* *[MINOR]* _style_ @SymbolResolver@ (expr): new constant fields @NULL_FUNCTION_MARKER@/@FUNCTION_CACHE_LOCK@ use SCREAMING_SNAKE_CASE while sibling static fields in this file use camelCase; same inconsistency for @LOCK_CACHE@ in @XmlFilePlugin@.
* *[MINOR]* _style_ @NameConverter@: new @ansiKeywords@ array literal elements are indented 14 spaces against a 6-space declaration (an 8-space step, not the required 3-space level).
* *[MINOR]* _style_ @NameConverter@.@setCurrentDialects@: lambda passed to @reservedSQL.computeIfAbsent(...)@ opens its brace on the same line as the arrow instead of Allman style.
* *[MINOR]* _style_ @NameConverterWorker@: an extra blank line was added inside the import block, splitting the previously-contiguous @com.goldencode.*@ import group.
* *[MINOR]* _style_ @UnreachableCodeWorker@.@getKnownValues@: a blank line was inserted immediately after the method body's opening brace, violating the "no blank line after @{@" rule.
* *[MINOR]* _style_ @TransformDriver@: added @numThreads, @ argument has trailing whitespace after the comma.
* *[MINOR]* _style_ @TransformDriver@.@JobDefinition@: new @numThreads@ field javadoc omits the trailing period used by every sibling field javadoc in the same class.
* *[MINOR]* _style_ @ConversionData@.@getConvertedClassName@: wrapped string-concat continuation was re-indented away from alignment under the opening quote, no longer following the file's established continuation convention (same issue in @saveClass@).
* *[MINOR]* _style_ @ConversionData@: stray trailing whitespace introduced on several blank/javadoc lines (e.g. in @getTempIdx@, @withServicesFile@, @getClassesInPackage@, @listLegacyClasses@, @findClassQName@, @getConvertedSimpleClassName@, @mustConvertArtifact@).
* *[MINOR]* _style_ @AstProcessorThread@: blank line immediately after the opening class brace, violating the no-blank-line-after-@{@ rule.
* *[MINOR]* _style_ @AstProcessorThread@: license boilerplate paragraph has a malformed comment line ("*" instead of "**") right after "without even the implied warranty of".
* *[MINOR]* _style_ @AstSymbolResolver@: anonymous @ContextLocal@ field initializer's overridden @initialValue()@ method lacks the required @@Override@ annotation, and has a stray extra semicolon after its closing brace.
* *[MINOR]* _style_ @AstSymbolResolver@.@execute@: a blank line was inserted between the "// obtain the current NamedFunction..." comment and the code it describes, violating the no-blank-line-between-comment-and-subject rule.
* *[MINOR]* _style_ @CommonAstSupport@.@generateMenuClassName@: wrapped parameter continuation lines are indented to a column that does not align under the first parameter.
* *[MINOR]* _style_ @CommonAstSupport@.@getBaseArtifactsIterator@: uses a raw, unparameterized @Iterator@ return type and the javadoc @@return@ tag is just "See above." rather than a real description.
* *[MINOR]* _style_ @PatternEngine@: static member access is inconsistent — @RuleContainer.getDebugLevel()@ is called qualified in @finish()@ but unqualified elsewhere (@processAst@, @gatherTargetPaths@) relying on the new @import static@.
* *[MINOR]* _style_ @PatternEngine@.@getCombinedResults@: two consecutive blank lines appear between the single-threaded-mode early return and the @addThreadJob@ call.
* *[MINOR]* _style_ @PatternEngine@: new javadoc uses markdown-style bold ("**global rules**", "**EngineState**") instead of the file's established @<b>@ HTML-tag convention.
* *[MINOR]* _style_ @RuleContainer@.@clone@: @for(RuleListElement elem : rules)@ is missing the required space between @for@ and @(@.
* *[MINOR]* _style_ @RuleListElement@: file still has no trailing newline at end-of-file after being modified in this diff.
* *[MINOR]* _style_ @ReportWorker@: stray extra semicolon after the @initialValue()@ method's closing brace, not present in the equivalent code in @DDLGeneratorWorker@.
* *[MINOR]* _style_ @DDLGeneratorWorker@.@generateWordTable@: line exceeds the 110-character limit (114 chars), and a double space appears between @TableHints@ and @tableHints@.
* *[MINOR]* _style_ @DDLGeneratorWorker@.@createDbObjectName@: line exceeds the 110-character limit (115 chars).
* *[MINOR]* _style_ @ReportWorker@: repeated double-space typo after assignment operator in several newly added lines (e.g. @WorkArea wa =  context.get();@) across @createDatabaseTables@, @prepareStatements@, @initializeDatabase@, and the match-category insert method.
* *[MINOR]* _style_ @PreprocessorHintsWorker@.@loadHints@: missing space between @synchronized@ and its opening parenthesis.
* *[MINOR]* _style_ @JavaPatternWorker@: a blank line was added immediately after the class's opening brace, before the new @tempDMOs@ field javadoc.
* *[MINOR]* _style_ @UastHintsWorker@: @synchronized(UastHintsWorker.this)@ is missing the space before @(@, inconsistent with @synchronized (SchemaWorker.this)@ used elsewhere in this same diff; also adds an explicit @ContextLocal@ import instead of a wildcard.
* *[MINOR]* _style_ @SymbolResolver@ (uast): new @ReentrantLock lock =  CLASS_LOCKS.computeIfAbsent(...)@ has two spaces after @=@; also adds explicit import @java.util.concurrent.locks.ReentrantLock@ instead of the wildcard form.
* *[MINOR]* _style_ @CallGraphWorker@: a blank line was inserted between two consecutive multi-line @GraphDB.createIndex(db, ...)@ calls, breaking the file's established no-blank-line grouping of these calls.
* *[MINOR]* _style_ @AstKey@.@getLock@: javadoc uses Markdown backticks instead of the file's @<code>@/@{@code}@ convention, and a single space after @@return@ instead of the file's two-space alignment.
* *[MINOR]* _style_ @DataModelWorker@: added @java.util.concurrent.ConcurrentHashMap@ and @CopyOnWriteArrayList@ as explicit imports instead of the wildcard @java.util.concurrent.*@.
* *[MINOR]* _style_ @SchemaWorker@: added @java.util.concurrent.ConcurrentHashMap@ and @com.goldencode.p2j.security.ContextLocal@ as explicit imports instead of wildcards.
* *[MINOR]* _style_ @AbstractResource@.@isAutoRelease@: javadoc text ("specific to the PatternEngine scope to prevent deadlocks or stale locks") reads oddly in this generic base-resource class and appears copy-pasted verbatim into @SecurityManager@'s identical method.
* *[MINOR]* _style_ @P2OLookup@: header history entry 051 and the new @resetPermP2OLookups@ javadoc contain typos ("reseting", "requeired", "incompleted") duplicated from @P2OAccessWorker@'s entry 032, which has the same typos.
