sourcecode 2.5.4__py3-none-any.whl → 2.5.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sourcecode/__init__.py +1 -1
- sourcecode/architecture_analyzer.py +75 -4
- sourcecode/reconciliation.py +256 -0
- sourcecode/summarizer.py +27 -5
- {sourcecode-2.5.4.dist-info → sourcecode-2.5.6.dist-info}/METADATA +1 -1
- {sourcecode-2.5.4.dist-info → sourcecode-2.5.6.dist-info}/RECORD +9 -9
- {sourcecode-2.5.4.dist-info → sourcecode-2.5.6.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.4.dist-info → sourcecode-2.5.6.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.4.dist-info → sourcecode-2.5.6.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -4,6 +4,12 @@ import re
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Literal, Optional
|
|
6
6
|
|
|
7
|
+
from sourcecode.reconciliation import (
|
|
8
|
+
incoherent_layers,
|
|
9
|
+
pattern_contradicted,
|
|
10
|
+
unattested_layers,
|
|
11
|
+
)
|
|
12
|
+
|
|
7
13
|
from sourcecode.schema import (
|
|
8
14
|
ArchitectureAnalysis,
|
|
9
15
|
ArchitectureDomain,
|
|
@@ -39,11 +45,27 @@ _SRC_TRANSPARENT = {
|
|
|
39
45
|
"src", "lib", "app", "pkg",
|
|
40
46
|
"main", "java", "kotlin", "scala", "groovy",
|
|
41
47
|
}
|
|
48
|
+
|
|
49
|
+
# `src/main/java17/` is a MULTI-RELEASE source set, as transparent as `java/`
|
|
50
|
+
# — JobRunr's minted a module called "java17".
|
|
51
|
+
_VERSIONED_SRC_RE = re.compile(r"^(?:java|kotlin|scala)\d+$")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_transparent_segment(name: str) -> bool:
|
|
55
|
+
"""A path segment that carries no architectural meaning of its own."""
|
|
56
|
+
low = name.lower()
|
|
57
|
+
return low in _SRC_TRANSPARENT or bool(_VERSIONED_SRC_RE.match(low))
|
|
42
58
|
_CODE_EXTENSIONS = {
|
|
43
59
|
".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs",
|
|
44
60
|
".go", ".java", ".kt", ".rs", ".rb",
|
|
45
61
|
}
|
|
46
|
-
|
|
62
|
+
# Names that never identify a module: generic buckets, and the reverse-DNS
|
|
63
|
+
# package roots every JVM repo starts its namespace with (JobRunr reported a
|
|
64
|
+
# module called "org").
|
|
65
|
+
_GENERIC_NAMES = {
|
|
66
|
+
"utils", "helpers", "common", "shared", "misc", "core", "root", "",
|
|
67
|
+
"org", "com", "io", "net",
|
|
68
|
+
}
|
|
47
69
|
|
|
48
70
|
_TEST_DIRS: frozenset[str] = frozenset({
|
|
49
71
|
"tests", "test", "spec", "specs", "__tests__", "e2e",
|
|
@@ -139,8 +161,14 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
139
161
|
"application": ["application", "usecases", "usecase"],
|
|
140
162
|
"infrastructure": ["infrastructure", "infra", "adapters", "persistence"],
|
|
141
163
|
},
|
|
164
|
+
# P1-D residue (found by measurement, 2026-07-16): "core" belongs with
|
|
165
|
+
# "api"/"store"/"db" — it names a module's importance ("the main module"),
|
|
166
|
+
# never an onion/hexagonal DOMAIN layer, and nearly every large JVM repo has
|
|
167
|
+
# one. With it, keycloak's `core/` (623 Java files) + a
|
|
168
|
+
# `representations/adapters/config` package read "Onion Architecture with
|
|
169
|
+
# domain, adapters layers". Kept are the names that state their role.
|
|
142
170
|
"onion": {
|
|
143
|
-
"domain": ["domain"
|
|
171
|
+
"domain": ["domain"],
|
|
144
172
|
"application": ["application", "usecases"],
|
|
145
173
|
"ports": ["ports", "interfaces"],
|
|
146
174
|
"adapters": ["adapters", "secondary"],
|
|
@@ -173,7 +201,7 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
173
201
|
"hexagonal": {
|
|
174
202
|
"port": ["port", "ports", "interface", "interfaces"],
|
|
175
203
|
"adapter": ["adapter", "adapters"],
|
|
176
|
-
"domain": ["domain", "
|
|
204
|
+
"domain": ["domain", "model", "models"], # "core" dropped — see onion
|
|
177
205
|
},
|
|
178
206
|
"monorepo": {
|
|
179
207
|
"apps": ["apps", "applications"],
|
|
@@ -196,6 +224,13 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
196
224
|
# it; without it the pattern falls through to layered / a weaker match.
|
|
197
225
|
_PATTERN_REQUIRED_KEYS: dict[str, frozenset[str]] = {
|
|
198
226
|
"mvc": frozenset({"view"}),
|
|
227
|
+
# Same principle as MVC/view — a pattern must show its DEFINING trait, not
|
|
228
|
+
# just any two of its layers. Hexagonal is literally "Ports and Adapters":
|
|
229
|
+
# a `models/` package next to an `adapters/` package is not one, and that
|
|
230
|
+
# pair alone was enough to label keycloak "Hexagonal Architecture". Onion's
|
|
231
|
+
# defining trait is the domain at the centre.
|
|
232
|
+
"hexagonal": frozenset({"port", "adapter"}),
|
|
233
|
+
"onion": frozenset({"domain"}),
|
|
199
234
|
}
|
|
200
235
|
|
|
201
236
|
# Minimum share of source files a matched layer must hold to count as a LAYER
|
|
@@ -646,6 +681,28 @@ class ArchitectureAnalyzer:
|
|
|
646
681
|
continue
|
|
647
682
|
matched[layer_key] = matched_dirs
|
|
648
683
|
layer_files[layer_key] = files
|
|
684
|
+
|
|
685
|
+
# SIM-3: a layer must belong to the same PROGRAM as the rest of the
|
|
686
|
+
# claim. Directory names are language-blind, so on a polyglot
|
|
687
|
+
# monorepo one pattern was assembled across two subsystems: keycloak
|
|
688
|
+
# read "MVC with controller, model, view layers" where `view` was a
|
|
689
|
+
# React SPA (87 .tsx, zero Java) sitting beside a 1088-file Java
|
|
690
|
+
# `model`. Mass cannot catch this — the SPA has genuine mass. The
|
|
691
|
+
# reconciliation rule drops layers owned by a different runtime;
|
|
692
|
+
# template/asset-only layers are untouched, so a real Spring-MVC
|
|
693
|
+
# `templates/` view survives.
|
|
694
|
+
_layer_paths = {k: [source_paths[i] for i in v] for k, v in layer_files.items()}
|
|
695
|
+
for _bad in incoherent_layers(pattern_name, _layer_paths, source_paths):
|
|
696
|
+
matched.pop(_bad, None)
|
|
697
|
+
layer_files.pop(_bad, None)
|
|
698
|
+
|
|
699
|
+
# A claim that IS a runtime split must show one: JobRunr, a JVM
|
|
700
|
+
# library, read "Fullstack with frontend, backend layers" off a
|
|
701
|
+
# `dashboard/ui/model/` package of 16 Java files. Nothing to prune —
|
|
702
|
+
# the pattern itself is what the evidence contradicts.
|
|
703
|
+
if pattern_contradicted(pattern_name, _layer_paths, source_paths):
|
|
704
|
+
continue
|
|
705
|
+
|
|
649
706
|
# A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
|
|
650
707
|
required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
|
|
651
708
|
if required and not required.issubset(matched.keys()):
|
|
@@ -809,6 +866,14 @@ class ArchitectureAnalyzer:
|
|
|
809
866
|
k: v for k, v in layer_files.items()
|
|
810
867
|
if v and (not total or len(v) / total >= _LAYER_MIN_SHARE)
|
|
811
868
|
}
|
|
869
|
+
# P1-D residue: these stem patterns are Python-shaped (cli.py,
|
|
870
|
+
# *_analyzer.py, config.py). On a Java repo almost nothing matches, so
|
|
871
|
+
# the few stray hits are not a layering — petclinic's "Layered
|
|
872
|
+
# Architecture with orchestration, data layers" rested on ONE directory
|
|
873
|
+
# entry plus a settings.gradle, with 47 Java files visible and ignored.
|
|
874
|
+
# A logical code layer that holds no code of this repo is not one.
|
|
875
|
+
for _bad in unattested_layers(non_empty, paths):
|
|
876
|
+
non_empty.pop(_bad, None)
|
|
812
877
|
if len(non_empty) >= 2:
|
|
813
878
|
return "layered", [
|
|
814
879
|
ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
|
|
@@ -828,13 +893,19 @@ class ArchitectureAnalyzer:
|
|
|
828
893
|
for p in paths:
|
|
829
894
|
parts = p.replace("\\", "/").split("/")
|
|
830
895
|
for part in parts[:-1]:
|
|
831
|
-
if (
|
|
896
|
+
if (not _is_transparent_segment(part)
|
|
832
897
|
and part.lower() not in _NON_SOURCE_DIRS
|
|
833
898
|
and part.lower() not in _GENERIC_NAMES):
|
|
834
899
|
module_files.setdefault(part, []).append(p)
|
|
835
900
|
break
|
|
836
901
|
|
|
837
902
|
meaningful = {k: v for k, v in module_files.items() if len(v) >= 3}
|
|
903
|
+
# P1-D residue: a MODULE of this repo must hold this repo's code. File
|
|
904
|
+
# count alone let build and asset directories be reported as modules —
|
|
905
|
+
# ofbiz "docker, framework, runtime, gradle", neo4j "packaging",
|
|
906
|
+
# eureka "images" (five PNGs). The real modules survive untouched.
|
|
907
|
+
for _bad in unattested_layers(meaningful, paths):
|
|
908
|
+
meaningful.pop(_bad, None)
|
|
838
909
|
if len(meaningful) >= 2:
|
|
839
910
|
return "modular", [
|
|
840
911
|
ArchitectureLayer(name=k, pattern="modular", files=v, confidence="low")
|
sourcecode/reconciliation.py
CHANGED
|
@@ -34,12 +34,21 @@ Rule registry (extraction):
|
|
|
34
34
|
declarations the regex extractor cannot bind, e.g. `public class\n
|
|
35
35
|
Name` split across lines, Unicode type identifiers).
|
|
36
36
|
|
|
37
|
+
Rule registry (architecture):
|
|
38
|
+
layer_language_coherence — a directory-name-matched layer holds NO file in
|
|
39
|
+
the runtime that carries the rest of the claimed pattern, while being
|
|
40
|
+
owned by a DIFFERENT runtime. The layer table and the file-language
|
|
41
|
+
census are contradicting each other: the "architecture" spans two
|
|
42
|
+
subsystems (SIM-3: keycloak's "MVC … view layer" was a React SPA, zero
|
|
43
|
+
Java files, sitting beside a 1088-file Java model).
|
|
44
|
+
|
|
37
45
|
Never raises. Empty evidence → no findings.
|
|
38
46
|
"""
|
|
39
47
|
from __future__ import annotations
|
|
40
48
|
|
|
41
49
|
import re
|
|
42
50
|
from dataclasses import dataclass
|
|
51
|
+
from pathlib import Path
|
|
43
52
|
|
|
44
53
|
|
|
45
54
|
@dataclass(frozen=True)
|
|
@@ -182,6 +191,253 @@ def _rule_parse_coverage(
|
|
|
182
191
|
]
|
|
183
192
|
|
|
184
193
|
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Architecture reconciliation (layer table × file-language census)
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
# Source extension → RUNTIME FAMILY. Grouping by runtime, not by extension, is
|
|
199
|
+
# what makes the rule correct: a Kotlin `view/` beside Java `model/` is ONE
|
|
200
|
+
# system (same runtime, direct interop) and must not be called a contradiction,
|
|
201
|
+
# while a TypeScript `view/` beside them is a separate program. Extensions
|
|
202
|
+
# absent here (.html/.ftl/.jsp/.css/.xml …) are deliberately unmapped: a
|
|
203
|
+
# template or asset carries no runtime of its own, so it can never contradict a
|
|
204
|
+
# claim — that carve-out is what keeps a genuine Spring-MVC `templates/` view
|
|
205
|
+
# layer intact.
|
|
206
|
+
_RUNTIME_FAMILY: dict[str, str] = {
|
|
207
|
+
".java": "jvm", ".kt": "jvm", ".kts": "jvm", ".scala": "jvm", ".groovy": "jvm",
|
|
208
|
+
".js": "js", ".jsx": "js", ".ts": "js", ".tsx": "js", ".mjs": "js", ".cjs": "js",
|
|
209
|
+
".py": "python",
|
|
210
|
+
".go": "go",
|
|
211
|
+
".rs": "rust",
|
|
212
|
+
".rb": "ruby",
|
|
213
|
+
".cs": "dotnet",
|
|
214
|
+
".php": "php",
|
|
215
|
+
".swift": "swift",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Patterns that describe REPO COMPOSITION rather than one program's internal
|
|
219
|
+
# architecture — their layers are SUPPOSED to span runtimes, so the coherence
|
|
220
|
+
# rule must never fire on them. (mvc/layered/onion/hexagonal/clean are claims
|
|
221
|
+
# about a single program and must hold together in one runtime.)
|
|
222
|
+
_CROSS_RUNTIME_PATTERNS: frozenset[str] = frozenset({
|
|
223
|
+
"fullstack", "microservices", "monorepo",
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
# Patterns whose meaning IS the runtime split — a "fullstack" repo names a
|
|
227
|
+
# frontend TIER and a backend TIER. If every layer runs on the same runtime
|
|
228
|
+
# there is no split, and the claim is a directory-name coincidence: JobRunr, a
|
|
229
|
+
# JVM library, read "Fullstack with frontend, backend layers" off a
|
|
230
|
+
# `dashboard/ui/model/` package holding 16 JAVA files. (Not monorepo or
|
|
231
|
+
# microservices: an all-TypeScript monorepo, or same-runtime services, are
|
|
232
|
+
# perfectly real.)
|
|
233
|
+
_SPLIT_REQUIRED_PATTERNS: frozenset[str] = frozenset({"fullstack"})
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def runtime_census(layer_files: dict[str, list[str]]) -> dict[str, dict[str, int]]:
|
|
237
|
+
"""{layer -> {runtime_family -> file count}} — the language evidence view.
|
|
238
|
+
|
|
239
|
+
Files whose extension carries no runtime (templates, assets, config) are
|
|
240
|
+
simply absent from the census; a layer made only of them yields {}.
|
|
241
|
+
"""
|
|
242
|
+
out: dict[str, dict[str, int]] = {}
|
|
243
|
+
for layer, files in layer_files.items():
|
|
244
|
+
counts: dict[str, int] = {}
|
|
245
|
+
for f in files:
|
|
246
|
+
fam = _RUNTIME_FAMILY.get(Path(str(f)).suffix.lower())
|
|
247
|
+
if fam:
|
|
248
|
+
counts[fam] = counts.get(fam, 0) + 1
|
|
249
|
+
out[layer] = counts
|
|
250
|
+
return out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _rule_layer_language_coherence(
|
|
254
|
+
pattern: str,
|
|
255
|
+
census: dict[str, dict[str, int]],
|
|
256
|
+
repo_runtimes: dict[str, int],
|
|
257
|
+
) -> list[ReconciliationFinding]:
|
|
258
|
+
"""A claimed layer is owned by a different runtime than the repo it describes.
|
|
259
|
+
|
|
260
|
+
The dominant runtime is taken from the WHOLE repo, not from the matched
|
|
261
|
+
layers: which directories happen to match a keyword table is exactly the
|
|
262
|
+
thing under suspicion, so it cannot also be the arbiter. (Measured: on a
|
|
263
|
+
Java backend beside a React SPA, layer-local dominance can invert — the SPA
|
|
264
|
+
out-matches the backend's one matched layer and the JAVA layer gets flagged.)
|
|
265
|
+
|
|
266
|
+
A layer is INCOHERENT iff it holds ZERO files of the repo's runtime AND at
|
|
267
|
+
least one file of another — it is a different program, not a facet of this
|
|
268
|
+
one. A layer with no runtime files at all (templates, assets) is never
|
|
269
|
+
flagged: it does not contradict, it just does not attest.
|
|
270
|
+
|
|
271
|
+
Silent for cross-runtime patterns, whose layers are meant to span runtimes.
|
|
272
|
+
"""
|
|
273
|
+
if pattern in _CROSS_RUNTIME_PATTERNS or len(census) < 2:
|
|
274
|
+
return []
|
|
275
|
+
if len(repo_runtimes) < 2:
|
|
276
|
+
return [] # single-runtime repo — nothing to contradict
|
|
277
|
+
|
|
278
|
+
dominant = max(sorted(repo_runtimes), key=lambda f: repo_runtimes[f])
|
|
279
|
+
|
|
280
|
+
incoherent = sorted(
|
|
281
|
+
layer for layer, counts in census.items()
|
|
282
|
+
if not counts.get(dominant) and any(counts.values())
|
|
283
|
+
)
|
|
284
|
+
if not incoherent:
|
|
285
|
+
return []
|
|
286
|
+
return [
|
|
287
|
+
ReconciliationFinding(
|
|
288
|
+
rule="layer_language_coherence",
|
|
289
|
+
symbols=tuple(incoherent),
|
|
290
|
+
detail=(
|
|
291
|
+
f"{len(incoherent)} layer(s) of the '{pattern}' claim hold no "
|
|
292
|
+
f"{dominant} file while the rest of the pattern is {dominant} — "
|
|
293
|
+
"the claim spans two separate programs, so it does not describe "
|
|
294
|
+
"one architecture."
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _rule_layer_code_attestation(
|
|
301
|
+
census: dict[str, dict[str, int]],
|
|
302
|
+
repo_runtimes: dict[str, int],
|
|
303
|
+
) -> list[ReconciliationFinding]:
|
|
304
|
+
"""A claimed code module holds no code of the repo's runtime.
|
|
305
|
+
|
|
306
|
+
Stricter sibling of `layer_language_coherence`, for claims whose layers are
|
|
307
|
+
modules or logical code layers — their whole role is to hold code, so a
|
|
308
|
+
directory with none is not one of them. (`layer_language_coherence` cannot
|
|
309
|
+
serve here: it deliberately ignores layers with no runtime files at all, so
|
|
310
|
+
that a real Spring-MVC `templates/` view survives. A view may be templates;
|
|
311
|
+
a MODULE may not.)
|
|
312
|
+
|
|
313
|
+
Measured on the field-test repos: ofbiz's "modular architecture" listed
|
|
314
|
+
`docker`, `gradle`, `runtime` and `themes` (shell scripts, wrapper jars, a
|
|
315
|
+
.gitignore, images — zero Java); neo4j listed `packaging`; eureka listed
|
|
316
|
+
`images` (five PNGs); petclinic's whole "Layered Architecture with
|
|
317
|
+
orchestration, data layers" rested on one directory entry and a
|
|
318
|
+
settings.gradle.
|
|
319
|
+
"""
|
|
320
|
+
if not repo_runtimes:
|
|
321
|
+
return []
|
|
322
|
+
dominant = max(sorted(repo_runtimes), key=lambda f: repo_runtimes[f])
|
|
323
|
+
unattested = sorted(
|
|
324
|
+
layer for layer, counts in census.items() if not counts.get(dominant)
|
|
325
|
+
)
|
|
326
|
+
if not unattested:
|
|
327
|
+
return []
|
|
328
|
+
return [
|
|
329
|
+
ReconciliationFinding(
|
|
330
|
+
rule="layer_code_attestation",
|
|
331
|
+
symbols=tuple(unattested),
|
|
332
|
+
detail=(
|
|
333
|
+
f"{len(unattested)} claimed layer(s) hold no {dominant} code — "
|
|
334
|
+
"a directory without the repo's code is not a module of it."
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _rule_composition_runtime_split(
|
|
341
|
+
pattern: str,
|
|
342
|
+
census: dict[str, dict[str, int]],
|
|
343
|
+
) -> list[ReconciliationFinding]:
|
|
344
|
+
"""A pattern that names a runtime split shows none.
|
|
345
|
+
|
|
346
|
+
The mirror image of `layer_language_coherence`: a claim about one program
|
|
347
|
+
must NOT span runtimes; a claim that IS the split must show it. Silent
|
|
348
|
+
unless the pattern's layers carry runtime evidence at all.
|
|
349
|
+
"""
|
|
350
|
+
if pattern not in _SPLIT_REQUIRED_PATTERNS or len(census) < 2:
|
|
351
|
+
return []
|
|
352
|
+
families = {fam for counts in census.values() for fam in counts}
|
|
353
|
+
if len(families) != 1:
|
|
354
|
+
return [] # a real split, or no runtime evidence to judge
|
|
355
|
+
only = next(iter(families))
|
|
356
|
+
return [
|
|
357
|
+
ReconciliationFinding(
|
|
358
|
+
rule="composition_runtime_split",
|
|
359
|
+
symbols=tuple(sorted(census)),
|
|
360
|
+
detail=(
|
|
361
|
+
f"the '{pattern}' claim names a runtime split but every layer is "
|
|
362
|
+
f"{only} — the layer names are a coincidence, not two tiers."
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def unattested_layers(
|
|
369
|
+
layer_files: dict[str, list[str]], repo_files: "list[str]"
|
|
370
|
+
) -> frozenset[str]:
|
|
371
|
+
"""Layers holding no code of the repo's runtime (convenience view).
|
|
372
|
+
|
|
373
|
+
For consumers claiming code modules / logical code layers. Never raises.
|
|
374
|
+
"""
|
|
375
|
+
try:
|
|
376
|
+
repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
|
|
377
|
+
out: set[str] = set()
|
|
378
|
+
for f in _rule_layer_code_attestation(runtime_census(layer_files), repo_runtimes):
|
|
379
|
+
out.update(f.symbols)
|
|
380
|
+
return frozenset(out)
|
|
381
|
+
except Exception:
|
|
382
|
+
return frozenset()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def reconcile_architecture_evidence(
|
|
386
|
+
*,
|
|
387
|
+
pattern: str,
|
|
388
|
+
layer_files: dict[str, list[str]],
|
|
389
|
+
repo_files: "list[str]",
|
|
390
|
+
) -> list[ReconciliationFinding]:
|
|
391
|
+
"""Run architecture reconciliation rules. Never raises.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
pattern: candidate pattern name (e.g. "mvc").
|
|
395
|
+
layer_files: {layer -> files matched for that layer}.
|
|
396
|
+
repo_files: every source file considered — supplies the repo's own
|
|
397
|
+
runtime, the arbiter of which layers belong to the claim.
|
|
398
|
+
"""
|
|
399
|
+
findings: list[ReconciliationFinding] = []
|
|
400
|
+
try:
|
|
401
|
+
census = runtime_census(layer_files)
|
|
402
|
+
repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
|
|
403
|
+
findings.extend(
|
|
404
|
+
_rule_layer_language_coherence(pattern, census, repo_runtimes)
|
|
405
|
+
)
|
|
406
|
+
findings.extend(_rule_composition_runtime_split(pattern, census))
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
return findings
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def pattern_contradicted(
|
|
413
|
+
pattern: str, layer_files: dict[str, list[str]], repo_files: "list[str]"
|
|
414
|
+
) -> bool:
|
|
415
|
+
"""True when a rule rejects the PATTERN itself, not just some of its layers.
|
|
416
|
+
|
|
417
|
+
`incoherent_layers` prunes layers and lets the pattern re-qualify on what
|
|
418
|
+
is left; a split-less "fullstack" has nothing to re-qualify with — the
|
|
419
|
+
claim as a whole is the thing contradicted.
|
|
420
|
+
"""
|
|
421
|
+
return any(
|
|
422
|
+
f.rule == "composition_runtime_split"
|
|
423
|
+
for f in reconcile_architecture_evidence(
|
|
424
|
+
pattern=pattern, layer_files=layer_files, repo_files=repo_files
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def incoherent_layers(
|
|
430
|
+
pattern: str, layer_files: dict[str, list[str]], repo_files: "list[str]"
|
|
431
|
+
) -> frozenset[str]:
|
|
432
|
+
"""Layer names the coherence rule rejects for `pattern` (convenience view)."""
|
|
433
|
+
out: set[str] = set()
|
|
434
|
+
for f in reconcile_architecture_evidence(
|
|
435
|
+
pattern=pattern, layer_files=layer_files, repo_files=repo_files
|
|
436
|
+
):
|
|
437
|
+
out.update(f.symbols)
|
|
438
|
+
return frozenset(out)
|
|
439
|
+
|
|
440
|
+
|
|
185
441
|
def reconcile_extraction_evidence(
|
|
186
442
|
*,
|
|
187
443
|
type_decl_files: frozenset[str],
|
sourcecode/summarizer.py
CHANGED
|
@@ -8,7 +8,8 @@ No API calls — templates applied directly over SourceMap.
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from sourcecode.architecture_analyzer import _LAYER_MIN_SHARE
|
|
11
|
+
from sourcecode.architecture_analyzer import _LAYER_MIN_SHARE, _PATTERN_REQUIRED_KEYS
|
|
12
|
+
from sourcecode.reconciliation import incoherent_layers, pattern_contradicted
|
|
12
13
|
from sourcecode.detectors.parsers import load_json_file, load_toml_file
|
|
13
14
|
from sourcecode.tree_utils import safe_read_text
|
|
14
15
|
from sourcecode.entrypoint_classifier import is_production_entry_point
|
|
@@ -62,7 +63,9 @@ _ARCH_LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
62
63
|
"hexagonal": {
|
|
63
64
|
"port": ["ports", "interfaces"],
|
|
64
65
|
"adapter": ["adapters"],
|
|
65
|
-
|
|
66
|
+
# P1-D residue: "core" names a module's importance, not a domain layer
|
|
67
|
+
# (keycloak `core/` = 623 Java files). Keep in step with the analyzer.
|
|
68
|
+
"domain": ["domain", "models"],
|
|
66
69
|
},
|
|
67
70
|
"fullstack": {
|
|
68
71
|
"frontend": ["frontend", "client", "web", "ui", "pages", "components"],
|
|
@@ -415,16 +418,35 @@ class ProjectSummarizer:
|
|
|
415
418
|
dir_files.setdefault(seg.lower(), set()).add(i)
|
|
416
419
|
total = len(file_paths)
|
|
417
420
|
|
|
418
|
-
def
|
|
421
|
+
def _layer_paths(keywords: list[str]) -> list[str]:
|
|
419
422
|
files: set[int] = set()
|
|
420
423
|
for d in keywords:
|
|
421
424
|
files |= dir_files.get(d, set())
|
|
422
|
-
|
|
425
|
+
if not files or (total and len(files) / total < _LAYER_MIN_SHARE):
|
|
426
|
+
return []
|
|
427
|
+
return [file_paths[i] for i in sorted(files)]
|
|
423
428
|
|
|
424
429
|
best_pattern: str | None = None
|
|
425
430
|
best_score = 0
|
|
426
431
|
for pattern_name, layer_keys in _ARCH_LAYER_PATTERNS.items():
|
|
427
|
-
|
|
432
|
+
layer_files: dict[str, list[str]] = {}
|
|
433
|
+
for layer, keywords in layer_keys.items():
|
|
434
|
+
paths = _layer_paths(keywords)
|
|
435
|
+
if paths:
|
|
436
|
+
layer_files[layer] = paths
|
|
437
|
+
# SIM-3: same coherence rule the architecture prose applies — this
|
|
438
|
+
# table is the headline's copy, and a fix applied to only one of the
|
|
439
|
+
# two duplicated tables leaves the other one lying.
|
|
440
|
+
for _bad in incoherent_layers(pattern_name, layer_files, file_paths):
|
|
441
|
+
layer_files.pop(_bad, None)
|
|
442
|
+
if pattern_contradicted(pattern_name, layer_files, file_paths):
|
|
443
|
+
continue
|
|
444
|
+
# A pattern must show its defining trait here too, or the headline
|
|
445
|
+
# contradicts the architecture prose that already enforces this.
|
|
446
|
+
required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
|
|
447
|
+
if required and not required.issubset(layer_files.keys()):
|
|
448
|
+
continue
|
|
449
|
+
score = len(layer_files)
|
|
428
450
|
if score > best_score:
|
|
429
451
|
best_score = score
|
|
430
452
|
best_pattern = pattern_name
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=vjtgJnZwnfESbFdca_Oeq9DtPkuQkSn7b-N3LsiDp78,308
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/archetype.py,sha256=d7yoN6Tj4OBj-VgSMPEKg4WF9H8FypnZ5A6AkUh5oIE,34484
|
|
4
|
-
sourcecode/architecture_analyzer.py,sha256=
|
|
4
|
+
sourcecode/architecture_analyzer.py,sha256=1CEIeFtuxdzeFaeUtA5QuDmFkb5eilUK8zRmk9tcDxc,55644
|
|
5
5
|
sourcecode/architecture_summary.py,sha256=pkl6lIOnSy-poa9EzW43U0MkXhx2FDfAFh8pDXLpcTo,26630
|
|
6
6
|
sourcecode/ast_extractor.py,sha256=FEsYTsMCXbJfSYQZRk1k-I-PjUNGnKqO1IxMftsgA5U,51351
|
|
7
7
|
sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
|
|
@@ -51,7 +51,7 @@ sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
|
|
|
51
51
|
sourcecode/prepare_context.py,sha256=PYygAKL_jwz8gGElgM9mroUSih18UKSSjv08ndfl4aA,222877
|
|
52
52
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
53
53
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
54
|
-
sourcecode/reconciliation.py,sha256=
|
|
54
|
+
sourcecode/reconciliation.py,sha256=j3PctM_9FlaAV_OQ5_LjNhuZCehJ-XIwLo_iQbRQKTo,18335
|
|
55
55
|
sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
56
56
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
57
57
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
@@ -75,7 +75,7 @@ sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16
|
|
|
75
75
|
sourcecode/spring_security_audit.py,sha256=Rk-aSohezdc7YDYbSoJquVnwpkDB8ty1BCD-4Hc4R5A,22832
|
|
76
76
|
sourcecode/spring_semantic.py,sha256=jteQ1PkY9ArFJv0embg_jBIdbOxqrk9mQ2Xz8OF_FKA,14214
|
|
77
77
|
sourcecode/spring_tx_analyzer.py,sha256=QMXkxa_hrqDVN3D8WJsd0nT7vfdZCT1E8ZWNoFFbvgU,34692
|
|
78
|
-
sourcecode/summarizer.py,sha256=
|
|
78
|
+
sourcecode/summarizer.py,sha256=E70QILQ8cCNIH485c7lVMv5dXOpUn8qx1habi1_cn6o,26340
|
|
79
79
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
80
80
|
sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
|
|
81
81
|
sourcecode/validation_inference.py,sha256=UtmYv-d4TEzT1ihGEipAhtXDrwq0XtVtjMpb3i7NsNc,10815
|
|
@@ -137,8 +137,8 @@ sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i
|
|
|
137
137
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
138
138
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
139
139
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
140
|
-
sourcecode-2.5.
|
|
141
|
-
sourcecode-2.5.
|
|
142
|
-
sourcecode-2.5.
|
|
143
|
-
sourcecode-2.5.
|
|
144
|
-
sourcecode-2.5.
|
|
140
|
+
sourcecode-2.5.6.dist-info/METADATA,sha256=MW8dYaMUIriCSIBFCDunGYxcxAK0p_0WF1fQHY4jidE,10851
|
|
141
|
+
sourcecode-2.5.6.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
142
|
+
sourcecode-2.5.6.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
143
|
+
sourcecode-2.5.6.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
144
|
+
sourcecode-2.5.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|