sourcecode 1.30.14__py3-none-any.whl → 1.30.16__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/cli.py +34 -3
- sourcecode/flow_analyzer.py +49 -24
- sourcecode/pr_comment_renderer.py +369 -0
- sourcecode/prepare_context.py +63 -15
- {sourcecode-1.30.14.dist-info → sourcecode-1.30.16.dist-info}/METADATA +48 -25
- {sourcecode-1.30.14.dist-info → sourcecode-1.30.16.dist-info}/RECORD +10 -9
- {sourcecode-1.30.14.dist-info → sourcecode-1.30.16.dist-info}/WHEEL +0 -0
- {sourcecode-1.30.14.dist-info → sourcecode-1.30.16.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.30.14.dist-info → sourcecode-1.30.16.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -161,7 +161,7 @@ Compressed AI-ready context for Java/Spring enterprise codebases.
|
|
|
161
161
|
# Known subcommand names — tokens matching these are routed as subcommands,
|
|
162
162
|
# not consumed as a repository path.
|
|
163
163
|
_SUBCOMMANDS: frozenset[str] = frozenset(
|
|
164
|
-
{"telemetry", "prepare-context", "version", "config", "analyze"}
|
|
164
|
+
{"telemetry", "prepare-context", "version", "config", "analyze", "repo-ir"}
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
# Mutable container holding the path extracted by _preprocess_argv().
|
|
@@ -1513,10 +1513,31 @@ def main(
|
|
|
1513
1513
|
data = redact_dict(data)
|
|
1514
1514
|
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
1515
1515
|
elif compact:
|
|
1516
|
+
if changed_only and _allowed_changed_files:
|
|
1517
|
+
sm = _replace(sm,
|
|
1518
|
+
file_paths=[p for p in sm.file_paths if p in _allowed_changed_files],
|
|
1519
|
+
entry_points=[ep for ep in sm.entry_points if ep.path in _allowed_changed_files],
|
|
1520
|
+
code_notes=[n for n in sm.code_notes if n.path in _allowed_changed_files],
|
|
1521
|
+
)
|
|
1516
1522
|
data = compact_view(sm, no_tree=no_tree, full=full)
|
|
1517
1523
|
if not no_redact:
|
|
1518
1524
|
data = redact_dict(data)
|
|
1519
|
-
|
|
1525
|
+
if format == "yaml":
|
|
1526
|
+
from io import StringIO
|
|
1527
|
+
from ruamel.yaml import YAML as _YAML
|
|
1528
|
+
_yaml = _YAML()
|
|
1529
|
+
_yaml.default_flow_style = False
|
|
1530
|
+
_yaml.representer.add_representer(
|
|
1531
|
+
type(None),
|
|
1532
|
+
lambda dumper, data_val: dumper.represent_scalar(
|
|
1533
|
+
"tag:yaml.org,2002:null", "null"
|
|
1534
|
+
),
|
|
1535
|
+
)
|
|
1536
|
+
_stream = StringIO()
|
|
1537
|
+
_yaml.dump(data, _stream)
|
|
1538
|
+
content = _stream.getvalue()
|
|
1539
|
+
else:
|
|
1540
|
+
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
1520
1541
|
else:
|
|
1521
1542
|
raw_dict = standard_view(sm, include_tree=tree and not no_tree)
|
|
1522
1543
|
if not no_redact:
|
|
@@ -1622,6 +1643,11 @@ def prepare_context_cmd(
|
|
|
1622
1643
|
"--symptom",
|
|
1623
1644
|
help="(fix-bug) Keyword hint for the bug: boosts matching files and surfaces related code notes.",
|
|
1624
1645
|
),
|
|
1646
|
+
format: Optional[str] = typer.Option(
|
|
1647
|
+
None,
|
|
1648
|
+
"--format",
|
|
1649
|
+
help="Output format: json (default) | github-comment (Markdown PR comment for review-pr task)",
|
|
1650
|
+
),
|
|
1625
1651
|
) -> None:
|
|
1626
1652
|
"""Task-specific context for AI coding agents.
|
|
1627
1653
|
|
|
@@ -1895,7 +1921,12 @@ def prepare_context_cmd(
|
|
|
1895
1921
|
if llm_prompt:
|
|
1896
1922
|
out["llm_prompt"] = builder.render_prompt(output)
|
|
1897
1923
|
|
|
1898
|
-
|
|
1924
|
+
if format == "github-comment" and task == "review-pr":
|
|
1925
|
+
from sourcecode.pr_comment_renderer import render_github_comment
|
|
1926
|
+
_pc_content = render_github_comment(out)
|
|
1927
|
+
else:
|
|
1928
|
+
_pc_content = json.dumps(out, indent=2, ensure_ascii=False)
|
|
1929
|
+
|
|
1899
1930
|
if output_path is not None:
|
|
1900
1931
|
output_path.write_text(_pc_content, encoding="utf-8")
|
|
1901
1932
|
else:
|
sourcecode/flow_analyzer.py
CHANGED
|
@@ -33,43 +33,57 @@ _METHOD_NAME_RE = re.compile(
|
|
|
33
33
|
r'(\w+)\s*\(',
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
# Runtime signal patterns: (compiled_regex, note_text)
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
36
|
+
# Runtime signal patterns: (compiled_regex, note_text, epistemic_level)
|
|
37
|
+
# epistemic_level follows the 4-value contract:
|
|
38
|
+
# STRUCTURAL SIGNAL — annotation or type-system evidence directly observed in source
|
|
39
|
+
# INFERRED (LOW CONFIDENCE) — heuristic code-pattern match; no full structural proof
|
|
40
|
+
_RUNTIME_SIGNALS: list[tuple[re.Pattern, str, str]] = [
|
|
40
41
|
# ── Conditional / auth guards ─────────────────────────────────────────────
|
|
41
42
|
(re.compile(r'@PreAuthorize|@Secured|@RolesAllowed', re.IGNORECASE),
|
|
42
|
-
"condition: authorization check present (@PreAuthorize / @Secured)"
|
|
43
|
+
"condition: authorization check present (@PreAuthorize / @Secured)",
|
|
44
|
+
"STRUCTURAL SIGNAL"),
|
|
43
45
|
(re.compile(r'isAuthenticated\(\)|hasRole\(|hasAuthority\(|SecurityContextHolder', re.IGNORECASE),
|
|
44
|
-
"condition: reads authentication context"
|
|
46
|
+
"condition: reads authentication context",
|
|
47
|
+
"STRUCTURAL SIGNAL"),
|
|
45
48
|
(re.compile(r'featureFlag|FeatureToggle|\.isEnabled\s*\(|\.isActive\s*\(', re.IGNORECASE),
|
|
46
|
-
"condition: feature flag gates execution"
|
|
49
|
+
"condition: feature flag gates execution",
|
|
50
|
+
"INFERRED (LOW CONFIDENCE)"),
|
|
47
51
|
# Null/empty guard with early return — matches if (...null/empty...) return/throw on same line
|
|
48
52
|
(re.compile(r'if\s*\([^)]*(?:==\s*null|!=\s*null|isEmpty\s*\(\)|isBlank\s*\(\))[^)]*\)'
|
|
49
53
|
r'\s*(?:\{?\s*)?(?:return|throw)\b', re.IGNORECASE),
|
|
50
|
-
"condition: null/empty guard with early return"
|
|
54
|
+
"condition: null/empty guard with early return",
|
|
55
|
+
"STRUCTURAL SIGNAL"),
|
|
51
56
|
|
|
52
57
|
# ── Optional execution / branching ────────────────────────────────────────
|
|
53
58
|
(re.compile(r'@Cacheable|@CacheEvict|@CachePut', re.IGNORECASE),
|
|
54
|
-
"branch: Spring cache
|
|
59
|
+
"branch: Spring cache annotation present — downstream call may be short-circuited",
|
|
60
|
+
"STRUCTURAL SIGNAL"),
|
|
55
61
|
(re.compile(r'\.getIfPresent\s*\(|cache\.get\s*\(|cacheManager\.', re.IGNORECASE),
|
|
56
|
-
"branch: manual cache lookup may short-
|
|
62
|
+
"branch: manual cache lookup detected — downstream call may be short-circuited",
|
|
63
|
+
"INFERRED (LOW CONFIDENCE)"),
|
|
57
64
|
(re.compile(r'Optional\s*<|\.orElseThrow\s*\(|\.orElseGet\s*\(|\.orElse\s*\(', re.IGNORECASE),
|
|
58
|
-
"branch: result may be absent
|
|
65
|
+
"branch: Optional type in use — result may be absent",
|
|
66
|
+
"STRUCTURAL SIGNAL"),
|
|
59
67
|
|
|
60
68
|
# ── Async / side effects ──────────────────────────────────────────────────
|
|
61
69
|
(re.compile(r'@Async\b'),
|
|
62
|
-
"async: runs in separate thread
|
|
70
|
+
"async: @Async annotation present — runs in separate thread",
|
|
71
|
+
"STRUCTURAL SIGNAL"),
|
|
63
72
|
(re.compile(r'CompletableFuture|\.supplyAsync\s*\(|\.runAsync\s*\('),
|
|
64
|
-
"async: non-blocking
|
|
73
|
+
"async: CompletableFuture detected — non-blocking execution",
|
|
74
|
+
"STRUCTURAL SIGNAL"),
|
|
65
75
|
(re.compile(r'\basync\s+def\b|\bawait\b', re.IGNORECASE),
|
|
66
|
-
"async: non-blocking
|
|
76
|
+
"async: async/await detected — non-blocking execution",
|
|
77
|
+
"STRUCTURAL SIGNAL"),
|
|
67
78
|
(re.compile(r'publishEvent\s*\(|applicationEventPublisher|eventPublisher\.', re.IGNORECASE),
|
|
68
|
-
"async: Spring application event emitted"
|
|
79
|
+
"async: Spring application event emitted",
|
|
80
|
+
"STRUCTURAL SIGNAL"),
|
|
69
81
|
(re.compile(r'kafkaTemplate\.|KafkaProducer|@KafkaListener', re.IGNORECASE),
|
|
70
|
-
"async: Kafka
|
|
82
|
+
"async: Kafka producer detected",
|
|
83
|
+
"STRUCTURAL SIGNAL"),
|
|
71
84
|
(re.compile(r'rabbitTemplate\.|amqpTemplate\.|@RabbitListener', re.IGNORECASE),
|
|
72
|
-
"async: RabbitMQ
|
|
85
|
+
"async: RabbitMQ producer detected",
|
|
86
|
+
"STRUCTURAL SIGNAL"),
|
|
73
87
|
]
|
|
74
88
|
|
|
75
89
|
|
|
@@ -98,18 +112,19 @@ def _read_safe(root: Path, rel_path: str) -> str:
|
|
|
98
112
|
return ""
|
|
99
113
|
|
|
100
114
|
|
|
101
|
-
def _collect_runtime_notes(content: str, lang: str) -> list[
|
|
115
|
+
def _collect_runtime_notes(content: str, lang: str) -> list[dict]:
|
|
102
116
|
"""Scan comment-stripped content for explicit runtime behavior signals.
|
|
103
117
|
|
|
104
118
|
Returns only notes backed by a direct code pattern match.
|
|
119
|
+
Each entry: {note: str, epistemic_level: str}.
|
|
105
120
|
Returns [] when no signals are found.
|
|
106
121
|
"""
|
|
107
122
|
clean = _strip_comments(content, lang)
|
|
108
|
-
notes: list[
|
|
123
|
+
notes: list[dict] = []
|
|
109
124
|
seen: set[str] = set()
|
|
110
|
-
for pattern, note in _RUNTIME_SIGNALS:
|
|
125
|
+
for pattern, note, epistemic_level in _RUNTIME_SIGNALS:
|
|
111
126
|
if note not in seen and pattern.search(clean):
|
|
112
|
-
notes.append(note)
|
|
127
|
+
notes.append({"note": note, "epistemic_level": epistemic_level})
|
|
113
128
|
seen.add(note)
|
|
114
129
|
return notes
|
|
115
130
|
|
|
@@ -348,8 +363,11 @@ def analyze_execution_paths(
|
|
|
348
363
|
"entry_point": {"step": entry_point_str, "notes": entry_notes},
|
|
349
364
|
"path": path_items,
|
|
350
365
|
"end_state": _detect_end_state([item["step"] for item in path_items]),
|
|
366
|
+
"end_state_epistemic_level": "INFERRED (LOW CONFIDENCE)",
|
|
351
367
|
})
|
|
352
368
|
|
|
369
|
+
return result
|
|
370
|
+
|
|
353
371
|
|
|
354
372
|
# ── Behavioral impact helpers ─────────────────────────────────────────────────
|
|
355
373
|
|
|
@@ -363,9 +381,13 @@ def _domain_from_class(class_name: str) -> str:
|
|
|
363
381
|
return re.sub(r"(?<=[a-z])(?=[A-Z])", " ", stripped).strip().lower()
|
|
364
382
|
|
|
365
383
|
|
|
366
|
-
def _impact_item(statement: str, support: str, certainty: str) -> dict:
|
|
367
|
-
|
|
368
|
-
|
|
384
|
+
def _impact_item(statement: str, support: str, certainty: str, *, epistemic_level: Optional[str] = None) -> dict:
|
|
385
|
+
if epistemic_level is None:
|
|
386
|
+
if certainty in ("high", "medium"):
|
|
387
|
+
epistemic_level = "STRUCTURAL SIGNAL"
|
|
388
|
+
else:
|
|
389
|
+
epistemic_level = "INFERRED (LOW CONFIDENCE)"
|
|
390
|
+
return {"statement": statement, "support": support, "epistemic_level": epistemic_level}
|
|
369
391
|
|
|
370
392
|
|
|
371
393
|
def _impact_descriptions(
|
|
@@ -449,6 +471,7 @@ def _impact_descriptions_for_controller(
|
|
|
449
471
|
"controller entry point modified",
|
|
450
472
|
"controller entry point is in changed files (direct diff evidence)",
|
|
451
473
|
certainty,
|
|
474
|
+
epistemic_level="FACT",
|
|
452
475
|
))
|
|
453
476
|
|
|
454
477
|
if re.search(r"@PreAuthorize|@Secured|@RolesAllowed|hasRole\(|isAuthenticated", ctrl_clean, re.IGNORECASE):
|
|
@@ -541,6 +564,7 @@ def analyze_behavioral_impact(
|
|
|
541
564
|
"affected_path": affected_path,
|
|
542
565
|
"impact": _impact_descriptions_for_controller(affected_path, end_state, ctrl_clean, evidence_level),
|
|
543
566
|
"end_state": end_state,
|
|
567
|
+
"end_state_epistemic_level": "INFERRED (LOW CONFIDENCE)",
|
|
544
568
|
"confidence": confidence,
|
|
545
569
|
"evidence_level": evidence_level,
|
|
546
570
|
"trace": trace,
|
|
@@ -638,6 +662,7 @@ def analyze_behavioral_impact(
|
|
|
638
662
|
"affected_path": affected_path,
|
|
639
663
|
"impact": _impact_descriptions(changed_class, changed_type, end_state, ctrl_clean, evidence_level),
|
|
640
664
|
"end_state": end_state,
|
|
665
|
+
"end_state_epistemic_level": "INFERRED (LOW CONFIDENCE)",
|
|
641
666
|
"confidence": confidence,
|
|
642
667
|
"evidence_level": evidence_level,
|
|
643
668
|
"trace": trace,
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""pr_comment_renderer.py — Renders review-pr output as a GitHub PR comment.
|
|
2
|
+
|
|
3
|
+
Mandatory 5-section format:
|
|
4
|
+
1. PR Change Summary — FACT only
|
|
5
|
+
2. Impacted Execution Flow — STRUCTURAL SIGNAL or FACT only, per-step evidence
|
|
6
|
+
3. Review Priority Order — ranked by evidence-backed impact
|
|
7
|
+
4. Risk / Impact Signals — labeled signals, never "risk" as conclusion
|
|
8
|
+
5. Review Guidance — uncertain items, evidence gaps
|
|
9
|
+
|
|
10
|
+
Contract:
|
|
11
|
+
- Every statement carries an explicit epistemic label.
|
|
12
|
+
- No speculation without label.
|
|
13
|
+
- No hidden confidence blending.
|
|
14
|
+
- Sections omitted when evidence is insufficient.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
_BADGE: dict[str, str] = {
|
|
22
|
+
"FACT": "`FACT`",
|
|
23
|
+
"STRUCTURAL SIGNAL": "`STRUCTURAL SIGNAL`",
|
|
24
|
+
"INFERRED (LOW CONFIDENCE)": "`INFERRED (LOW CONFIDENCE)`",
|
|
25
|
+
"OMITTED": "`OMITTED`",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_STRUCTURAL_EVIDENCE = frozenset({"direct_injection", "direct_call"})
|
|
29
|
+
|
|
30
|
+
_MAX_PRIORITY_FILES = 10
|
|
31
|
+
_MAX_FLOW_PATHS = 3
|
|
32
|
+
_MAX_SIGNALS = 12
|
|
33
|
+
_MAX_UNCERTAIN = 6
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _badge(level: str) -> str:
|
|
37
|
+
return _BADGE.get(level, f"`{level}`")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _short(path: str) -> str:
|
|
41
|
+
parts = Path(path).parts
|
|
42
|
+
return "/".join(parts[-2:]) if len(parts) > 2 else path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _evidence_to_epistemic(evidence_level: str) -> str:
|
|
46
|
+
if evidence_level in _STRUCTURAL_EVIDENCE:
|
|
47
|
+
return "STRUCTURAL SIGNAL"
|
|
48
|
+
if evidence_level == "heuristic_only":
|
|
49
|
+
return "INFERRED (LOW CONFIDENCE)"
|
|
50
|
+
return "OMITTED"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── Section 1: PR Change Summary ──────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
def _section_change_summary(out: dict) -> str:
|
|
56
|
+
runtime: list[dict] = out.get("runtime_changes", [])
|
|
57
|
+
build: dict = out.get("build_changes", {})
|
|
58
|
+
build_files: list[str] = build.get("files", []) if build else []
|
|
59
|
+
base = out.get("base_ref") or out.get("since") or "HEAD~1"
|
|
60
|
+
|
|
61
|
+
committed: list[dict] = out.get("committed_changes", [])
|
|
62
|
+
uncommitted: list[dict] = out.get("uncommitted_changes", [])
|
|
63
|
+
|
|
64
|
+
lines = [
|
|
65
|
+
"### 1. PR Change Summary",
|
|
66
|
+
"",
|
|
67
|
+
f"**Diff:** `git diff {base}...HEAD` | "
|
|
68
|
+
f"**Committed:** {len(committed)} files {_badge('FACT')} | "
|
|
69
|
+
f"**Unstaged:** {len(uncommitted)} files {_badge('FACT')}",
|
|
70
|
+
"",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
if uncommitted:
|
|
74
|
+
lines.append(
|
|
75
|
+
f"> **Note:** {len(uncommitted)} file(s) in working tree not committed — "
|
|
76
|
+
"present in analysis, absent from PR diff."
|
|
77
|
+
)
|
|
78
|
+
lines.append("")
|
|
79
|
+
|
|
80
|
+
if not runtime and not build_files:
|
|
81
|
+
lines.append("*No runtime files changed.*")
|
|
82
|
+
return "\n".join(lines)
|
|
83
|
+
|
|
84
|
+
lines += ["| File | Artifact Type | Role | Source |",
|
|
85
|
+
"|------|--------------|------|--------|"]
|
|
86
|
+
|
|
87
|
+
for rc in runtime:
|
|
88
|
+
path = rc.get("path", "")
|
|
89
|
+
atype = rc.get("artifact_type", "")
|
|
90
|
+
effect = rc.get("change_effect", {})
|
|
91
|
+
stmt = effect.get("statement", "") if isinstance(effect, dict) else ""
|
|
92
|
+
diff_src = rc.get("diff_source", "committed")
|
|
93
|
+
src_label = "committed" if "committed" in diff_src else "unstaged"
|
|
94
|
+
lines.append(f"| `{_short(path)}` | `{atype}` | {stmt} | {src_label} |")
|
|
95
|
+
|
|
96
|
+
for bf in build_files:
|
|
97
|
+
lines.append(f"| `{_short(bf)}` | `build_manifest` | build / dependency configuration | committed |")
|
|
98
|
+
|
|
99
|
+
lines.append("")
|
|
100
|
+
lines.append(f"*All file entries above are {_badge('FACT')} — directly observed in diff.*")
|
|
101
|
+
return "\n".join(lines)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ── Section 2: Impacted Execution Flow ────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
def _section_execution_flow(out: dict) -> str:
|
|
107
|
+
behavioral: list[dict] = out.get("behavioral_impact", [])
|
|
108
|
+
|
|
109
|
+
# Only STRUCTURAL SIGNAL or FACT paths — exclude heuristic_only
|
|
110
|
+
strong = [
|
|
111
|
+
bi for bi in behavioral
|
|
112
|
+
if bi.get("evidence_level", "none") in _STRUCTURAL_EVIDENCE
|
|
113
|
+
]
|
|
114
|
+
weak = [
|
|
115
|
+
bi for bi in behavioral
|
|
116
|
+
if bi.get("evidence_level", "none") == "heuristic_only"
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
lines = ["### 2. Impacted Execution Flow", ""]
|
|
120
|
+
|
|
121
|
+
if not strong and not weak:
|
|
122
|
+
lines.append("*No traceable execution flow — insufficient structural evidence.*")
|
|
123
|
+
return "\n".join(lines)
|
|
124
|
+
|
|
125
|
+
if strong:
|
|
126
|
+
for bi in strong[:_MAX_FLOW_PATHS]:
|
|
127
|
+
ev = bi.get("evidence_level", "")
|
|
128
|
+
ep = _evidence_to_epistemic(ev)
|
|
129
|
+
entry = bi.get("entry_point", "")
|
|
130
|
+
affected: list[str] = bi.get("affected_path", [])
|
|
131
|
+
end_state = bi.get("end_state", "")
|
|
132
|
+
trace: list[str] = bi.get("trace", [])
|
|
133
|
+
|
|
134
|
+
end_state_ep = bi.get("end_state_epistemic_level", "INFERRED (LOW CONFIDENCE)")
|
|
135
|
+
steps = [f"`{entry}`"] + [f"`{s}`" for s in affected]
|
|
136
|
+
if end_state:
|
|
137
|
+
steps.append(f"**{end_state}** {_badge(end_state_ep)}")
|
|
138
|
+
|
|
139
|
+
lines.append(f"**Flow:** {' → '.join(steps)}")
|
|
140
|
+
lines.append(f"**Evidence:** {_badge(ep)}")
|
|
141
|
+
|
|
142
|
+
if trace:
|
|
143
|
+
for t in trace:
|
|
144
|
+
lines.append(f"- {t}")
|
|
145
|
+
lines.append("")
|
|
146
|
+
|
|
147
|
+
if weak:
|
|
148
|
+
lines.append(f"**Excluded from flow** ({len(weak)} path(s) — evidence insufficient):")
|
|
149
|
+
for bi in weak[:3]:
|
|
150
|
+
entry = bi.get("entry_point", "")
|
|
151
|
+
lines.append(
|
|
152
|
+
f"- `{entry}` path — {_badge('INFERRED (LOW CONFIDENCE)')}: "
|
|
153
|
+
"class reference detected but no injection or call evidence"
|
|
154
|
+
)
|
|
155
|
+
lines.append("")
|
|
156
|
+
|
|
157
|
+
return "\n".join(lines).rstrip()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── Section 3: Review Priority Order ─────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
def _section_priority_order(out: dict) -> str:
|
|
163
|
+
order: list[str] = out.get("suggested_review_order", []) or out.get("review_hotspots", [])
|
|
164
|
+
runtime: list[dict] = out.get("runtime_changes", [])
|
|
165
|
+
|
|
166
|
+
cls_map: dict[str, dict] = {rc["path"]: rc for rc in runtime}
|
|
167
|
+
|
|
168
|
+
lines = ["### 3. Review Priority Order", ""]
|
|
169
|
+
|
|
170
|
+
if not order:
|
|
171
|
+
lines.append("*No priority order available — diff scope too narrow to rank.*")
|
|
172
|
+
return "\n".join(lines)
|
|
173
|
+
|
|
174
|
+
for i, path in enumerate(order[:_MAX_PRIORITY_FILES], 1):
|
|
175
|
+
rc = cls_map.get(path, {})
|
|
176
|
+
effect = rc.get("change_effect", {})
|
|
177
|
+
stmt = effect.get("statement", "application source") if isinstance(effect, dict) else "application source"
|
|
178
|
+
ep = effect.get("epistemic_level", "STRUCTURAL SIGNAL") if isinstance(effect, dict) else "STRUCTURAL SIGNAL"
|
|
179
|
+
atype = rc.get("artifact_type", "source")
|
|
180
|
+
lines.append(f"{i}. `{_short(path)}` — {_badge(ep)}: {atype} — {stmt}")
|
|
181
|
+
|
|
182
|
+
return "\n".join(lines)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ── Section 4: Risk / Impact Signals ─────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def _section_signals(out: dict) -> str:
|
|
188
|
+
lines = ["### 4. Risk / Impact Signals", ""]
|
|
189
|
+
signals: list[str] = []
|
|
190
|
+
|
|
191
|
+
# Security files in diff — FACT (files are in diff)
|
|
192
|
+
sec = out.get("security_impact", {})
|
|
193
|
+
if sec:
|
|
194
|
+
files: list[str] = sec.get("affected_resources", [])
|
|
195
|
+
ep = sec.get("epistemic_level", "STRUCTURAL SIGNAL")
|
|
196
|
+
basis = sec.get("basis", "")
|
|
197
|
+
if files:
|
|
198
|
+
names = ", ".join(f"`{_short(f)}`" for f in files)
|
|
199
|
+
signals.append(f"{_badge(ep)}: security-classified files changed — {names}")
|
|
200
|
+
if basis:
|
|
201
|
+
signals.append(f" *Classification basis: {basis}*")
|
|
202
|
+
risk_ep = sec.get("risk_epistemic_level", "INFERRED (LOW CONFIDENCE)")
|
|
203
|
+
signals.append(
|
|
204
|
+
f"{_badge(risk_ep)}: authentication / access-control behavior may be affected — "
|
|
205
|
+
"inspect changed security files to confirm"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Transactional boundary — STRUCTURAL SIGNAL if @Transactional detected, else INFERRED
|
|
209
|
+
txn = out.get("transactional_impact", {})
|
|
210
|
+
if txn:
|
|
211
|
+
files_t: list[str] = txn.get("affected_transactions", [])
|
|
212
|
+
ep_t = txn.get("epistemic_level", "STRUCTURAL SIGNAL")
|
|
213
|
+
basis_t = txn.get("basis", "")
|
|
214
|
+
risk_ep_t = txn.get("risk_epistemic_level", "INFERRED (LOW CONFIDENCE)")
|
|
215
|
+
if files_t:
|
|
216
|
+
names_t = ", ".join(f"`{_short(f)}`" for f in files_t)
|
|
217
|
+
signals.append(f"{_badge(ep_t)}: service/business-logic files changed — {names_t}")
|
|
218
|
+
if basis_t:
|
|
219
|
+
signals.append(f" *Classification basis: {basis_t}*")
|
|
220
|
+
signals.append(
|
|
221
|
+
f"{_badge(risk_ep_t)}: transactional boundary may be affected — "
|
|
222
|
+
"@Transactional scope not confirmed by AST"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Configuration files — FACT
|
|
226
|
+
cfg = out.get("configuration_impact", {})
|
|
227
|
+
if cfg:
|
|
228
|
+
cfg_files: list[str] = cfg.get("changed_configs", [])
|
|
229
|
+
if cfg_files:
|
|
230
|
+
names_c = ", ".join(f"`{_short(f)}`" for f in cfg_files)
|
|
231
|
+
signals.append(f"{_badge('FACT')}: configuration files modified — {names_c}")
|
|
232
|
+
|
|
233
|
+
# Behavioral impact signals — use each item's epistemic_level
|
|
234
|
+
behavioral: list[dict] = out.get("behavioral_impact", [])
|
|
235
|
+
for bi in behavioral[:_MAX_FLOW_PATHS]:
|
|
236
|
+
for item in bi.get("impact", []):
|
|
237
|
+
stmt = item.get("statement", "")
|
|
238
|
+
ep_i = item.get("epistemic_level", "INFERRED (LOW CONFIDENCE)")
|
|
239
|
+
support = item.get("support", "")
|
|
240
|
+
if stmt:
|
|
241
|
+
signals.append(f"{_badge(ep_i)}: {stmt}")
|
|
242
|
+
if support:
|
|
243
|
+
signals.append(f" *{support}*")
|
|
244
|
+
|
|
245
|
+
# Runtime notes from execution paths
|
|
246
|
+
exec_paths: list[dict] = out.get("execution_paths", [])
|
|
247
|
+
seen_notes: set[str] = set()
|
|
248
|
+
for ep_dict in exec_paths:
|
|
249
|
+
entry_notes: list = ep_dict.get("entry_point", {}).get("notes", [])
|
|
250
|
+
path_notes: list = [n for item in ep_dict.get("path", []) for n in item.get("notes", [])]
|
|
251
|
+
for note_obj in entry_notes + path_notes:
|
|
252
|
+
if isinstance(note_obj, dict):
|
|
253
|
+
note = note_obj.get("note", "")
|
|
254
|
+
n_ep = note_obj.get("epistemic_level", "STRUCTURAL SIGNAL")
|
|
255
|
+
if note and note not in seen_notes:
|
|
256
|
+
signals.append(f"{_badge(n_ep)}: {note}")
|
|
257
|
+
seen_notes.add(note)
|
|
258
|
+
|
|
259
|
+
if not signals:
|
|
260
|
+
lines.append("*No impact signals detected — insufficient evidence.*")
|
|
261
|
+
return "\n".join(lines)
|
|
262
|
+
|
|
263
|
+
lines.extend(signals[:_MAX_SIGNALS])
|
|
264
|
+
return "\n".join(lines)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ── Section 5: Review Guidance ────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
def _section_guidance(out: dict) -> str:
|
|
270
|
+
lines = ["### 5. Review Guidance", ""]
|
|
271
|
+
|
|
272
|
+
# Inspect first — top priority files
|
|
273
|
+
order: list[str] = out.get("suggested_review_order", []) or out.get("review_hotspots", [])
|
|
274
|
+
runtime: list[dict] = out.get("runtime_changes", [])
|
|
275
|
+
cls_map: dict[str, dict] = {rc["path"]: rc for rc in runtime}
|
|
276
|
+
|
|
277
|
+
if order:
|
|
278
|
+
lines.append("**Inspect first:**")
|
|
279
|
+
for path in order[:3]:
|
|
280
|
+
rc = cls_map.get(path, {})
|
|
281
|
+
effect = rc.get("change_effect", {})
|
|
282
|
+
stmt = effect.get("statement", "") if isinstance(effect, dict) else ""
|
|
283
|
+
ep = effect.get("epistemic_level", "STRUCTURAL SIGNAL") if isinstance(effect, dict) else "STRUCTURAL SIGNAL"
|
|
284
|
+
label = f" — {stmt}" if stmt else ""
|
|
285
|
+
lines.append(f"- `{_short(path)}`{label} {_badge(ep)}")
|
|
286
|
+
lines.append("")
|
|
287
|
+
|
|
288
|
+
# Uncertain — heuristic-only behavioral paths
|
|
289
|
+
behavioral: list[dict] = out.get("behavioral_impact", [])
|
|
290
|
+
weak_paths = [
|
|
291
|
+
bi for bi in behavioral
|
|
292
|
+
if bi.get("evidence_level", "none") == "heuristic_only"
|
|
293
|
+
]
|
|
294
|
+
if weak_paths:
|
|
295
|
+
lines.append("**Uncertain (heuristic-only — verify manually):**")
|
|
296
|
+
for bi in weak_paths[:_MAX_UNCERTAIN]:
|
|
297
|
+
entry = bi.get("entry_point", "")
|
|
298
|
+
affected: list[str] = bi.get("affected_path", [])
|
|
299
|
+
path_str = " → ".join(affected) if affected else "unknown"
|
|
300
|
+
lines.append(
|
|
301
|
+
f"- `{entry}` → {path_str} "
|
|
302
|
+
f"{_badge('INFERRED (LOW CONFIDENCE)')}: class reference only, no injection/call proof"
|
|
303
|
+
)
|
|
304
|
+
lines.append("")
|
|
305
|
+
|
|
306
|
+
# Evidence gaps
|
|
307
|
+
gaps: list[str] = out.get("gaps", [])
|
|
308
|
+
cov = out.get("test_coverage_risk", {})
|
|
309
|
+
test_files: list[str] = cov.get("changed_files_without_tests", [])
|
|
310
|
+
test_basis = cov.get("basis", "")
|
|
311
|
+
cov_ep = cov.get("epistemic_level", "INFERRED (LOW CONFIDENCE)")
|
|
312
|
+
|
|
313
|
+
gap_items: list[str] = list(gaps)
|
|
314
|
+
if test_files:
|
|
315
|
+
shown = test_files[:5]
|
|
316
|
+
omitted = len(test_files) - len(shown)
|
|
317
|
+
names = ", ".join(f"`{_short(f)}`" for f in shown)
|
|
318
|
+
suffix = f" +{omitted} more" if omitted else ""
|
|
319
|
+
gap_items.append(
|
|
320
|
+
f"changed files without test coverage: {names}{suffix} "
|
|
321
|
+
f"{_badge(cov_ep)}"
|
|
322
|
+
+ (f" — {test_basis}" if test_basis else "")
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if gap_items:
|
|
326
|
+
lines.append("**Evidence gaps / missing coverage:**")
|
|
327
|
+
for g in gap_items:
|
|
328
|
+
lines.append(f"- {g}")
|
|
329
|
+
else:
|
|
330
|
+
lines.append("*No evidence gaps detected.*")
|
|
331
|
+
|
|
332
|
+
return "\n".join(lines)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
def render_github_comment(out: dict) -> str:
|
|
338
|
+
"""Render review-pr TaskOutput dict as a GitHub PR comment (Markdown).
|
|
339
|
+
|
|
340
|
+
5 mandatory sections. Every statement carries an explicit epistemic label.
|
|
341
|
+
"""
|
|
342
|
+
base = out.get("base_ref") or out.get("since") or "HEAD~1"
|
|
343
|
+
header = (
|
|
344
|
+
"## sourcecode PR Analysis\n\n"
|
|
345
|
+
f"**Base:** `{base}` | "
|
|
346
|
+
f"**Review type:** pull request | "
|
|
347
|
+
f"**Epistemic contract:** FACT · STRUCTURAL SIGNAL · INFERRED (LOW CONFIDENCE) · OMITTED"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
sections = [
|
|
351
|
+
header,
|
|
352
|
+
_section_change_summary(out),
|
|
353
|
+
_section_execution_flow(out),
|
|
354
|
+
_section_priority_order(out),
|
|
355
|
+
_section_signals(out),
|
|
356
|
+
_section_guidance(out),
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
footer = (
|
|
360
|
+
"*Generated by [sourcecode](https://github.com/sourcecode-ai/sourcecode). "
|
|
361
|
+
"Labels: "
|
|
362
|
+
"`FACT` = diff/AST evidence · "
|
|
363
|
+
"`STRUCTURAL SIGNAL` = annotation/import/wiring · "
|
|
364
|
+
"`INFERRED (LOW CONFIDENCE)` = heuristic pattern · "
|
|
365
|
+
"`OMITTED` = insufficient evidence.*"
|
|
366
|
+
)
|
|
367
|
+
sections.append(footer)
|
|
368
|
+
|
|
369
|
+
return "\n\n---\n\n".join(s for s in sections if s.strip())
|
sourcecode/prepare_context.py
CHANGED
|
@@ -1001,15 +1001,25 @@ class TaskContextBuilder:
|
|
|
1001
1001
|
if _security_files:
|
|
1002
1002
|
_pr_security_impact = {
|
|
1003
1003
|
"affected_resources": _security_files,
|
|
1004
|
+
"epistemic_level": "STRUCTURAL SIGNAL",
|
|
1005
|
+
"basis": "files classified as security artifact type (name/annotation pattern)",
|
|
1004
1006
|
"risk_level": "high",
|
|
1007
|
+
"risk_epistemic_level": "INFERRED (LOW CONFIDENCE)",
|
|
1005
1008
|
}
|
|
1006
1009
|
if _transaction_files:
|
|
1007
1010
|
_pr_transactional_impact = {
|
|
1008
1011
|
"affected_transactions": _transaction_files,
|
|
1012
|
+
"epistemic_level": "STRUCTURAL SIGNAL",
|
|
1013
|
+
"basis": "files classified as service/business_logic artifact type",
|
|
1009
1014
|
"risk": "possible transaction boundary change",
|
|
1015
|
+
"risk_epistemic_level": "INFERRED (LOW CONFIDENCE)",
|
|
1010
1016
|
}
|
|
1011
1017
|
if _config_files:
|
|
1012
|
-
_pr_configuration_impact = {
|
|
1018
|
+
_pr_configuration_impact = {
|
|
1019
|
+
"changed_configs": _config_files,
|
|
1020
|
+
"epistemic_level": "FACT",
|
|
1021
|
+
"basis": "files present in diff",
|
|
1022
|
+
}
|
|
1013
1023
|
|
|
1014
1024
|
# Test coverage risk scoped to changed source files only
|
|
1015
1025
|
_changed_src = [
|
|
@@ -1026,6 +1036,8 @@ class TaskContextBuilder:
|
|
|
1026
1036
|
_pr_test_coverage_risk = {
|
|
1027
1037
|
"changed_files_without_tests": _untested_changed[:10],
|
|
1028
1038
|
"risk_level": _test_risk_level,
|
|
1039
|
+
"epistemic_level": "INFERRED (LOW CONFIDENCE)",
|
|
1040
|
+
"basis": f"{len(_untested_changed)} changed source files have no matching test file by stem",
|
|
1029
1041
|
}
|
|
1030
1042
|
|
|
1031
1043
|
# Pre-classify changed files once — reused for hotspots, order, and runtime/build split
|
|
@@ -1144,6 +1156,10 @@ class TaskContextBuilder:
|
|
|
1144
1156
|
"change_effect": {
|
|
1145
1157
|
"statement": _ARTIFACT_CHANGE_EFFECT.get(_f_atype, "application source (artifact role requires annotation inspection)"),
|
|
1146
1158
|
"classification_method": _f_cls.get("confidence", "low"),
|
|
1159
|
+
"epistemic_level": (
|
|
1160
|
+
"STRUCTURAL SIGNAL" if _f_cls.get("confidence", "low") == "high"
|
|
1161
|
+
else "INFERRED (LOW CONFIDENCE)"
|
|
1162
|
+
),
|
|
1147
1163
|
},
|
|
1148
1164
|
"evidence_completeness": _impact_entry.get("evidence", {}),
|
|
1149
1165
|
}
|
|
@@ -1287,7 +1303,8 @@ class TaskContextBuilder:
|
|
|
1287
1303
|
relevant_files = sorted(_synonym_boosted, key=lambda rf: -rf.score)
|
|
1288
1304
|
_synonym_note = (
|
|
1289
1305
|
f"Frontend concept detected ({', '.join(_frontend_kws)}). "
|
|
1290
|
-
"
|
|
1306
|
+
"Backend service-layer and interceptor files boosted by symptom keyword match "
|
|
1307
|
+
"[INFERRED (LOW CONFIDENCE) — pattern heuristic, not structural proof]."
|
|
1291
1308
|
)
|
|
1292
1309
|
symptom_note = _synonym_note
|
|
1293
1310
|
|
|
@@ -2706,9 +2723,14 @@ class TaskContextBuilder:
|
|
|
2706
2723
|
# Without reverse dependency graph, we cannot make traceable structural claims.
|
|
2707
2724
|
# Also gated on semantic diff evidence (api_change/security_change) per file.
|
|
2708
2725
|
_has_graph_ev = bool(graph_edges)
|
|
2709
|
-
behavioral_changes: list[
|
|
2726
|
+
behavioral_changes: list[dict] = (
|
|
2710
2727
|
[
|
|
2711
|
-
|
|
2728
|
+
{
|
|
2729
|
+
"statement": f"{Path(_p).name}: {_CHANGE_EFFECT.get(_cls['artifact_type'], 'application source')}",
|
|
2730
|
+
"diff_severity": diff_severities.get(_p),
|
|
2731
|
+
"epistemic_level": "STRUCTURAL SIGNAL",
|
|
2732
|
+
"basis": "diff_severity in (api_change, security_change) with import-graph evidence present",
|
|
2733
|
+
}
|
|
2712
2734
|
for _p, _cls in classifications.items()
|
|
2713
2735
|
if not _cls["is_noise"]
|
|
2714
2736
|
and diff_severities.get(_p, "unknown") in ("api_change", "security_change")
|
|
@@ -2716,26 +2738,52 @@ class TaskContextBuilder:
|
|
|
2716
2738
|
if _has_graph_ev else []
|
|
2717
2739
|
)
|
|
2718
2740
|
|
|
2719
|
-
def _runtime_impact(tc: dict[str, int]) -> list[
|
|
2720
|
-
_ri: list[
|
|
2721
|
-
# Only emit claims that are evidence-backed (annotation-confirmed roles)
|
|
2741
|
+
def _runtime_impact(tc: dict[str, int]) -> list[dict]:
|
|
2742
|
+
_ri: list[dict] = []
|
|
2722
2743
|
if "entrypoint" in tc:
|
|
2723
|
-
_ri.append(
|
|
2744
|
+
_ri.append({
|
|
2745
|
+
"signal": "entrypoint-classified file modified",
|
|
2746
|
+
"epistemic_level": "STRUCTURAL SIGNAL",
|
|
2747
|
+
"basis": "artifact_type=entrypoint confirmed by annotation or naming evidence",
|
|
2748
|
+
})
|
|
2724
2749
|
if "spring_config" in tc:
|
|
2725
|
-
_ri.append(
|
|
2750
|
+
_ri.append({
|
|
2751
|
+
"signal": "Spring @Configuration-classified file modified",
|
|
2752
|
+
"epistemic_level": "STRUCTURAL SIGNAL",
|
|
2753
|
+
"basis": "@Configuration annotation detected in file content",
|
|
2754
|
+
})
|
|
2726
2755
|
if "security" in tc:
|
|
2727
|
-
_ri.append(
|
|
2756
|
+
_ri.append({
|
|
2757
|
+
"signal": "security-classified file modified",
|
|
2758
|
+
"epistemic_level": "STRUCTURAL SIGNAL",
|
|
2759
|
+
"basis": "artifact_type=security confirmed by annotation or naming evidence",
|
|
2760
|
+
})
|
|
2728
2761
|
if "db_migration" in tc:
|
|
2729
|
-
_ri.append(
|
|
2730
|
-
|
|
2762
|
+
_ri.append({
|
|
2763
|
+
"signal": "database schema migration file present in diff",
|
|
2764
|
+
"epistemic_level": "FACT",
|
|
2765
|
+
"basis": "artifact_type=db_migration (file extension/naming convention)",
|
|
2766
|
+
})
|
|
2731
2767
|
_svc = tc.get("service", 0)
|
|
2732
2768
|
if _svc >= 2:
|
|
2733
|
-
_ri.append(
|
|
2769
|
+
_ri.append({
|
|
2770
|
+
"signal": f"{_svc} @Service-annotated file(s) modified",
|
|
2771
|
+
"epistemic_level": "STRUCTURAL SIGNAL",
|
|
2772
|
+
"basis": "@Service annotation detected in file content",
|
|
2773
|
+
})
|
|
2734
2774
|
_repo = tc.get("repository", 0) + tc.get("mapper", 0)
|
|
2735
2775
|
if _repo > 0:
|
|
2736
|
-
_ri.append(
|
|
2776
|
+
_ri.append({
|
|
2777
|
+
"signal": f"{_repo} persistence-classified component(s) modified",
|
|
2778
|
+
"epistemic_level": "STRUCTURAL SIGNAL",
|
|
2779
|
+
"basis": "artifact_type in (repository, mapper) confirmed by annotation evidence",
|
|
2780
|
+
})
|
|
2737
2781
|
if "build_manifest" in tc:
|
|
2738
|
-
_ri.append(
|
|
2782
|
+
_ri.append({
|
|
2783
|
+
"signal": "build manifest modified",
|
|
2784
|
+
"epistemic_level": "FACT",
|
|
2785
|
+
"basis": "artifact_type=build_manifest (pom.xml/build.gradle/package.json naming)",
|
|
2786
|
+
})
|
|
2739
2787
|
return _ri
|
|
2740
2788
|
|
|
2741
2789
|
_max_hop = max((e["hop"] for e in graph_edges), default=0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.30.
|
|
3
|
+
Version: 1.30.16
|
|
4
4
|
Summary: Deterministic codebase context for AI coding agents
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -221,7 +221,7 @@ Description-Content-Type: text/markdown
|
|
|
221
221
|
|
|
222
222
|
**Deterministic, behavior-aware codebase context for AI agents and PR review.**
|
|
223
223
|
|
|
224
|
-

|
|
225
225
|

|
|
226
226
|
|
|
227
227
|
---
|
|
@@ -257,7 +257,7 @@ pipx install sourcecode
|
|
|
257
257
|
|
|
258
258
|
```bash
|
|
259
259
|
sourcecode version
|
|
260
|
-
# sourcecode 1.30.
|
|
260
|
+
# sourcecode 1.30.16
|
|
261
261
|
```
|
|
262
262
|
|
|
263
263
|
---
|
|
@@ -353,6 +353,7 @@ sourcecode prepare-context TASK [PATH] [OPTIONS]
|
|
|
353
353
|
|--------|-------------|
|
|
354
354
|
| `--since REF` | Git ref for `delta` task (e.g. `HEAD~3`, `main`, `v1.2.0`). Required for `delta`; ignored for other tasks. |
|
|
355
355
|
| `--symptom TEXT` | *(fix-bug only)* Keyword hint for the bug — boosts matching files and surfaces related code notes. |
|
|
356
|
+
| `--format TEXT` | Output format: `json` (default) \| `github-comment` (Markdown PR comment, `review-pr` only). |
|
|
356
357
|
| `--llm-prompt` | Append a ready-to-use LLM prompt to the output. |
|
|
357
358
|
| `--dry-run` | Show what would be analyzed without running it. |
|
|
358
359
|
| `--copy` / `-c` | Copy output to clipboard after a successful run. |
|
|
@@ -374,6 +375,9 @@ sourcecode prepare-context delta . --since main
|
|
|
374
375
|
# Onboard with a ready-to-paste LLM prompt
|
|
375
376
|
sourcecode prepare-context onboard --llm-prompt
|
|
376
377
|
|
|
378
|
+
# PR analysis as a GitHub Markdown comment (paste directly into PR)
|
|
379
|
+
sourcecode prepare-context review-pr --since main --format github-comment
|
|
380
|
+
|
|
377
381
|
# List all tasks
|
|
378
382
|
sourcecode prepare-context --task-help
|
|
379
383
|
```
|
|
@@ -444,22 +448,25 @@ sourcecode prepare-context review-pr
|
|
|
444
448
|
"name": "Order",
|
|
445
449
|
"entry_point": {
|
|
446
450
|
"step": "OrderController.createOrder",
|
|
447
|
-
"notes": [
|
|
451
|
+
"notes": [
|
|
452
|
+
{ "note": "condition: authorization check present (@PreAuthorize / @Secured)",
|
|
453
|
+
"epistemic_level": "STRUCTURAL SIGNAL" }
|
|
454
|
+
]
|
|
448
455
|
},
|
|
449
456
|
"path": [
|
|
450
457
|
{
|
|
451
458
|
"step": "ShippingService.process",
|
|
452
459
|
"notes": [
|
|
453
|
-
"branch: Spring cache
|
|
454
|
-
|
|
460
|
+
{ "note": "branch: Spring cache annotation present — downstream call may be short-circuited",
|
|
461
|
+
"epistemic_level": "STRUCTURAL SIGNAL" },
|
|
462
|
+
{ "note": "async: @Async annotation present — runs in separate thread",
|
|
463
|
+
"epistemic_level": "STRUCTURAL SIGNAL" }
|
|
455
464
|
]
|
|
456
465
|
},
|
|
457
|
-
{
|
|
458
|
-
"step": "OrderRepository.save",
|
|
459
|
-
"notes": []
|
|
460
|
-
}
|
|
466
|
+
{ "step": "OrderRepository.save", "notes": [] }
|
|
461
467
|
],
|
|
462
|
-
"end_state": "DB write"
|
|
468
|
+
"end_state": "DB write",
|
|
469
|
+
"end_state_epistemic_level": "INFERRED (LOW CONFIDENCE)"
|
|
463
470
|
}
|
|
464
471
|
]
|
|
465
472
|
}
|
|
@@ -474,17 +481,33 @@ sourcecode prepare-context review-pr
|
|
|
474
481
|
|
|
475
482
|
**Runtime signals detected per step:**
|
|
476
483
|
|
|
477
|
-
| Signal | Example code | Note emitted |
|
|
478
|
-
|
|
479
|
-
| Auth guard | `@PreAuthorize`, `@Secured
|
|
480
|
-
|
|
|
481
|
-
|
|
|
482
|
-
|
|
|
483
|
-
|
|
|
484
|
-
|
|
|
485
|
-
|
|
|
486
|
-
|
|
|
487
|
-
|
|
|
484
|
+
| Signal | Example code | Note emitted | Epistemic level |
|
|
485
|
+
|--------|-------------|--------------|-----------------|
|
|
486
|
+
| Auth guard | `@PreAuthorize`, `@Secured` | `condition: authorization check present (@PreAuthorize / @Secured)` | `STRUCTURAL SIGNAL` |
|
|
487
|
+
| Auth context read | `isAuthenticated()`, `SecurityContextHolder` | `condition: reads authentication context` | `STRUCTURAL SIGNAL` |
|
|
488
|
+
| Feature flag | `featureFlag.isEnabled()`, `FeatureToggle` | `condition: feature flag gates execution` | `INFERRED (LOW CONFIDENCE)` |
|
|
489
|
+
| Null/empty guard | `if (x == null) return` | `condition: null/empty guard with early return` | `STRUCTURAL SIGNAL` |
|
|
490
|
+
| Spring cache | `@Cacheable`, `@CacheEvict` | `branch: Spring cache annotation present — downstream call may be short-circuited` | `STRUCTURAL SIGNAL` |
|
|
491
|
+
| Manual cache | `cache.get()`, `cacheManager.` | `branch: manual cache lookup detected — downstream call may be short-circuited` | `INFERRED (LOW CONFIDENCE)` |
|
|
492
|
+
| Optional absence | `Optional<>`, `.orElseThrow()` | `branch: Optional type in use — result may be absent` | `STRUCTURAL SIGNAL` |
|
|
493
|
+
| Async thread | `@Async` | `async: @Async annotation present — runs in separate thread` | `STRUCTURAL SIGNAL` |
|
|
494
|
+
| CompletableFuture | `CompletableFuture`, `.supplyAsync()` | `async: CompletableFuture detected — non-blocking execution` | `STRUCTURAL SIGNAL` |
|
|
495
|
+
| Event publishing | `publishEvent()`, `applicationEventPublisher` | `async: Spring application event emitted` | `STRUCTURAL SIGNAL` |
|
|
496
|
+
| Kafka | `kafkaTemplate.`, `KafkaProducer` | `async: Kafka producer detected` | `STRUCTURAL SIGNAL` |
|
|
497
|
+
| RabbitMQ | `rabbitTemplate.`, `amqpTemplate.` | `async: RabbitMQ producer detected` | `STRUCTURAL SIGNAL` |
|
|
498
|
+
|
|
499
|
+
**Epistemic contract:**
|
|
500
|
+
|
|
501
|
+
Every output field in `review-pr` carries an explicit `epistemic_level`:
|
|
502
|
+
|
|
503
|
+
| Level | Meaning |
|
|
504
|
+
|-------|---------|
|
|
505
|
+
| `FACT` | Directly observed in diff (file present, config changed) |
|
|
506
|
+
| `STRUCTURAL SIGNAL` | Annotation or type-system evidence in source (`@Service`, `@Transactional`, injection) |
|
|
507
|
+
| `INFERRED (LOW CONFIDENCE)` | Heuristic pattern match — no full structural proof |
|
|
508
|
+
| `OMITTED` | Insufficient evidence — field not emitted |
|
|
509
|
+
|
|
510
|
+
No field blends certainty levels without labeling. `end_state` (e.g. `"DB write"`) is always accompanied by `end_state_epistemic_level: "INFERRED (LOW CONFIDENCE)"` — it is a keyword-match heuristic, not an AST-verified fact.
|
|
488
511
|
|
|
489
512
|
**Other `review-pr` output fields:**
|
|
490
513
|
|
|
@@ -492,9 +515,9 @@ sourcecode prepare-context review-pr
|
|
|
492
515
|
|-------|-------------|
|
|
493
516
|
| `review_hotspots` | Top changed files ranked by impact score |
|
|
494
517
|
| `suggested_review_order` | Security → API → Service → Persistence → Config |
|
|
495
|
-
| `security_impact` | Changed files
|
|
496
|
-
| `transactional_impact` |
|
|
497
|
-
| `test_coverage_risk` | Changed source files with no corresponding test |
|
|
518
|
+
| `security_impact` | Changed security-classified files (`epistemic_level: STRUCTURAL SIGNAL`) + risk note (`INFERRED (LOW CONFIDENCE)`) |
|
|
519
|
+
| `transactional_impact` | Changed service/business-logic files with possible transaction boundary effect |
|
|
520
|
+
| `test_coverage_risk` | Changed source files with no corresponding test (`epistemic_level: INFERRED (LOW CONFIDENCE)`) |
|
|
498
521
|
| `affected_modules` | DDD domain modules touched by the change |
|
|
499
522
|
|
|
500
523
|
---
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=oPVEr2t1nGeRp9mdSXT_sS6JJ-3h2Q78aLDRzcMzVaY,104
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=RTNExwWPXzjgLaRueT7UuxkPj5ZEToWjGbx1j0LSZ9E,10250
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=MyBa0Hf5HmkudZQDLKrjcWDKETXETXl0mQX1swtTwAA,39091
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=XgrZg2DcWcUm9r87cRG3KGO7IK2TIL_N-CvhSbUmmh4,49901
|
|
6
6
|
sourcecode/classifier.py,sha256=pYve2J1LqtYssU3lYLMDz18PT-CjN5c18QYE7R_IG1Q,7507
|
|
7
|
-
sourcecode/cli.py,sha256=
|
|
7
|
+
sourcecode/cli.py,sha256=SQfchQoeVZB7ATLOTL_fimtgqmUORZE6rUG-vSiySJc,85829
|
|
8
8
|
sourcecode/code_notes_analyzer.py,sha256=y1MJBnPZHYp4i6cQCXUb9ATIyifS_qMQWjw_8lPkpsU,9215
|
|
9
9
|
sourcecode/confidence_analyzer.py,sha256=xw_Jv8pAd0wd8t2vvQlorw8Ih0rSF3YCoFS8K-_4aXg,15762
|
|
10
10
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
@@ -17,11 +17,12 @@ sourcecode/doc_analyzer.py,sha256=afA4uJFwXZ_uR2l4J0pQwbeTkRkGmKdN9KhRVYePBUw,24
|
|
|
17
17
|
sourcecode/entrypoint_classifier.py,sha256=gvKgl0f5T8ol1r4JMmkeqGHuZTfZJiOwFOWdc7EYwYw,4061
|
|
18
18
|
sourcecode/env_analyzer.py,sha256=GxCidahAAIptTdDFIlVB6URd4HBnBlIX_SqUov3MBRQ,22076
|
|
19
19
|
sourcecode/file_classifier.py,sha256=48ly5Z6exkzBy8lNy1AkdP4-oJqIA1zT3LZfffuTyDo,11572
|
|
20
|
-
sourcecode/flow_analyzer.py,sha256=
|
|
20
|
+
sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,28781
|
|
21
21
|
sourcecode/git_analyzer.py,sha256=_pCg2V4d2aa17k9hayTzpexAj8syvyk4y9NYNvvgOAI,12802
|
|
22
22
|
sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
|
|
23
23
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
24
|
-
sourcecode/
|
|
24
|
+
sourcecode/pr_comment_renderer.py,sha256=k5pCIP6iBNwy5UYPxu47CAq-62j4E9QZZOPyL3trH80,14799
|
|
25
|
+
sourcecode/prepare_context.py,sha256=pStv1z7bO0_p7YmUTH6NSnc7xjmkNrA9Atdvp6y8UiY,152165
|
|
25
26
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
26
27
|
sourcecode/ranking_engine.py,sha256=virVglafZufioHpZpwktjMvUiL0TZELWQCQnQNV8dFo,9360
|
|
27
28
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
@@ -63,8 +64,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
63
64
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
64
65
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
65
66
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
66
|
-
sourcecode-1.30.
|
|
67
|
-
sourcecode-1.30.
|
|
68
|
-
sourcecode-1.30.
|
|
69
|
-
sourcecode-1.30.
|
|
70
|
-
sourcecode-1.30.
|
|
67
|
+
sourcecode-1.30.16.dist-info/METADATA,sha256=PbY-VNukP8UNwllcv7cy9Zvq7Pxv0TGJgIqZRs3A1fU,28956
|
|
68
|
+
sourcecode-1.30.16.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
69
|
+
sourcecode-1.30.16.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
70
|
+
sourcecode-1.30.16.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
71
|
+
sourcecode-1.30.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|