sourcecode 2.5.4__py3-none-any.whl → 2.5.5__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 CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "2.5.4"
7
+ __version__ = "2.5.5"
@@ -4,6 +4,8 @@ import re
4
4
  from pathlib import Path
5
5
  from typing import Literal, Optional
6
6
 
7
+ from sourcecode.reconciliation import incoherent_layers
8
+
7
9
  from sourcecode.schema import (
8
10
  ArchitectureAnalysis,
9
11
  ArchitectureDomain,
@@ -139,8 +141,14 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
139
141
  "application": ["application", "usecases", "usecase"],
140
142
  "infrastructure": ["infrastructure", "infra", "adapters", "persistence"],
141
143
  },
144
+ # P1-D residue (found by measurement, 2026-07-16): "core" belongs with
145
+ # "api"/"store"/"db" — it names a module's importance ("the main module"),
146
+ # never an onion/hexagonal DOMAIN layer, and nearly every large JVM repo has
147
+ # one. With it, keycloak's `core/` (623 Java files) + a
148
+ # `representations/adapters/config` package read "Onion Architecture with
149
+ # domain, adapters layers". Kept are the names that state their role.
142
150
  "onion": {
143
- "domain": ["domain", "core"],
151
+ "domain": ["domain"],
144
152
  "application": ["application", "usecases"],
145
153
  "ports": ["ports", "interfaces"],
146
154
  "adapters": ["adapters", "secondary"],
@@ -173,7 +181,7 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
173
181
  "hexagonal": {
174
182
  "port": ["port", "ports", "interface", "interfaces"],
175
183
  "adapter": ["adapter", "adapters"],
176
- "domain": ["domain", "core", "model", "models"],
184
+ "domain": ["domain", "model", "models"], # "core" dropped — see onion
177
185
  },
178
186
  "monorepo": {
179
187
  "apps": ["apps", "applications"],
@@ -196,6 +204,13 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
196
204
  # it; without it the pattern falls through to layered / a weaker match.
197
205
  _PATTERN_REQUIRED_KEYS: dict[str, frozenset[str]] = {
198
206
  "mvc": frozenset({"view"}),
207
+ # Same principle as MVC/view — a pattern must show its DEFINING trait, not
208
+ # just any two of its layers. Hexagonal is literally "Ports and Adapters":
209
+ # a `models/` package next to an `adapters/` package is not one, and that
210
+ # pair alone was enough to label keycloak "Hexagonal Architecture". Onion's
211
+ # defining trait is the domain at the centre.
212
+ "hexagonal": frozenset({"port", "adapter"}),
213
+ "onion": frozenset({"domain"}),
199
214
  }
200
215
 
201
216
  # Minimum share of source files a matched layer must hold to count as a LAYER
@@ -646,6 +661,24 @@ class ArchitectureAnalyzer:
646
661
  continue
647
662
  matched[layer_key] = matched_dirs
648
663
  layer_files[layer_key] = files
664
+
665
+ # SIM-3: a layer must belong to the same PROGRAM as the rest of the
666
+ # claim. Directory names are language-blind, so on a polyglot
667
+ # monorepo one pattern was assembled across two subsystems: keycloak
668
+ # read "MVC with controller, model, view layers" where `view` was a
669
+ # React SPA (87 .tsx, zero Java) sitting beside a 1088-file Java
670
+ # `model`. Mass cannot catch this — the SPA has genuine mass. The
671
+ # reconciliation rule drops layers owned by a different runtime;
672
+ # template/asset-only layers are untouched, so a real Spring-MVC
673
+ # `templates/` view survives.
674
+ for _bad in incoherent_layers(
675
+ pattern_name,
676
+ {k: [source_paths[i] for i in v] for k, v in layer_files.items()},
677
+ source_paths,
678
+ ):
679
+ matched.pop(_bad, None)
680
+ layer_files.pop(_bad, None)
681
+
649
682
  # A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
650
683
  required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
651
684
  if required and not required.issubset(matched.keys()):
@@ -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,142 @@ 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
+
227
+ def runtime_census(layer_files: dict[str, list[str]]) -> dict[str, dict[str, int]]:
228
+ """{layer -> {runtime_family -> file count}} — the language evidence view.
229
+
230
+ Files whose extension carries no runtime (templates, assets, config) are
231
+ simply absent from the census; a layer made only of them yields {}.
232
+ """
233
+ out: dict[str, dict[str, int]] = {}
234
+ for layer, files in layer_files.items():
235
+ counts: dict[str, int] = {}
236
+ for f in files:
237
+ fam = _RUNTIME_FAMILY.get(Path(str(f)).suffix.lower())
238
+ if fam:
239
+ counts[fam] = counts.get(fam, 0) + 1
240
+ out[layer] = counts
241
+ return out
242
+
243
+
244
+ def _rule_layer_language_coherence(
245
+ pattern: str,
246
+ census: dict[str, dict[str, int]],
247
+ repo_runtimes: dict[str, int],
248
+ ) -> list[ReconciliationFinding]:
249
+ """A claimed layer is owned by a different runtime than the repo it describes.
250
+
251
+ The dominant runtime is taken from the WHOLE repo, not from the matched
252
+ layers: which directories happen to match a keyword table is exactly the
253
+ thing under suspicion, so it cannot also be the arbiter. (Measured: on a
254
+ Java backend beside a React SPA, layer-local dominance can invert — the SPA
255
+ out-matches the backend's one matched layer and the JAVA layer gets flagged.)
256
+
257
+ A layer is INCOHERENT iff it holds ZERO files of the repo's runtime AND at
258
+ least one file of another — it is a different program, not a facet of this
259
+ one. A layer with no runtime files at all (templates, assets) is never
260
+ flagged: it does not contradict, it just does not attest.
261
+
262
+ Silent for cross-runtime patterns, whose layers are meant to span runtimes.
263
+ """
264
+ if pattern in _CROSS_RUNTIME_PATTERNS or len(census) < 2:
265
+ return []
266
+ if len(repo_runtimes) < 2:
267
+ return [] # single-runtime repo — nothing to contradict
268
+
269
+ dominant = max(sorted(repo_runtimes), key=lambda f: repo_runtimes[f])
270
+
271
+ incoherent = sorted(
272
+ layer for layer, counts in census.items()
273
+ if not counts.get(dominant) and any(counts.values())
274
+ )
275
+ if not incoherent:
276
+ return []
277
+ return [
278
+ ReconciliationFinding(
279
+ rule="layer_language_coherence",
280
+ symbols=tuple(incoherent),
281
+ detail=(
282
+ f"{len(incoherent)} layer(s) of the '{pattern}' claim hold no "
283
+ f"{dominant} file while the rest of the pattern is {dominant} — "
284
+ "the claim spans two separate programs, so it does not describe "
285
+ "one architecture."
286
+ ),
287
+ )
288
+ ]
289
+
290
+
291
+ def reconcile_architecture_evidence(
292
+ *,
293
+ pattern: str,
294
+ layer_files: dict[str, list[str]],
295
+ repo_files: "list[str]",
296
+ ) -> list[ReconciliationFinding]:
297
+ """Run architecture reconciliation rules. Never raises.
298
+
299
+ Args:
300
+ pattern: candidate pattern name (e.g. "mvc").
301
+ layer_files: {layer -> files matched for that layer}.
302
+ repo_files: every source file considered — supplies the repo's own
303
+ runtime, the arbiter of which layers belong to the claim.
304
+ """
305
+ findings: list[ReconciliationFinding] = []
306
+ try:
307
+ repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
308
+ findings.extend(
309
+ _rule_layer_language_coherence(
310
+ pattern, runtime_census(layer_files), repo_runtimes
311
+ )
312
+ )
313
+ except Exception:
314
+ pass
315
+ return findings
316
+
317
+
318
+ def incoherent_layers(
319
+ pattern: str, layer_files: dict[str, list[str]], repo_files: "list[str]"
320
+ ) -> frozenset[str]:
321
+ """Layer names the coherence rule rejects for `pattern` (convenience view)."""
322
+ out: set[str] = set()
323
+ for f in reconcile_architecture_evidence(
324
+ pattern=pattern, layer_files=layer_files, repo_files=repo_files
325
+ ):
326
+ out.update(f.symbols)
327
+ return frozenset(out)
328
+
329
+
185
330
  def reconcile_extraction_evidence(
186
331
  *,
187
332
  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
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
- "domain": ["domain", "core", "models"],
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,33 @@ class ProjectSummarizer:
415
418
  dir_files.setdefault(seg.lower(), set()).add(i)
416
419
  total = len(file_paths)
417
420
 
418
- def _has_mass(keywords: list[str]) -> bool:
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
- return bool(files) and (not total or len(files) / total >= _LAYER_MIN_SHARE)
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
- score = sum(1 for keywords in layer_keys.values() if _has_mass(keywords))
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
+ # A pattern must show its defining trait here too, or the headline
443
+ # contradicts the architecture prose that already enforces this.
444
+ required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
445
+ if required and not required.issubset(layer_files.keys()):
446
+ continue
447
+ score = len(layer_files)
428
448
  if score > best_score:
429
449
  best_score = score
430
450
  best_pattern = pattern_name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.4
3
+ Version: 2.5.5
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -1,7 +1,7 @@
1
- sourcecode/__init__.py,sha256=XumauaLNdzXEwNGsJupjZthcLY2Qo8ud3CgsBguv8-o,308
1
+ sourcecode/__init__.py,sha256=RsMXFkrSEd5UuAhMfPgfXfYvqM0A43vYcSfn086K17s,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=CoxlEDWn1axufjIT4fpNHd_2HNUKgqBClq9z9k6jp4M,51684
4
+ sourcecode/architecture_analyzer.py,sha256=vQQzcvmniq4go4bWNB2Xm5HlyIoW65cU7ktJqHIfDpU,53614
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=kW8PyB_R7J2Ig0N_QP9Jdm1fQXM1Ebf3PZbaTc80ULA,7941
54
+ sourcecode/reconciliation.py,sha256=L4r_k6c768yq4GSzSOsJHxot2y8bHREA6up68CUkzo4,13853
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=BaBO0WKQi4fX2b2Xjip-GLEZwI3PYiV_U9jmBQlrfFI,25060
78
+ sourcecode/summarizer.py,sha256=4IPhFg3Anrqa9U17i_bBKq70Q7eVefsCEK3idFC4VsY,26217
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.4.dist-info/METADATA,sha256=vFyQq0a39iemrb04OqC6TwsnM_xA7j6PTBz_lbEccTM,10851
141
- sourcecode-2.5.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
142
- sourcecode-2.5.4.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
143
- sourcecode-2.5.4.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
144
- sourcecode-2.5.4.dist-info/RECORD,,
140
+ sourcecode-2.5.5.dist-info/METADATA,sha256=jTeYMssrsUEZUxEVu8WAQq0ihp7c8FtWCT27m2eNeXo,10851
141
+ sourcecode-2.5.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
142
+ sourcecode-2.5.5.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
143
+ sourcecode-2.5.5.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
144
+ sourcecode-2.5.5.dist-info/RECORD,,