sourcecode 2.5.5__py3-none-any.whl → 2.5.7__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 +46 -8
- sourcecode/architecture_summary.py +55 -6
- sourcecode/confidence_analyzer.py +39 -0
- sourcecode/reconciliation.py +305 -3
- sourcecode/summarizer.py +3 -1
- {sourcecode-2.5.5.dist-info → sourcecode-2.5.7.dist-info}/METADATA +1 -1
- {sourcecode-2.5.5.dist-info → sourcecode-2.5.7.dist-info}/RECORD +11 -11
- {sourcecode-2.5.5.dist-info → sourcecode-2.5.7.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.5.dist-info → sourcecode-2.5.7.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.5.dist-info → sourcecode-2.5.7.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -4,7 +4,11 @@ import re
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Literal, Optional
|
|
6
6
|
|
|
7
|
-
from sourcecode.reconciliation import
|
|
7
|
+
from sourcecode.reconciliation import (
|
|
8
|
+
incoherent_layers,
|
|
9
|
+
pattern_contradicted,
|
|
10
|
+
unattested_layers,
|
|
11
|
+
)
|
|
8
12
|
|
|
9
13
|
from sourcecode.schema import (
|
|
10
14
|
ArchitectureAnalysis,
|
|
@@ -41,11 +45,27 @@ _SRC_TRANSPARENT = {
|
|
|
41
45
|
"src", "lib", "app", "pkg",
|
|
42
46
|
"main", "java", "kotlin", "scala", "groovy",
|
|
43
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))
|
|
44
58
|
_CODE_EXTENSIONS = {
|
|
45
59
|
".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs",
|
|
46
60
|
".go", ".java", ".kt", ".rs", ".rb",
|
|
47
61
|
}
|
|
48
|
-
|
|
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
|
+
}
|
|
49
69
|
|
|
50
70
|
_TEST_DIRS: frozenset[str] = frozenset({
|
|
51
71
|
"tests", "test", "spec", "specs", "__tests__", "e2e",
|
|
@@ -671,14 +691,18 @@ class ArchitectureAnalyzer:
|
|
|
671
691
|
# reconciliation rule drops layers owned by a different runtime;
|
|
672
692
|
# template/asset-only layers are untouched, so a real Spring-MVC
|
|
673
693
|
# `templates/` view survives.
|
|
674
|
-
for
|
|
675
|
-
|
|
676
|
-
{k: [source_paths[i] for i in v] for k, v in layer_files.items()},
|
|
677
|
-
source_paths,
|
|
678
|
-
):
|
|
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):
|
|
679
696
|
matched.pop(_bad, None)
|
|
680
697
|
layer_files.pop(_bad, None)
|
|
681
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
|
+
|
|
682
706
|
# A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
|
|
683
707
|
required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
|
|
684
708
|
if required and not required.issubset(matched.keys()):
|
|
@@ -842,6 +866,14 @@ class ArchitectureAnalyzer:
|
|
|
842
866
|
k: v for k, v in layer_files.items()
|
|
843
867
|
if v and (not total or len(v) / total >= _LAYER_MIN_SHARE)
|
|
844
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)
|
|
845
877
|
if len(non_empty) >= 2:
|
|
846
878
|
return "layered", [
|
|
847
879
|
ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
|
|
@@ -861,13 +893,19 @@ class ArchitectureAnalyzer:
|
|
|
861
893
|
for p in paths:
|
|
862
894
|
parts = p.replace("\\", "/").split("/")
|
|
863
895
|
for part in parts[:-1]:
|
|
864
|
-
if (
|
|
896
|
+
if (not _is_transparent_segment(part)
|
|
865
897
|
and part.lower() not in _NON_SOURCE_DIRS
|
|
866
898
|
and part.lower() not in _GENERIC_NAMES):
|
|
867
899
|
module_files.setdefault(part, []).append(p)
|
|
868
900
|
break
|
|
869
901
|
|
|
870
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)
|
|
871
909
|
if len(meaningful) >= 2:
|
|
872
910
|
return "modular", [
|
|
873
911
|
ArchitectureLayer(name=k, pattern="modular", files=v, confidence="low")
|
|
@@ -205,6 +205,22 @@ class ArchitectureSummarizer:
|
|
|
205
205
|
for f in s.frameworks[:2]
|
|
206
206
|
))[:3]
|
|
207
207
|
|
|
208
|
+
# F-1 reconciliation: the headline describes ONE program, but fw_names is
|
|
209
|
+
# gathered from every stack in the repo. On a polyglot repository that
|
|
210
|
+
# welds a foreign runtime's framework onto the primary's label. Drop what
|
|
211
|
+
# the claimed stack does not attest, and never let the TYPE label — which
|
|
212
|
+
# those same frameworks drove — survive its own evidence (P2-F).
|
|
213
|
+
from sourcecode.reconciliation import type_claim_unattested, unattested_frameworks
|
|
214
|
+
|
|
215
|
+
foreign = unattested_frameworks(primary.stack, sm.project_type, sm.stacks)
|
|
216
|
+
attested = [name for name in fw_names if name not in foreign]
|
|
217
|
+
dropped = [name for name in fw_names if name in foreign]
|
|
218
|
+
if dropped and not attested and type_claim_unattested(
|
|
219
|
+
claimed_stack=primary.stack, project_type=sm.project_type, stacks=sm.stacks
|
|
220
|
+
):
|
|
221
|
+
return self._describe_foreign_evidence(sm, stack_label, runtime, dropped)
|
|
222
|
+
fw_names = attested
|
|
223
|
+
|
|
208
224
|
fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
|
|
209
225
|
if runtime:
|
|
210
226
|
# BUG #4: never assert "rest api" in the headline unless the endpoints
|
|
@@ -237,6 +253,29 @@ class ArchitectureSummarizer:
|
|
|
237
253
|
return f"{stack_label} {runtime.lower()}{fw_str}."
|
|
238
254
|
return f"{stack_label} project{fw_str}."
|
|
239
255
|
|
|
256
|
+
def _describe_foreign_evidence(
|
|
257
|
+
self, sm: SourceMap, stack_label: str, runtime: str, dropped: list[str]
|
|
258
|
+
) -> str:
|
|
259
|
+
"""Headline for a claim whose defining evidence belongs to another stack.
|
|
260
|
+
|
|
261
|
+
Says what IS known instead of asserting a program that does not exist:
|
|
262
|
+
the primary stack, the foreign frameworks, and the stacks that actually
|
|
263
|
+
declare them. The reader gets the same facts, correctly attributed.
|
|
264
|
+
"""
|
|
265
|
+
from sourcecode.context_summarizer import _STACK_LABELS
|
|
266
|
+
from sourcecode.reconciliation import framework_owner_stacks
|
|
267
|
+
|
|
268
|
+
owners = framework_owner_stacks(sm.stacks)
|
|
269
|
+
owner_ids = sorted({sid for name in dropped for sid in owners.get(name, ())})
|
|
270
|
+
owner_labels = [_STACK_LABELS.get(sid, sid) for sid in owner_ids] or ["another"]
|
|
271
|
+
plural = "s" if len(owner_labels) > 1 else ""
|
|
272
|
+
return (
|
|
273
|
+
f"{stack_label} project. Polyglot repository: the {runtime.lower()} "
|
|
274
|
+
f"evidence ({', '.join(dropped)}) is declared by the "
|
|
275
|
+
f"{', '.join(owner_labels)} stack{plural}, not by {stack_label} — no "
|
|
276
|
+
"single stack owns this repository's architecture. See `stacks`."
|
|
277
|
+
)
|
|
278
|
+
|
|
240
279
|
def _endpoint_support(self) -> tuple[int, int]:
|
|
241
280
|
"""Return (total, high_confidence) endpoint counts from the canonical
|
|
242
281
|
Java endpoint extractor — the same source the `endpoints` command uses,
|
|
@@ -412,41 +451,51 @@ class ArchitectureSummarizer:
|
|
|
412
451
|
return None
|
|
413
452
|
|
|
414
453
|
def _summarize_dotnet_entry(self, stacks: list[StackDetection]) -> list[str]:
|
|
454
|
+
# P2-F-RESIDUE(c): every line here counts .csproj files — facts about the
|
|
455
|
+
# .NET stack ALONE. Unscoped, they read as claims about the repository:
|
|
456
|
+
# the confluent examples monorepo (20+ subprojects, mostly Java/Python/Go)
|
|
457
|
+
# reported a bare "1 project detected." beside a headline that had just
|
|
458
|
+
# called it polyglot. Naming the stack costs one word and makes each line
|
|
459
|
+
# true on any repo, so no polyglot test is needed to decide the wording.
|
|
460
|
+
from sourcecode.context_summarizer import _STACK_LABELS
|
|
461
|
+
|
|
415
462
|
dotnet_stacks = [s for s in stacks if s.stack == "dotnet"]
|
|
416
463
|
if not dotnet_stacks:
|
|
417
464
|
return []
|
|
465
|
+
label = _STACK_LABELS.get("dotnet", "dotnet") # same table as the headline
|
|
418
466
|
lines: list[str] = []
|
|
419
467
|
signals = [sig for s in dotnet_stacks for sig in s.signals]
|
|
420
468
|
|
|
421
469
|
for sig in signals:
|
|
422
470
|
if "project" in sig and "detected" in sig:
|
|
423
|
-
|
|
471
|
+
count = sig.split(" ", 1)[0]
|
|
472
|
+
lines.append(f"{label} projects: {count}.")
|
|
424
473
|
break
|
|
425
474
|
|
|
426
475
|
for sig in signals:
|
|
427
476
|
if sig.startswith("project types:"):
|
|
428
477
|
types = sig.removeprefix("project types:").strip()
|
|
429
|
-
lines.append(f"
|
|
478
|
+
lines.append(f"{label} project types: {types}.")
|
|
430
479
|
break
|
|
431
480
|
|
|
432
481
|
for sig in signals:
|
|
433
482
|
if sig.startswith("target frameworks:"):
|
|
434
483
|
fws = sig.removeprefix("target frameworks:").strip()
|
|
435
|
-
lines.append(f"
|
|
484
|
+
lines.append(f"{label} target frameworks: {fws}.")
|
|
436
485
|
break
|
|
437
486
|
|
|
438
487
|
for sig in signals:
|
|
439
488
|
if sig.startswith("architecture:"):
|
|
440
489
|
pattern = sig.removeprefix("architecture:").strip()
|
|
441
|
-
lines.append(f"
|
|
490
|
+
lines.append(f"{label} solution pattern: {pattern}.")
|
|
442
491
|
break
|
|
443
492
|
|
|
444
493
|
framework_names = [f.name for s in dotnet_stacks for f in s.frameworks]
|
|
445
494
|
if framework_names:
|
|
446
|
-
lines.append(f"
|
|
495
|
+
lines.append(f"{label} frameworks: {', '.join(framework_names)}.")
|
|
447
496
|
|
|
448
497
|
if not lines:
|
|
449
|
-
lines.append("
|
|
498
|
+
lines.append(f"{label} solution detected.")
|
|
450
499
|
return lines
|
|
451
500
|
|
|
452
501
|
def _summarize_go_entry(self, path: str, content: str) -> list[str]:
|
|
@@ -102,6 +102,45 @@ class ConfidenceAnalyzer:
|
|
|
102
102
|
if heuristic_only and not any(s.detection_method != "heuristic" for s in sm.stacks):
|
|
103
103
|
anomalies.append("All stacks detected via heuristic only — no manifest found")
|
|
104
104
|
|
|
105
|
+
# ── Anomaly: a claim attributed to a stack that does not attest it ────
|
|
106
|
+
# F-1 reconciliation, second sink. The headline degrades itself
|
|
107
|
+
# (architecture_summary), but `project_type` and `primary_stack` are
|
|
108
|
+
# separate structured fields an agent can read on their own — and they
|
|
109
|
+
# keep asserting the claim this rule refutes. Same rule, same judgement,
|
|
110
|
+
# different surface: here it becomes an anomaly + gap instead of prose.
|
|
111
|
+
if primary_stacks:
|
|
112
|
+
from sourcecode.reconciliation import (
|
|
113
|
+
reconcile_headline_evidence,
|
|
114
|
+
type_claim_unattested,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
_primary = primary_stacks[0]
|
|
118
|
+
for _rf in reconcile_headline_evidence(
|
|
119
|
+
claimed_stack=_primary.stack,
|
|
120
|
+
project_type=sm.project_type,
|
|
121
|
+
stacks=sm.stacks,
|
|
122
|
+
):
|
|
123
|
+
anomalies.append(
|
|
124
|
+
f"{', '.join(_rf.symbols)} declared by another runtime, not by the "
|
|
125
|
+
f"{_primary.stack} primary stack"
|
|
126
|
+
)
|
|
127
|
+
_reason = _rf.detail
|
|
128
|
+
if type_claim_unattested(
|
|
129
|
+
claimed_stack=_primary.stack,
|
|
130
|
+
project_type=sm.project_type,
|
|
131
|
+
stacks=sm.stacks,
|
|
132
|
+
):
|
|
133
|
+
_reason += (
|
|
134
|
+
f' project_type="{sm.project_type}" rests on that same evidence — '
|
|
135
|
+
f"it is not attested for the {_primary.stack} primary stack, and "
|
|
136
|
+
"the repository is polyglot rather than one program."
|
|
137
|
+
)
|
|
138
|
+
gaps.append(AnalysisGap(
|
|
139
|
+
area=_rf.rule,
|
|
140
|
+
reason=_reason,
|
|
141
|
+
impact="medium",
|
|
142
|
+
))
|
|
143
|
+
|
|
105
144
|
# ── Anomaly: entry points all low-confidence ──────────────────────────
|
|
106
145
|
if normalized_entry_points and all(ep.confidence == "low" for ep in normalized_entry_points):
|
|
107
146
|
anomalies.append("All entry points are low-confidence (heuristic/code_signal only)")
|
sourcecode/reconciliation.py
CHANGED
|
@@ -42,6 +42,15 @@ Rule registry (architecture):
|
|
|
42
42
|
subsystems (SIM-3: keycloak's "MVC … view layer" was a React SPA, zero
|
|
43
43
|
Java files, sitting beside a 1088-file Java model).
|
|
44
44
|
|
|
45
|
+
Rule registry (narrative):
|
|
46
|
+
framework_stack_attestation — the headline welds a stack label to a
|
|
47
|
+
framework that no stack of that runtime declares. The stack table and
|
|
48
|
+
the framework-ownership view are contradicting each other: the sentence
|
|
49
|
+
describes one program while the evidence names two (P2-F: the confluent
|
|
50
|
+
examples repo read "C#/.NET rest api using JAX-RS" — a JVM framework,
|
|
51
|
+
declared by a Java stack in one sub-workspace, attributed to a .NET
|
|
52
|
+
primary that declares no framework at all).
|
|
53
|
+
|
|
45
54
|
Never raises. Empty evidence → no findings.
|
|
46
55
|
"""
|
|
47
56
|
from __future__ import annotations
|
|
@@ -223,6 +232,15 @@ _CROSS_RUNTIME_PATTERNS: frozenset[str] = frozenset({
|
|
|
223
232
|
"fullstack", "microservices", "monorepo",
|
|
224
233
|
})
|
|
225
234
|
|
|
235
|
+
# Patterns whose meaning IS the runtime split — a "fullstack" repo names a
|
|
236
|
+
# frontend TIER and a backend TIER. If every layer runs on the same runtime
|
|
237
|
+
# there is no split, and the claim is a directory-name coincidence: JobRunr, a
|
|
238
|
+
# JVM library, read "Fullstack with frontend, backend layers" off a
|
|
239
|
+
# `dashboard/ui/model/` package holding 16 JAVA files. (Not monorepo or
|
|
240
|
+
# microservices: an all-TypeScript monorepo, or same-runtime services, are
|
|
241
|
+
# perfectly real.)
|
|
242
|
+
_SPLIT_REQUIRED_PATTERNS: frozenset[str] = frozenset({"fullstack"})
|
|
243
|
+
|
|
226
244
|
|
|
227
245
|
def runtime_census(layer_files: dict[str, list[str]]) -> dict[str, dict[str, int]]:
|
|
228
246
|
"""{layer -> {runtime_family -> file count}} — the language evidence view.
|
|
@@ -288,6 +306,91 @@ def _rule_layer_language_coherence(
|
|
|
288
306
|
]
|
|
289
307
|
|
|
290
308
|
|
|
309
|
+
def _rule_layer_code_attestation(
|
|
310
|
+
census: dict[str, dict[str, int]],
|
|
311
|
+
repo_runtimes: dict[str, int],
|
|
312
|
+
) -> list[ReconciliationFinding]:
|
|
313
|
+
"""A claimed code module holds no code of the repo's runtime.
|
|
314
|
+
|
|
315
|
+
Stricter sibling of `layer_language_coherence`, for claims whose layers are
|
|
316
|
+
modules or logical code layers — their whole role is to hold code, so a
|
|
317
|
+
directory with none is not one of them. (`layer_language_coherence` cannot
|
|
318
|
+
serve here: it deliberately ignores layers with no runtime files at all, so
|
|
319
|
+
that a real Spring-MVC `templates/` view survives. A view may be templates;
|
|
320
|
+
a MODULE may not.)
|
|
321
|
+
|
|
322
|
+
Measured on the field-test repos: ofbiz's "modular architecture" listed
|
|
323
|
+
`docker`, `gradle`, `runtime` and `themes` (shell scripts, wrapper jars, a
|
|
324
|
+
.gitignore, images — zero Java); neo4j listed `packaging`; eureka listed
|
|
325
|
+
`images` (five PNGs); petclinic's whole "Layered Architecture with
|
|
326
|
+
orchestration, data layers" rested on one directory entry and a
|
|
327
|
+
settings.gradle.
|
|
328
|
+
"""
|
|
329
|
+
if not repo_runtimes:
|
|
330
|
+
return []
|
|
331
|
+
dominant = max(sorted(repo_runtimes), key=lambda f: repo_runtimes[f])
|
|
332
|
+
unattested = sorted(
|
|
333
|
+
layer for layer, counts in census.items() if not counts.get(dominant)
|
|
334
|
+
)
|
|
335
|
+
if not unattested:
|
|
336
|
+
return []
|
|
337
|
+
return [
|
|
338
|
+
ReconciliationFinding(
|
|
339
|
+
rule="layer_code_attestation",
|
|
340
|
+
symbols=tuple(unattested),
|
|
341
|
+
detail=(
|
|
342
|
+
f"{len(unattested)} claimed layer(s) hold no {dominant} code — "
|
|
343
|
+
"a directory without the repo's code is not a module of it."
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _rule_composition_runtime_split(
|
|
350
|
+
pattern: str,
|
|
351
|
+
census: dict[str, dict[str, int]],
|
|
352
|
+
) -> list[ReconciliationFinding]:
|
|
353
|
+
"""A pattern that names a runtime split shows none.
|
|
354
|
+
|
|
355
|
+
The mirror image of `layer_language_coherence`: a claim about one program
|
|
356
|
+
must NOT span runtimes; a claim that IS the split must show it. Silent
|
|
357
|
+
unless the pattern's layers carry runtime evidence at all.
|
|
358
|
+
"""
|
|
359
|
+
if pattern not in _SPLIT_REQUIRED_PATTERNS or len(census) < 2:
|
|
360
|
+
return []
|
|
361
|
+
families = {fam for counts in census.values() for fam in counts}
|
|
362
|
+
if len(families) != 1:
|
|
363
|
+
return [] # a real split, or no runtime evidence to judge
|
|
364
|
+
only = next(iter(families))
|
|
365
|
+
return [
|
|
366
|
+
ReconciliationFinding(
|
|
367
|
+
rule="composition_runtime_split",
|
|
368
|
+
symbols=tuple(sorted(census)),
|
|
369
|
+
detail=(
|
|
370
|
+
f"the '{pattern}' claim names a runtime split but every layer is "
|
|
371
|
+
f"{only} — the layer names are a coincidence, not two tiers."
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def unattested_layers(
|
|
378
|
+
layer_files: dict[str, list[str]], repo_files: "list[str]"
|
|
379
|
+
) -> frozenset[str]:
|
|
380
|
+
"""Layers holding no code of the repo's runtime (convenience view).
|
|
381
|
+
|
|
382
|
+
For consumers claiming code modules / logical code layers. Never raises.
|
|
383
|
+
"""
|
|
384
|
+
try:
|
|
385
|
+
repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
|
|
386
|
+
out: set[str] = set()
|
|
387
|
+
for f in _rule_layer_code_attestation(runtime_census(layer_files), repo_runtimes):
|
|
388
|
+
out.update(f.symbols)
|
|
389
|
+
return frozenset(out)
|
|
390
|
+
except Exception:
|
|
391
|
+
return frozenset()
|
|
392
|
+
|
|
393
|
+
|
|
291
394
|
def reconcile_architecture_evidence(
|
|
292
395
|
*,
|
|
293
396
|
pattern: str,
|
|
@@ -304,17 +407,34 @@ def reconcile_architecture_evidence(
|
|
|
304
407
|
"""
|
|
305
408
|
findings: list[ReconciliationFinding] = []
|
|
306
409
|
try:
|
|
410
|
+
census = runtime_census(layer_files)
|
|
307
411
|
repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
|
|
308
412
|
findings.extend(
|
|
309
|
-
_rule_layer_language_coherence(
|
|
310
|
-
pattern, runtime_census(layer_files), repo_runtimes
|
|
311
|
-
)
|
|
413
|
+
_rule_layer_language_coherence(pattern, census, repo_runtimes)
|
|
312
414
|
)
|
|
415
|
+
findings.extend(_rule_composition_runtime_split(pattern, census))
|
|
313
416
|
except Exception:
|
|
314
417
|
pass
|
|
315
418
|
return findings
|
|
316
419
|
|
|
317
420
|
|
|
421
|
+
def pattern_contradicted(
|
|
422
|
+
pattern: str, layer_files: dict[str, list[str]], repo_files: "list[str]"
|
|
423
|
+
) -> bool:
|
|
424
|
+
"""True when a rule rejects the PATTERN itself, not just some of its layers.
|
|
425
|
+
|
|
426
|
+
`incoherent_layers` prunes layers and lets the pattern re-qualify on what
|
|
427
|
+
is left; a split-less "fullstack" has nothing to re-qualify with — the
|
|
428
|
+
claim as a whole is the thing contradicted.
|
|
429
|
+
"""
|
|
430
|
+
return any(
|
|
431
|
+
f.rule == "composition_runtime_split"
|
|
432
|
+
for f in reconcile_architecture_evidence(
|
|
433
|
+
pattern=pattern, layer_files=layer_files, repo_files=repo_files
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
318
438
|
def incoherent_layers(
|
|
319
439
|
pattern: str, layer_files: dict[str, list[str]], repo_files: "list[str]"
|
|
320
440
|
) -> frozenset[str]:
|
|
@@ -327,6 +447,188 @@ def incoherent_layers(
|
|
|
327
447
|
return frozenset(out)
|
|
328
448
|
|
|
329
449
|
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
# Narrative reconciliation (headline claim × stack ownership)
|
|
452
|
+
# ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
# Stack identifier → RUNTIME FAMILY. Same reasoning as `_RUNTIME_FAMILY` above,
|
|
455
|
+
# applied to the stack ontology instead of file extensions: a Kotlin framework
|
|
456
|
+
# named in a Java headline is ONE program (same runtime, direct interop), while a
|
|
457
|
+
# Java framework named in a .NET headline is a different one. Stacks absent here
|
|
458
|
+
# are deliberately unjudgeable: an unknown stack can never prove a contradiction.
|
|
459
|
+
_STACK_FAMILY: dict[str, str] = {
|
|
460
|
+
"java": "jvm", "kotlin": "jvm", "scala": "jvm", "groovy": "jvm",
|
|
461
|
+
"nodejs": "js", "deno": "js", "bun": "js",
|
|
462
|
+
"python": "python",
|
|
463
|
+
"go": "go",
|
|
464
|
+
"rust": "rust",
|
|
465
|
+
"ruby": "ruby",
|
|
466
|
+
"dotnet": "dotnet",
|
|
467
|
+
"php": "php",
|
|
468
|
+
"swift": "swift",
|
|
469
|
+
"dart": "dart",
|
|
470
|
+
"elixir": "beam", "erlang": "beam",
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
# Project types that are SUPPOSED to span runtimes — their headline names a tier
|
|
474
|
+
# split, so a framework from another runtime is the point, not a contradiction.
|
|
475
|
+
# Same carve-out (and same names) as `_CROSS_RUNTIME_PATTERNS` for layer claims.
|
|
476
|
+
_CROSS_RUNTIME_TYPES = _CROSS_RUNTIME_PATTERNS
|
|
477
|
+
|
|
478
|
+
# Project types DRIVEN by framework presence: `_classify_project_type` pools
|
|
479
|
+
# frameworks across every stack to pick them, while a separate vote picks the
|
|
480
|
+
# primary stack — so on a polyglot repo the type can be decided by one stack's
|
|
481
|
+
# evidence and reported as another's. When the frameworks that drove the type are
|
|
482
|
+
# all foreign to the primary, the type inherits their lie: the sentence
|
|
483
|
+
# "C#/.NET rest api" is the same claim as "using JAX-RS", one clause shorter.
|
|
484
|
+
# Lives here, not in a consumer: BOTH the headline and the confidence/gap surface
|
|
485
|
+
# must judge this identically or the duplicated table lies again.
|
|
486
|
+
_FRAMEWORK_DRIVEN_TYPES: frozenset[str] = frozenset({
|
|
487
|
+
"api", "webapp", "web_mvc", "angular-spa",
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def framework_owner_stacks(stacks) -> dict[str, frozenset[str]]:
|
|
492
|
+
"""{framework name -> stack ids that declare it} — the ownership view.
|
|
493
|
+
|
|
494
|
+
Ownership is already structural: a FrameworkDetection is discovered inside a
|
|
495
|
+
StackDetection's manifests, so which runtime a framework belongs to is
|
|
496
|
+
evidence the detector recorded, never a name lookup (VAI-safe — no rule here
|
|
497
|
+
branches on what the framework is called).
|
|
498
|
+
"""
|
|
499
|
+
out: dict[str, set[str]] = {}
|
|
500
|
+
for stack in stacks or []:
|
|
501
|
+
sid = getattr(stack, "stack", "") or ""
|
|
502
|
+
if not sid:
|
|
503
|
+
continue
|
|
504
|
+
for fw in getattr(stack, "frameworks", None) or []:
|
|
505
|
+
name = getattr(fw, "name", "") or ""
|
|
506
|
+
if name:
|
|
507
|
+
out.setdefault(name, set()).add(sid)
|
|
508
|
+
return {name: frozenset(ids) for name, ids in out.items()}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def framework_owner_families(stacks) -> dict[str, frozenset[str]]:
|
|
512
|
+
"""{framework name -> runtime families that declare it} — the ownership view."""
|
|
513
|
+
return {
|
|
514
|
+
name: frozenset(
|
|
515
|
+
fam for sid in ids if (fam := _STACK_FAMILY.get(sid)) is not None
|
|
516
|
+
)
|
|
517
|
+
for name, ids in framework_owner_stacks(stacks).items()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _rule_framework_stack_attestation(
|
|
522
|
+
claimed_family: str | None,
|
|
523
|
+
owner_families: dict[str, frozenset[str]],
|
|
524
|
+
) -> list[ReconciliationFinding]:
|
|
525
|
+
"""A framework named in a headline is declared by no stack of the claimed runtime.
|
|
526
|
+
|
|
527
|
+
The headline describes ONE program: it welds a stack label ("C#/.NET") to the
|
|
528
|
+
frameworks found anywhere in the repo. On a polyglot repository those can be
|
|
529
|
+
different programs, and the sentence asserts a relationship the evidence
|
|
530
|
+
denies — the confluent examples repo read "C#/.NET rest api using JAX-RS",
|
|
531
|
+
attributing a JVM-only framework, declared by a Java stack in one
|
|
532
|
+
sub-workspace, to a .NET primary that declares no framework at all.
|
|
533
|
+
|
|
534
|
+
Fires only when ownership is PROVEN and disjoint: the framework has at least
|
|
535
|
+
one known owning family and the claimed family is not among them. An unknown
|
|
536
|
+
stack, or a framework no stack claims, yields nothing.
|
|
537
|
+
"""
|
|
538
|
+
if not claimed_family:
|
|
539
|
+
return []
|
|
540
|
+
foreign = sorted(
|
|
541
|
+
name for name, fams in owner_families.items()
|
|
542
|
+
if fams and claimed_family not in fams
|
|
543
|
+
)
|
|
544
|
+
if not foreign:
|
|
545
|
+
return []
|
|
546
|
+
others = sorted({fam for name in foreign for fam in owner_families[name]})
|
|
547
|
+
return [
|
|
548
|
+
ReconciliationFinding(
|
|
549
|
+
rule="framework_stack_attestation",
|
|
550
|
+
symbols=tuple(foreign),
|
|
551
|
+
# Sink-neutral: this detail is read by a prose surface AND by a
|
|
552
|
+
# structured gap, so it states the contradiction, never "the
|
|
553
|
+
# headline says…" — the headline may already have dropped the claim.
|
|
554
|
+
detail=(
|
|
555
|
+
f"{len(foreign)} framework(s) are declared only by "
|
|
556
|
+
f"{', '.join(others)} stack(s), not by the {claimed_family} "
|
|
557
|
+
"primary stack — attributing them to it describes two separate "
|
|
558
|
+
"programs as one."
|
|
559
|
+
),
|
|
560
|
+
)
|
|
561
|
+
]
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def reconcile_headline_evidence(
|
|
565
|
+
*,
|
|
566
|
+
claimed_stack: str,
|
|
567
|
+
project_type: str | None,
|
|
568
|
+
stacks,
|
|
569
|
+
) -> list[ReconciliationFinding]:
|
|
570
|
+
"""Run headline reconciliation rules. Never raises.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
claimed_stack: stack id the headline's label is built from (the primary).
|
|
574
|
+
project_type: claimed type — cross-runtime types are exempt, their
|
|
575
|
+
headline is MEANT to name another tier's framework.
|
|
576
|
+
stacks: every StackDetection, each owning its own frameworks.
|
|
577
|
+
"""
|
|
578
|
+
if project_type in _CROSS_RUNTIME_TYPES:
|
|
579
|
+
return []
|
|
580
|
+
findings: list[ReconciliationFinding] = []
|
|
581
|
+
try:
|
|
582
|
+
findings.extend(
|
|
583
|
+
_rule_framework_stack_attestation(
|
|
584
|
+
_STACK_FAMILY.get(claimed_stack or ""),
|
|
585
|
+
framework_owner_families(stacks),
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
except Exception:
|
|
589
|
+
pass
|
|
590
|
+
return findings
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def type_claim_unattested(
|
|
594
|
+
*, claimed_stack: str, project_type: str | None, stacks
|
|
595
|
+
) -> bool:
|
|
596
|
+
"""True when a framework-driven type label rests ONLY on foreign evidence.
|
|
597
|
+
|
|
598
|
+
The sibling of `_rule_framework_stack_attestation` for the type clause: the
|
|
599
|
+
frameworks that decided `project_type` are declared, every one of them, by
|
|
600
|
+
stacks of another runtime. The narrative consumer degrades the sentence; the
|
|
601
|
+
confidence consumer records the gap — same judgement, two sinks, one table.
|
|
602
|
+
|
|
603
|
+
Provable only: silent when the type is not framework-driven, when the claimed
|
|
604
|
+
stack's runtime is unknown, or when no framework is owned at all.
|
|
605
|
+
"""
|
|
606
|
+
try:
|
|
607
|
+
if project_type not in _FRAMEWORK_DRIVEN_TYPES:
|
|
608
|
+
return False
|
|
609
|
+
family = _STACK_FAMILY.get(claimed_stack or "")
|
|
610
|
+
if not family:
|
|
611
|
+
return False
|
|
612
|
+
owners = framework_owner_families(stacks)
|
|
613
|
+
if not owners:
|
|
614
|
+
return False
|
|
615
|
+
return not any(family in fams for fams in owners.values())
|
|
616
|
+
except Exception:
|
|
617
|
+
return False
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def unattested_frameworks(
|
|
621
|
+
claimed_stack: str, project_type: str | None, stacks
|
|
622
|
+
) -> frozenset[str]:
|
|
623
|
+
"""Framework names the claimed stack does not attest (convenience view)."""
|
|
624
|
+
out: set[str] = set()
|
|
625
|
+
for f in reconcile_headline_evidence(
|
|
626
|
+
claimed_stack=claimed_stack, project_type=project_type, stacks=stacks
|
|
627
|
+
):
|
|
628
|
+
out.update(f.symbols)
|
|
629
|
+
return frozenset(out)
|
|
630
|
+
|
|
631
|
+
|
|
330
632
|
def reconcile_extraction_evidence(
|
|
331
633
|
*,
|
|
332
634
|
type_decl_files: frozenset[str],
|
sourcecode/summarizer.py
CHANGED
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from sourcecode.architecture_analyzer import _LAYER_MIN_SHARE, _PATTERN_REQUIRED_KEYS
|
|
12
|
-
from sourcecode.reconciliation import incoherent_layers
|
|
12
|
+
from sourcecode.reconciliation import incoherent_layers, pattern_contradicted
|
|
13
13
|
from sourcecode.detectors.parsers import load_json_file, load_toml_file
|
|
14
14
|
from sourcecode.tree_utils import safe_read_text
|
|
15
15
|
from sourcecode.entrypoint_classifier import is_production_entry_point
|
|
@@ -439,6 +439,8 @@ class ProjectSummarizer:
|
|
|
439
439
|
# two duplicated tables leaves the other one lying.
|
|
440
440
|
for _bad in incoherent_layers(pattern_name, layer_files, file_paths):
|
|
441
441
|
layer_files.pop(_bad, None)
|
|
442
|
+
if pattern_contradicted(pattern_name, layer_files, file_paths):
|
|
443
|
+
continue
|
|
442
444
|
# A pattern must show its defining trait here too, or the headline
|
|
443
445
|
# contradicts the architecture prose that already enforces this.
|
|
444
446
|
required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=AYI3dPxhkPPbI6cutQ3CdTcxsveWkRyIzUZMCY7lfFs,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=
|
|
5
|
-
sourcecode/architecture_summary.py,sha256=
|
|
4
|
+
sourcecode/architecture_analyzer.py,sha256=1CEIeFtuxdzeFaeUtA5QuDmFkb5eilUK8zRmk9tcDxc,55644
|
|
5
|
+
sourcecode/architecture_summary.py,sha256=oBkjx7hZxmpdKLt4EracjRKHJFOGuPTd6SadTyPxY7Q,29568
|
|
6
6
|
sourcecode/ast_extractor.py,sha256=FEsYTsMCXbJfSYQZRk1k-I-PjUNGnKqO1IxMftsgA5U,51351
|
|
7
7
|
sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
|
|
8
8
|
sourcecode/call_surface.py,sha256=fiqYfHooxN1fX9oQoysq1LS3LoZcobhyUNjAGEZKpwk,4148
|
|
@@ -12,7 +12,7 @@ sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,1207
|
|
|
12
12
|
sourcecode/classifier.py,sha256=9olDN5joeKHcwG9vf73X-t_son16hLVNib0rBbqk1vc,18865
|
|
13
13
|
sourcecode/cli.py,sha256=46Chp0JsEWD_j0HUWOx4tOhrsXSWgWV8JhL5G8SyW4I,322078
|
|
14
14
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
15
|
-
sourcecode/confidence_analyzer.py,sha256=
|
|
15
|
+
sourcecode/confidence_analyzer.py,sha256=KCRg8B4WH15LpOB7oYhE4dD5nou9Xi48RT3vpaO6Ar4,20868
|
|
16
16
|
sourcecode/context_cache.py,sha256=maws79rLKDwyZ7c9Q2LwlIxqZHRFlaKzErtO3mglWoA,25036
|
|
17
17
|
sourcecode/context_graph.py,sha256=WFtUvfxoE8XtQxtRJLvaF4SJuS00ICjNoTk0sH2_V5U,44454
|
|
18
18
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
@@ -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=Ulmo4cNgeBnS1KxWv7kKFkXkwgJaQ70P0x6zC4F0I_I,26342
|
|
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.7.dist-info/METADATA,sha256=WZPw6sppRfqGy5VxcYxS97BHKScBppWAW2UI8eD_TIM,10851
|
|
141
|
+
sourcecode-2.5.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
142
|
+
sourcecode-2.5.7.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
143
|
+
sourcecode-2.5.7.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
144
|
+
sourcecode-2.5.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|