Copilot commented on code in PR #6315:
URL: https://github.com/apache/fineract/pull/6315#discussion_r3841448177


##########
tools/archmetrics_to_vega.py:
##########
@@ -0,0 +1,234 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Turn the java-architecture-metrics JSON report into Vega-Lite specs.
+
+The plugin reports, per component: classCount, abstractTypeCount,
+afferentCoupling (Ca), efferentCoupling (Ce), abstractness (A),
+instability (I), distance (D), plus the directed incoming/outgoingComponents
+sets. Everything below is derived from that one file.
+
+    python3 tools/archmetrics_to_vega.py <report.json> [--outdir DIR] 
[--exclude external,platform]
+"""
+import argparse, json, pathlib, sys
+
+MAIN_SEQUENCE = "main-sequence.vl.json"
+DISTANCE_RANK = "distance-ranking.vl.json"
+COUPLING_MTX = "cross-feature-matrix.vl.json"
+
+
+def load(path, exclude):
+    report = json.loads(pathlib.Path(path).read_text())
+    rows = []
+    for c in report.get("components", []):
+        if c.get("component") in exclude:
+            continue
+        out = [d for d in c.get("outgoingComponents", []) if d not in exclude]
+        inc = [d for d in c.get("incomingComponents", []) if d not in exclude]
+        rows.append({
+            "component": c.get("component"),
+            "classCount": c.get("classCount", 0),
+            "abstractTypeCount": c.get("abstractTypeCount", 0),
+            "Ca": c.get("afferentCoupling", 0),
+            "Ce": c.get("efferentCoupling", 0),
+            "A": round(c.get("abstractness", 0.0), 4),
+            "I": round(c.get("instability", 0.0), 4),
+            "D": round(c.get("distance", 0.0), 4),
+            "CFV": len(out),
+            "outgoing": sorted(out),
+            "incoming": sorted(inc),
+        })
+    return sorted(rows, key=lambda r: -r["CFV"])
+
+
+def main_sequence(rows):
+    return {
+        "$schema": "https://vega.github.io/schema/vega-lite/v5.json";,
+        "title": "Abstractness vs Instability (distance from the main 
sequence)",
+        "width": 520, "height": 420,
+        "data": {"values": [{k: r[k] for k in ("component", "A", "I", "D", 
"classCount")} for r in rows]},
+        "layer": [
+            {   # the main sequence itself: A + I = 1
+                "data": {"values": [{"I": 0, "A": 1}, {"I": 1, "A": 0}]},
+                "mark": {"type": "line", "strokeDash": [6, 4], "color": 
"#888"},
+                "encoding": {
+                    "x": {"field": "I", "type": "quantitative"},
+                    "y": {"field": "A", "type": "quantitative"},
+                },
+            },
+            {
+                "mark": {"type": "circle", "opacity": 0.8},
+                "encoding": {
+                    "x": {"field": "I", "type": "quantitative", "title": 
"Instability (I = Ce / (Ca + Ce))",
+                          "scale": {"domain": [0, 1]}},
+                    "y": {"field": "A", "type": "quantitative", "title": 
"Abstractness (A)",
+                          "scale": {"domain": [0, 1]}},
+                    "size": {"field": "classCount", "type": "quantitative", 
"title": "classes"},
+                    "color": {"field": "D", "type": "quantitative", "title": 
"Distance",
+                              "scale": {"scheme": "orangered"}},
+                    "tooltip": [{"field": c, "type": t} for c, t in
+                                (("component", "nominal"), ("A", 
"quantitative"), ("I", "quantitative"),
+                                 ("D", "quantitative"), ("classCount", 
"quantitative"))],
+                },
+            },
+            {
+                "mark": {"type": "text", "dy": -10, "fontSize": 9},
+                "encoding": {
+                    "x": {"field": "I", "type": "quantitative"},
+                    "y": {"field": "A", "type": "quantitative"},
+                    "text": {"field": "component", "type": "nominal"},
+                },
+            },
+        ],
+    }
+
+
+def distance_ranking(rows):
+    return {
+        "$schema": "https://vega.github.io/schema/vega-lite/v5.json";,
+        "title": "Distance from the main sequence, by component",
+        "width": 520, "height": {"step": 14},
+        "data": {"values": [{k: r[k] for k in ("component", "D", "Ce", "Ca")} 
for r in rows]},
+        "mark": "bar",
+        "encoding": {
+            "y": {"field": "component", "type": "nominal", "sort": "-x", 
"title": None},
+            "x": {"field": "D", "type": "quantitative", "title": "Distance (D 
= |A + I - 1|)"},
+            "color": {"field": "D", "type": "quantitative", "legend": None,
+                      "scale": {"scheme": "orangered"}},
+            "tooltip": [{"field": c, "type": t} for c, t in
+                        (("component", "nominal"), ("D", "quantitative"),
+                         ("Ce", "quantitative"), ("Ca", "quantitative"))],
+        },
+    }
+
+
+def coupling_matrix(rows):
+    cells = [{"from": r["component"], "to": t, "v": 1} for r in rows for t in 
r["outgoing"]]
+    return {
+        "$schema": "https://vega.github.io/schema/vega-lite/v5.json";,
+        "title": "Cross-feature dependencies (row depends on column)",
+        "width": 520, "height": 520,
+        "data": {"values": cells},
+        "mark": "rect",
+        "encoding": {
+            "y": {"field": "from", "type": "nominal", "title": "depends on 
->"},
+            "x": {"field": "to", "type": "nominal", "title": None,
+                  "axis": {"labelAngle": -45, "orient": "top"}},
+            "color": {"value": "#d1495b"},
+            "tooltip": [{"field": "from", "type": "nominal"}, {"field": "to", 
"type": "nominal"}],
+        },
+    }
+
+
+_NAME_LIMIT = 12
+
+
+def _names(items):
+    """Render a dependency list, truncated so a section stays readable."""
+    if not items:
+        return "_nothing_"
+    shown = ", ".join(f"`{d}`" for d in items[:_NAME_LIMIT])
+    if len(items) > _NAME_LIMIT:
+        shown += f", … and {len(items) - _NAME_LIMIT} more"
+    return shown
+
+
+def skeleton(rows, min_cfv):
+    """Emit one AsciiDoc section per component, for future authors to fill in.
+
+    Every component present in the report gets a heading, its measured numbers 
and its
+    dependency list. The analysis prose is left as TODO markers so that the 
structure
+    exists before anyone writes a word.
+    """
+    out = [
+        "// Generated by tools/archmetrics_to_vega.py --skeleton. Do not edit 
by hand:",
+        "// regenerate with ./gradlew architectureMetricsReport. Fill in the 
TODO",
+        "// sections in the chapter that includes this file, not here.",
+        "",
+        "// Components with at least %d outgoing cross-component dependency, 
ordered by" % min_cfv,
+        "// outgoing dependency count.",
+        "",
+    ]
+    for r in rows:
+        out += [
+            f"=== {r['component']}",
+            "",
+            '[cols="^1,^1,^1,^1,^1,^1,^1", options="header"]',
+            "|===",
+            "| Classes | Abstract | Ce | Ca | I | A | D",
+            "",
+            f"| {r['classCount']} | {r['abstractTypeCount']} | {r['Ce']} | 
{r['Ca']} "
+            f"| {r['I']:.2f} | {r['A']:.2f} | {r['D']:.2f}",
+            "|===",
+            "",
+            f"*Depends on ({len(r['outgoing'])}):* " + _names(r["outgoing"]),
+            "",
+            f"*Depended on by ({len(r['incoming'])}):* " + 
_names(r["incoming"]),
+            "",
+            "// TODO: what this component is responsible for",
+            "// TODO: which of the dependencies above are genuine and which 
are violations",
+            "// TODO: proposed fix, estimated effort, impact on the source 
code",
+            "",
+        ]
+    return "\n".join(out)
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("report", type=pathlib.Path)
+    ap.add_argument("--outdir", type=pathlib.Path,
+                    
default=pathlib.Path("fineract/fineract-doc/src/docs/en/diagrams"))

Review Comment:
   `--outdir` default points to `fineract/fineract-doc/...`, but this repo uses 
`fineract-doc/...` at the root. Running the script without `--outdir` will 
write to an unexpected path (and may create a stray `fineract/` directory). 
Consider defaulting relative to the repo root derived from `__file__` (e.g., 
`Path(__file__).resolve().parents[1] / 'fineract-doc/src/docs/en/diagrams'`) or 
at least changing the literal to `fineract-doc/src/docs/en/diagrams`.



##########
fineract-doc/src/docs/en/chapters/architecture/cross-feature-boundary-violations.adoc:
##########
@@ -0,0 +1,1426 @@
+= Cross-Feature Boundary Violations
+
+Fineract is built from more than thirty Gradle modules, but the feature code 
inside
+them still behaves largely as a single unit. A class in the accounting feature 
holds
+a direct reference to a charge entity; a client service imports types from 
loan,
+savings, collateral and holidays. The module structure describes an intent 
that the
+code does not yet honour.
+
+This chapter measures how far the code is from that intent, explains which 
parts of
+the gap are worth closing, and sets out the criteria used to decide which 
packages to
+decouple first.
+
+== Introduction
+
+=== What a Feature Boundary Means in Fineract
+
+A *feature* is a business capability that owns its own data and rules — loan, 
savings,
+charge, accounting, client, group. In the target architecture each feature is 
a Gradle
+module (`fineract-loan`, `fineract-savings`, `fineract-charge`) that publishes 
a small,
+deliberate set of types and keeps everything else to itself.
+
+A feature's *boundary* is that published set. Code inside the feature may use 
anything
+it owns. Code outside it may use only what the feature publishes, and only 
through
+contracts rather than concrete implementations.
+
+Two shared modules sit beneath every feature and are deliberately outside this 
rule:
+
+* `fineract-core` — shared domain primitives, contracts and abstractions that 
any
+  feature may depend on.
+* `fineract-command` — the command/handler infrastructure through which state 
changes flow.
+
+A dependency on either is not a violation. It is the mechanism by which 
features are
+meant to reach one another.
+
+One complication has to be stated up front, because it shapes everything that 
follows.
+Fineract's features are Gradle modules, but Java code is organised by 
*package*, and the
+two do not line up: 159 of 1023 main-source packages are split across two to 
four
+modules. `org.apache.fineract.portfolio.loanaccount.service`, for example, has 
classes
+in `fineract-loan`, `fineract-provider`, `fineract-progressive-loan` and 
`fineract-core`
+simultaneously. A boundary is therefore a property of *modules*, not of 
package names,
+and any measurement keyed on package names alone will misattribute code.
+
+=== What Counts as a Boundary Violation
+
+A *cross-feature violation* (CFV) is a compile-time reference from a class 
owned by one
+feature to an implementation type owned by another feature.
+
+Counted as a violation:
+
+* Importing a concrete class, entity or service implementation from another 
feature.
+* A JPA association that maps directly onto another feature's entity — for 
example
+  `ProductToGLAccountMapping` in the accounting module holding a `Charge` field
+  (see `ProductToGLAccountMapping.java`), which binds the accounting object 
model to
+  the charge object model at compile time.
+* Injecting another feature's `…ReadPlatformService` implementation rather 
than a
+  contract published for the purpose.
+* Reading another feature's tables directly in hand-written SQL.
+
+Not counted as a violation:
+
+* Any dependency on `fineract-core` or `fineract-command`.
+* A feature *implementing* a contract that another feature owns. This is the 
inversion
+  that fixes a violation rather than an instance of one, and it is the shape 
every fix in
+  <<_how_a_violation_is_fixed>> takes.
+* Dependencies on platform infrastructure — security, event framework, jobs, 
Liquibase.
+
+The distinction between the second and third bullets of the two lists matters 
more than
+it may appear. Accounting depending on charge is a violation. Charge 
implementing an
+interface that accounting owns is not — the compile-time arrow now points from 
charge
+into accounting-owned contracts, and accounting no longer needs to know charge 
exists.
+That single reversal is the core move used throughout this chapter.
+
+=== What Kind of Entanglement Are We Trying to Solve
+
+"Coupling" covers several distinct problems in Fineract, and they do not have 
the same
+cost, the same fix, or the same urgency. Separating them prevents both 
overstating the
+problem and fixing the wrong layer.
+
+==== Compile-Time Entanglement
+
+One feature's classes refer to another feature's classes, so the two cannot be 
compiled,
+tested or released apart. This is the entanglement this chapter measures and 
the one the
+CFV count describes.
+
+It is also the only one of the four that can be fixed purely by moving and 
re-shaping
+Java code, which is why it is addressed first.
+
+==== Database Entanglement
+
+All features share a single tenant schema, and that schema contains foreign 
keys that
+cross feature lines. The accounting-to-charge relationship again serves as the 
example:
+
+[source,xml]
+----
+<addForeignKeyConstraint baseColumnNames="charge_id" 
baseTableName="acc_product_mapping"
+                         constraintName="FK_acc_product_mapping_m_charge"
+                         onDelete="RESTRICT" onUpdate="RESTRICT"
+                         referencedColumnNames="id" 
referencedTableName="m_charge"/>
+----
+
+`acc_product_mapping` is an accounting table. `m_charge` is a charge table. 
The database
+enforces a hard reference between them independently of anything the Java code 
does.
+
+This is the single most important qualification in the chapter: *removing a 
compile-time
+dependency does not remove the corresponding data dependency.* Replacing the 
`Charge`
+association with a `Long chargeId` decouples the object model while the 
foreign key
+constraint remains exactly as it was. That is a real and worthwhile gain — the 
modules
+compile apart — but it is not database separation, and the chapter does not 
claim it as
+such.
+
+==== Infrastructure Entanglement
+
+Every feature uses the same command bus, event framework, security context, 
tenant
+resolution and Liquibase master changelog. Features are entangled through this 
shared
+substrate in the sense that they all depend on it.
+
+This is by design and is not a target. Shared infrastructure is what makes 
features
+interoperable at all; the aim is for it to be the *only* thing they share.
+
+==== Runtime and Transactional Entanglement
+
+Cross-feature calls run in-process and typically inside one database 
transaction, so a
+failure in one feature can roll back work in another. Decoupling at compile 
time does
+not change this, and this chapter does not attempt to.
+
+It is recorded here because it is the boundary of what the work achieves: the 
outcome is
+modules that build and test independently, not services that fail 
independently.
+
+=== Why Cross-Feature Dependencies Matter
+
+The measured position — 115 cross-feature dependency pairs across 13,887 
class-level
+edges, with only 2 of 20 features clean — has concrete consequences:
+
+* *Change amplification.* A change to a charge entity can force recompilation 
and
+  retesting of accounting, loan, savings and shareaccounts, because all four 
reference it
+  directly.
+* *No independent testing.* A feature cannot be exercised without standing up 
most of the
+  platform, which pushes work into slow integration tests.
+* *Dependency cycles.* Mutual dependencies between features mean there is no 
ordering in
+  which they can be built or reasoned about separately.
+* *Onboarding cost.* A contributor cannot form a working mental model of one 
feature
+  without also learning several others.
+* *Modularisation is blocked.* Extracting any feature into its own module is 
impossible
+  while its compile-time reach is unbounded, so the module structure cannot 
advance.
+
+=== Scope of This Analysis
+
+Twenty feature packages were analysed, covering the portfolio and accounting 
features
+that make up the functional core of the platform. The unit of measurement is 
the
+feature-to-feature dependency pair; supporting evidence is drawn from 
class-level
+metrics because package-level metrics cannot be attributed reliably (see
+<<_what_a_feature_boundary_means_in_fineract>>).
+
+==== Which Database
+
+Fineract runs on MariaDB and PostgreSQL, and the analysis is 
*engine-independent* — no
+measurement in this chapter varies by database vendor.
+
+What matters is the schema layout, which is identical on both engines:
+
+* A single *tenant schema* holds the tables of every feature. There is no 
per-feature
+  schema, and no per-feature database.
+* Schema evolution is driven by one Liquibase master changelog, with the bulk 
of the
+  feature tables created in `0001_initial_schema.xml`. Some newer modules
+  (`fineract-savings`, `fineract-progressive-loan`, `fineract-rates`,
+  `fineract-working-capital-loan`) carry their own changelog directories, so 
migration
+  ownership is partially distributed already.
+* Cross-feature foreign keys exist throughout that shared schema.
+
+In scope: identifying the foreign keys and shared tables that constrain a 
proposed
+extraction, and reporting them as part of each case study's impact assessment.
+
+Out of scope: splitting the schema, removing cross-feature foreign keys, or 
introducing
+per-feature datasources. Those are separate pieces of work with their own 
migration and
+operational risk, and none of the changes proposed here depend on them.
+
+==== Infrastructure in Scope
+
+The build, test and enforcement infrastructure is in scope, because decoupling 
that is
+not enforced regresses:
+
+* Gradle module structure and inter-module dependency declarations.
+* The contract-hosting role of `fineract-core`.
+* Automated boundary verification in the test suite.
+
+Application runtime infrastructure — deployment topology, messaging, caching, 
connection
+pooling — is not in scope and is unaffected by this work.
+
+==== What This Chapter Does Not Cover
+
+* *Microservice extraction.* The target is a modular monolith. Nothing here 
proposes
+  separate deployables.
+* *Runtime or transactional isolation*, as set out above.
+* *A complete directed dependency graph from MetricsTree alone.* MetricsTree 
measures
+  coupling magnitude, never direction: `Ce = 11` says a package reaches out to 
eleven
+  outside classes, never which ones. Directed feature-to-feature edges are 
derived from
+  the import graph; MetricsTree supplies magnitude and feature-envy evidence.
+
+=== How to Read This Chapter
+
+<<_measuring_boundary_violations>> covers tooling, metric definitions and the 
measured
+baseline. Readers who want to know *what to do* rather than *how it was 
measured* can
+begin at <<_deciding_whether_a_package_is_worth_decoupling>>, which sets out 
the
+selection criteria, and then read the three case studies.
+
+Each case study follows the same six-part structure — the package, its metric 
profile,
+the violation, the proposed fix, the estimated effort, and the impact on the 
source code
+— so that the same slot can be compared across all three severities in
+<<_comparative_analysis>>.
+
+== Complexity
+
+=== What Complexity We Are Solving
+
+The complexity being removed is *accidental*, not *essential*.
+
+Essential complexity is inherent in the domain and will survive any 
refactoring. A loan
+genuinely has charges. Posting a repayment genuinely produces accounting 
entries. A
+client genuinely has loans, savings accounts and collateral. No amount of 
module
+restructuring makes these relationships go away, and none of the work proposed 
here
+attempts to.
+
+Accidental complexity is how those relationships are currently *expressed in 
code*:
+
+* Accounting knows the `Charge` *class* when all it requires is a charge 
*identifier*
+  and a small amount of charge metadata.
+* The client feature imports concrete types from loan, savings, charge, 
collateral,
+  note, accounting, staff, working days, holidays and data queries in order to 
assemble
+  what is ultimately a read-only view.
+* A feature that needs one field from a neighbour takes a compile-time 
dependency on
+  that neighbour's entire object graph.
+
+In each case the *dependency is real* but the *coupling is far stronger than 
the
+dependency requires*. That gap — between what a feature genuinely needs and 
what it
+currently imports — is the complexity this work removes.
+
+=== Where the Complexity Shows Up
+
+* *Build and test cycles.* Touching a widely-referenced entity triggers 
rebuilds far
+  beyond the feature that owns it.
+* *Cyclic reasoning.* With 115 cross-feature pairs among 20 features, 
dependency cycles
+  are the norm rather than the exception, and no feature can be understood in 
isolation.
+* *Misleading module structure.* Gradle modules imply boundaries that the code 
does not
+  respect, so the build files actively mislead newcomers.
+* *Hidden coupling.* Because 159 packages are split across modules, some 
cross-module
+  dependencies appear as ordinary intra-package references and are invisible to
+  package-level metrics — the coupling is worse than a naive `Ce` reading 
suggests.
+
+=== What Decoupling Will Not Simplify
+
+Stated plainly, so the expected benefit is not oversold:
+
+* *Domain complexity is unchanged.* The same business rules, in the same 
number, after
+  the work as before.
+* *Class count rises.* Interfaces and DTOs in `fineract-core` are added, not 
removed.
+  Decoupling trades a smaller number of tightly-bound types for a larger 
number of
+  loosely-bound ones.
+* *Indirection increases.* Navigating from a caller to the implementation now 
passes
+  through a contract, which is a real cost to readability at the call site.
+* *The database is untouched.* Cross-feature foreign keys remain.
+* *Runtime behaviour is unchanged.* The same calls happen in the same order in 
the same
+  transaction.
+
+The trade is deliberate: more types, more indirection, and in exchange a module
+structure that can be enforced automatically and evolved independently.
+
+== Measuring Boundary Violations
+
+=== Tooling and Method
+
+The platform was measured twice, with independent tooling, because no single 
tool
+produces everything the analysis needs.
+
+The *first pass* used the MetricsTree plugin inside IntelliJ IDEA. It supplies 
coupling
+magnitude and the feature-envy metrics that identify individual misplaced 
classes, but it
+measures magnitude only — never direction. An efferent coupling of 11 states 
that a
+package reaches eleven classes it does not own and says nothing about which 
ones, so the
+directed feature-to-feature edges had to be derived separately from the import 
graph.
+
+The *second pass* uses a Gradle plugin, 
`io.github.usekylis.java-architecture-metrics`,
+applied to the build itself. It computes the same Robert C. Martin set and 
additionally
+reports, for every component, the set of components it depends on and the set 
that depend
+on it. That is the directed matrix, produced by the build rather than 
reconstructed
+afterwards, which makes it the authoritative source for the violation counts.
+
+The two passes agree on 11 of the 17 comparable features. Where they differ 
the cause is
+taxonomy rather than measurement — most of it because fixed and recurring 
deposits have
+no package of their own and roll up into savings in the build-generated view.
+
+==== MetricsTree Setup
+
+[cols="1,3", options="header"]
+|===
+| Component | Version and state
+
+| IDE | IntelliJ IDEA Community Edition 2025.2
+| Plugin | MetricsTree 2026.0.0 (`org.b333vv.metricstree`)
+| Gradle sync | Complete — 141 module descriptors in external module storage
+| Module scoping | Available; the plugin's module selector lists every module 
containing classes
+|===
+
+Open the project, let the Gradle import finish, then open the *MetricsTree* 
tool window
+from the bottom dock. Metrics are calculated from the *Project Metrics* tab; 
the *Class
+Metrics* tab only reports on the file currently open in the editor.
+
+One structural caveat governs how the output can be read. MetricsTree 
aggregates by
+*package*, while Fineract's features are *Gradle modules*, and the two do not 
line up:
+
+> 159 of 1023 main-source packages are split across two to four modules.
+
+----
+org.apache.fineract.portfolio.loanaccount.service    fineract-loan=50, 
fineract-provider=47, fineract-progressive-loan=20, fineract-core=1
+org.apache.fineract.portfolio.loanaccount.domain     fineract-loan=75, 
fineract-progressive-loan=3, fineract-core=2, fineract-provider=2
+org.apache.fineract.infrastructure.security.service  fineract-security=16, 
fineract-core=9, fineract-provider=6
+org.apache.fineract.portfolio.charge.exception       fineract-charge=16, 
fineract-savings=3, fineract-core=1
+----
+
+A dependency between two classes in different modules that happen to share a 
package name
+is *intra-package*, so `Ce` does not count it. Cross-module coupling hidden 
inside a shared
+namespace is invisible to the package-level metric, and scoping runs per 
module does not
+fix it. Only class-level export avoids the problem, because its rows are keyed 
by fully
+qualified class name and each name resolves to exactly one module.
+
+==== How Measurements Were Collected
+
+*Build-generated pass (authoritative).* The Gradle plugin reads compiled 
bytecode rather
+than source: for every class it extracts the package, the abstract and 
interface flags,
+and the set of types referenced from the constant pool. Each class is then 
assigned to a
+component by a classifier that collapses packages into features, and the 
results are
+aggregated per component.
+
+The critical property is how a dependency's target is resolved. The analyzer 
looks the
+target class up in a map built *only from the classes it scanned*; anything 
outside that
+set is discarded. A run scoped to one module therefore computes that module's 
coupling as
+though the rest of the platform did not exist, and every cross-module edge 
silently
+disappears. The measurement consequently runs as a single aggregate scan over 
every Java
+module at once.
+
+*Corroborating pass.* MetricsTree's class-level export was aggregated per 
module to
+provide the feature-envy view — `ATFD`, `FDP` and `LAA` — which the 
build-generated
+report does not compute. Measured against this checkout, 5336 top-level 
classes across 36
+modules resolve with zero name collisions, so attribution is exact rather than
+best-effort.
+
+==== Reproducing These Results
+
+The build-generated figures are reproduced with a single command:
+
+[source,bash]
+----
+./gradlew architectureMetricsReport
+----
+
+It runs two tasks in sequence. `architectureMetrics` scans every module's 
compiled classes
+and writes `fineract-feature-metrics.json` and `.csv` under 
`build/reports/architecture`.
+`architectureMetricsDiagrams` then regenerates the Vega-Lite specifications in
+`fineract-doc/src/docs/en/diagrams` from that report, so the diagrams in this 
chapter
+cannot drift from the numbers they are drawn from.

Review Comment:
   The reproduction section appears out of sync with the Gradle tasks added in 
this PR: it states that `architectureMetrics` writes 
`fineract-feature-metrics.json` under `build/reports/architecture`, but the 
build config writes `fineract-package-metrics.*` under 
`build/reports/architecture/packages/` and `fineract-feature-metrics.*` under 
`build/reports/architecture/features/` via `architectureMetricsFeatures`. 
Aligning the task names and paths here will make the docs reproducible.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to