flash-context 0.3.1__tar.gz → 0.3.2__tar.gz

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.
Files changed (21) hide show
  1. {flash_context-0.3.1 → flash_context-0.3.2}/CHANGELOG.md +22 -0
  2. {flash_context-0.3.1 → flash_context-0.3.2}/PKG-INFO +11 -19
  3. {flash_context-0.3.1 → flash_context-0.3.2}/README.md +10 -18
  4. {flash_context-0.3.1 → flash_context-0.3.2}/pyproject.toml +1 -1
  5. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/agent.py +138 -15
  6. {flash_context-0.3.1 → flash_context-0.3.2}/.env.example +0 -0
  7. {flash_context-0.3.1 → flash_context-0.3.2}/.gitignore +0 -0
  8. {flash_context-0.3.1 → flash_context-0.3.2}/LICENSE +0 -0
  9. {flash_context-0.3.1 → flash_context-0.3.2}/config.yaml.example +0 -0
  10. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/__init__.py +0 -0
  11. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/cli.py +0 -0
  12. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/config.py +0 -0
  13. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/llm.py +0 -0
  14. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/logging.py +0 -0
  15. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/mcp_server.py +0 -0
  16. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/tools/__init__.py +0 -0
  17. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/tools/glob.py +0 -0
  18. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/tools/grep.py +0 -0
  19. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/tools/read.py +0 -0
  20. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/tools/safety.py +0 -0
  21. {flash_context-0.3.1 → flash_context-0.3.2}/src/flash_context/types.py +0 -0
@@ -4,6 +4,28 @@ User-visible changes and migration instructions for each release.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.2] - 2026-11-01
8
+
9
+ ### Fixed
10
+
11
+ - Require whole, case-sensitive query tokens inside exact-symbol citations,
12
+ including read-derived fallback evidence. Reject plausible-home file citations
13
+ without matching source text and retain scoped Python preflight diagnostics.
14
+ - Report `query symbol not found in verified evidence` when symbol lookups finish
15
+ without accepted evidence, rather than relying on generic termination warnings.
16
+
17
+ ### Migration
18
+
19
+ - Exact-symbol queries may now return `NO_CITATIONS_FOUND` with empty citations
20
+ and snippets where previous versions returned unrelated files. Token presence
21
+ is lexical evidence, not proof of a definition or semantic relevance. Public
22
+ schemas, configuration, and CLI exit semantics are unchanged.
23
+
24
+ ### Changed
25
+
26
+ - Condense the README overview and move safety and evidence-validation details
27
+ to the architecture documentation.
28
+
7
29
  ## [0.3.1] - 2026-11-01
8
30
 
9
31
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flash-context
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Read-only context exploration and retrieval for AI agents, starting with codebases and repositories.
5
5
  Project-URL: Repository, https://github.com/zyf0717/flash-context
6
6
  Project-URL: Issues, https://github.com/zyf0717/flash-context/issues
@@ -28,24 +28,16 @@ Description-Content-Type: text/markdown
28
28
  [![Python versions](https://img.shields.io/pypi/pyversions/flash-context)](https://pypi.org/project/flash-context/)
29
29
  [![License](https://img.shields.io/pypi/l/flash-context)](LICENSE)
30
30
 
31
- A lightweight, model-agnostic repository explorer that combines deterministic
32
- lookup with delegated LLM search to deliver compact, validated source evidence.
33
- Available as a Python CLI and Model Context Protocol (MCP) server.
34
-
35
- Delegate recursive, repetitive file search and reading to a utility model
36
- behind an OpenAI-compatible endpoint. The calling agent receives validated
37
- file-line citations and source snippets to use in its main task.
38
-
39
- Codebases and repositories are the current focus; the same tools explore
40
- documentation, configuration, and other local text files. CLI and MCP share one
41
- exploration core with read-only tools, path safety, and resource limits.
42
- Scans and regex execution are bounded; partial results report truncation.
43
- Citation validation checks safe paths and existing line coverage, not semantic
44
- relevance or minimal ranges. Citation precision depends on the explorer model.
45
- Implementation queries continue past documentation-only citations within the turn budget; documentation-only
46
- fallbacks carry incomplete-result metadata and warnings. Build outputs such as
47
- `dist/` remain excluded; inspect a source checkout for implementation evidence.
48
- Optional metadata-only trajectory logs must stay outside the inspected repository.
31
+ A lightweight, model-agnostic repository explorer for AI agents, available as a
32
+ Python CLI and Model Context Protocol (MCP) server. It combines deterministic
33
+ lookup with search and reading delegated to a utility model through an
34
+ OpenAI-compatible endpoint, returning compact, validated file-line citations and
35
+ source snippets.
36
+
37
+ CLI and MCP share a read-only core for exploring code, documentation,
38
+ configuration, and other local text files with path safety and resource limits.
39
+ See [safety and evidence validation](docs/architecture.md#safety-and-evidence-validation)
40
+ for details and limitations.
49
41
 
50
42
  Inspired by [FastContext](https://arxiv.org/abs/2606.14066), a now-withdrawn paper
51
43
  on separating repository exploration from task solving, using specialized models
@@ -5,24 +5,16 @@
5
5
  [![Python versions](https://img.shields.io/pypi/pyversions/flash-context)](https://pypi.org/project/flash-context/)
6
6
  [![License](https://img.shields.io/pypi/l/flash-context)](LICENSE)
7
7
 
8
- A lightweight, model-agnostic repository explorer that combines deterministic
9
- lookup with delegated LLM search to deliver compact, validated source evidence.
10
- Available as a Python CLI and Model Context Protocol (MCP) server.
11
-
12
- Delegate recursive, repetitive file search and reading to a utility model
13
- behind an OpenAI-compatible endpoint. The calling agent receives validated
14
- file-line citations and source snippets to use in its main task.
15
-
16
- Codebases and repositories are the current focus; the same tools explore
17
- documentation, configuration, and other local text files. CLI and MCP share one
18
- exploration core with read-only tools, path safety, and resource limits.
19
- Scans and regex execution are bounded; partial results report truncation.
20
- Citation validation checks safe paths and existing line coverage, not semantic
21
- relevance or minimal ranges. Citation precision depends on the explorer model.
22
- Implementation queries continue past documentation-only citations within the turn budget; documentation-only
23
- fallbacks carry incomplete-result metadata and warnings. Build outputs such as
24
- `dist/` remain excluded; inspect a source checkout for implementation evidence.
25
- Optional metadata-only trajectory logs must stay outside the inspected repository.
8
+ A lightweight, model-agnostic repository explorer for AI agents, available as a
9
+ Python CLI and Model Context Protocol (MCP) server. It combines deterministic
10
+ lookup with search and reading delegated to a utility model through an
11
+ OpenAI-compatible endpoint, returning compact, validated file-line citations and
12
+ source snippets.
13
+
14
+ CLI and MCP share a read-only core for exploring code, documentation,
15
+ configuration, and other local text files with path safety and resource limits.
16
+ See [safety and evidence validation](docs/architecture.md#safety-and-evidence-validation)
17
+ for details and limitations.
26
18
 
27
19
  Inspired by [FastContext](https://arxiv.org/abs/2606.14066), a now-withdrawn paper
28
20
  on separating repository exploration from task solving, using specialized models
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flash-context"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "Read-only context exploration and retrieval for AI agents, starting with codebases and repositories."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -62,12 +62,21 @@ EXACT_TEXT_RE = re.compile(r"[`'\"](?P<target>[^`'\"]{1,120})[`'\"]")
62
62
  TOKEN_RE = re.compile(r"\b(?P<token>[A-Za-z_][A-Za-z0-9_]*)\b")
63
63
 
64
64
 
65
+ @dataclass(frozen=True, slots=True)
66
+ class _DefinitionScan:
67
+ citations: tuple[Citation, ...]
68
+ status: Literal["unique", "missing", "ambiguous", "incomplete"]
69
+ warnings: tuple[str, ...]
70
+
71
+
65
72
  @dataclass(slots=True)
66
73
  class EvidenceState:
67
74
  reads: list[ToolObservation] = field(default_factory=list)
68
75
  citations: list[Citation] = field(default_factory=list)
69
76
  observations: dict[str, ToolObservation] = field(default_factory=dict)
70
77
  prefer_source: bool = False
78
+ symbol: str | None = None
79
+ definition_scan: _DefinitionScan | None = None
71
80
 
72
81
 
73
82
  def explore(
@@ -79,10 +88,15 @@ def explore(
79
88
  request.validate()
80
89
  repo_root = request.repo_root.resolve()
81
90
  validate_trajectory_dir(settings.traj_dir, repo_root)
91
+ evidence = EvidenceState(
92
+ prefer_source=_implementation_query(request.query),
93
+ symbol=_query_symbol(request.query),
94
+ )
82
95
  fast_result = _try_exact_fast_path(
83
96
  request,
84
97
  repo_root=repo_root,
85
98
  settings=settings,
99
+ evidence=evidence,
86
100
  )
87
101
  if fast_result is not None:
88
102
  return fast_result
@@ -99,12 +113,28 @@ def explore(
99
113
  limits=settings.limits,
100
114
  )
101
115
  messages = _initial_messages(request, file_sample, settings)
116
+ if evidence.definition_scan is not None:
117
+ scan = evidence.definition_scan
118
+ messages.append(
119
+ {
120
+ "role": "system",
121
+ "content": (
122
+ f"Local Python .py/.pyi definition scan for {evidence.symbol}: "
123
+ f"{scan.status}. "
124
+ "A complete missing result means zero Python definition matches; "
125
+ "assignments, imports, docs, and other languages may still "
126
+ "contain the symbol. Never infer a plausible home file. "
127
+ "Cited lines must contain the exact case-sensitive whole symbol."
128
+ ),
129
+ }
130
+ )
102
131
  tools = tool_schemas()
103
132
  # A capped file sample is expected; actual scan failures must reach callers.
104
133
  warnings = list(file_sample.warnings)
134
+ if evidence.definition_scan is not None:
135
+ warnings.extend(evidence.definition_scan.warnings)
105
136
  truncated = bool(warnings)
106
137
  last_content = ""
107
- evidence = EvidenceState(prefer_source=_implementation_query(request.query))
108
138
 
109
139
  try:
110
140
  for turn in range(1, effective_max_turns + 1):
@@ -186,6 +216,22 @@ def explore(
186
216
  tool_calls = _extract_tool_calls(message)
187
217
  if not tool_calls:
188
218
  citations = _best_current_citations(evidence)
219
+ if evidence.symbol and content_text == NO_CITATIONS_FOUND:
220
+ citations = []
221
+ elif evidence.symbol and not citations and turn < effective_max_turns:
222
+ messages.append({"role": "assistant", "content": content_text})
223
+ messages.append(
224
+ {
225
+ "role": "user",
226
+ "content": (
227
+ "No verified citation contains the exact query symbol. "
228
+ "Search with fresh read-only calls, or return "
229
+ "NO_CITATIONS_FOUND."
230
+ ),
231
+ }
232
+ )
233
+ last_content = ""
234
+ continue
189
235
  if evidence.prefer_source and _documentation_only(citations):
190
236
  last_content = ""
191
237
  messages.append({"role": "assistant", "content": content_text})
@@ -308,6 +354,8 @@ def explore(
308
354
  last_content,
309
355
  evidence.reads,
310
356
  )
357
+ if evidence.symbol:
358
+ citations = _best_current_citations(evidence)
311
359
  _append_warning_once(warnings, "max turns exceeded after partial answer")
312
360
  if request.citation and not citations:
313
361
  _append_warning_once(
@@ -439,6 +487,9 @@ def _finalize_result(
439
487
  truncated: bool,
440
488
  warnings: list[str],
441
489
  ) -> ExploreResult:
490
+ if _query_symbol(request.query) and not citations:
491
+ answer = NO_CITATIONS_FOUND
492
+ _append_warning_once(warnings, "query symbol not found in verified evidence")
442
493
  citations = _merge_adjacent_citations(citations)[:MAX_FINAL_CITATIONS]
443
494
  if _implementation_query(request.query) and _documentation_only(citations):
444
495
  truncated = True
@@ -580,6 +631,7 @@ def _try_exact_fast_path(
580
631
  *,
581
632
  repo_root: Path,
582
633
  settings: Settings,
634
+ evidence: EvidenceState,
583
635
  ) -> ExploreResult | None:
584
636
  bare_path = request.query.strip()
585
637
  if len(bare_path) >= 2 and bare_path[0] in "`'\"" and bare_path[-1] == bare_path[0]:
@@ -610,7 +662,6 @@ def _try_exact_fast_path(
610
662
  )
611
663
  if not _is_exact_lookup(request.query):
612
664
  return None
613
- evidence = EvidenceState()
614
665
  warnings: list[str] = []
615
666
  citations = _validated_citations_from_content(
616
667
  request.query,
@@ -643,6 +694,13 @@ def _try_exact_fast_path(
643
694
  target=target,
644
695
  allow_assignment=True,
645
696
  )
697
+ citations = _validate_citations(
698
+ citations,
699
+ repo_root=repo_root,
700
+ settings=settings,
701
+ evidence=evidence,
702
+ warnings=warnings,
703
+ )
646
704
  if citations:
647
705
  return _finalize_result(
648
706
  request=request,
@@ -659,25 +717,77 @@ def _try_exact_fast_path(
659
717
  symbol = _extract_pathless_symbol(request.query)
660
718
  if symbol is None:
661
719
  return None
662
- citation = _unique_definition_citation(
720
+ scan = _scan_definitions(
663
721
  repo_root=repo_root,
664
722
  settings=settings,
665
723
  symbol=symbol,
666
724
  )
667
- if citation is None:
725
+ evidence.definition_scan = scan
726
+ if scan.status != "unique":
668
727
  return None
669
728
  return _finalize_result(
670
729
  request=request,
671
730
  repo_root=repo_root,
672
731
  settings=settings,
673
732
  answer="",
674
- citations=[citation],
733
+ citations=list(scan.citations),
675
734
  turns_used=0,
676
735
  truncated=False,
677
736
  warnings=[],
678
737
  )
679
738
 
680
739
 
740
+ def _query_symbol(query: str) -> str | None:
741
+ if not _is_exact_lookup(query) or extract_citations(query):
742
+ return None
743
+ path = _extract_explicit_path(query)
744
+ target = (
745
+ _extract_exact_target(query, path) if path else _extract_pathless_symbol(query)
746
+ )
747
+ if target and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", target):
748
+ return target
749
+ return None
750
+
751
+
752
+ def _symbol_in_range(
753
+ citation: Citation,
754
+ reads: list[ToolObservation],
755
+ symbol: str | None,
756
+ ) -> bool:
757
+ if symbol is None:
758
+ return True
759
+ pattern = re.compile(rf"\b{re.escape(symbol)}\b")
760
+ for read in reads:
761
+ if not _citation_has_read_coverage(citation, [read]) or read.content is None:
762
+ continue
763
+ start, _ = _parse_line_range(read.line_range or "")
764
+ assert start is not None and citation.start_line is not None
765
+ assert citation.end_line is not None
766
+ lines = read.content.split("\n")
767
+ lines = [line + "\n" for line in lines[:-1]] + lines[-1:]
768
+ text = "".join(
769
+ lines[citation.start_line - start : citation.end_line - start + 1]
770
+ )
771
+ for match in pattern.finditer(text):
772
+ # A byte-capped trailing token has no verified right boundary.
773
+ if match.end() < len(text) or not read.truncated or text.endswith("\n"):
774
+ return True
775
+ return False
776
+
777
+
778
+ def _eligible_reads(evidence: EvidenceState) -> list[ToolObservation]:
779
+ if evidence.symbol is None:
780
+ return evidence.reads
781
+ return [
782
+ read
783
+ for read in evidence.reads
784
+ if any(
785
+ _symbol_in_range(citation, [read], evidence.symbol)
786
+ for citation in _citations_from_reads([read])
787
+ )
788
+ ]
789
+
790
+
681
791
  def _is_exact_lookup(query: str) -> bool:
682
792
  # Admit a small lookup vocabulary; any unexplained intent goes to the model.
683
793
  paths = list(EXPLICIT_PATH_RE.finditer(query))
@@ -798,12 +908,12 @@ def _exact_matches_in_file(
798
908
  return citations if 0 < len(citations) <= MAX_FINAL_CITATIONS else []
799
909
 
800
910
 
801
- def _unique_definition_citation(
911
+ def _scan_definitions(
802
912
  *,
803
913
  repo_root: Path,
804
914
  settings: Settings,
805
915
  symbol: str,
806
- ) -> Citation | None:
916
+ ) -> _DefinitionScan:
807
917
  definition_pattern = rf"^\s*(?:async\s+def|def|class)\s+{re.escape(symbol)}\b"
808
918
  grep_result = repo_grep(
809
919
  repo_root=repo_root,
@@ -826,9 +936,16 @@ def _unique_definition_citation(
826
936
  for result in (grep_result, pyi_result)
827
937
  for hit in result.hits
828
938
  ]
829
- if grep_result.truncated or pyi_result.truncated or len(citations) > 1:
830
- return None
831
- return citations[0] if len(citations) == 1 else None
939
+ status: Literal["unique", "missing", "ambiguous", "incomplete"]
940
+ if grep_result.truncated or pyi_result.truncated:
941
+ status = "incomplete"
942
+ elif len(citations) > 1:
943
+ status = "ambiguous"
944
+ else:
945
+ status = "unique" if citations else "missing"
946
+ return _DefinitionScan(
947
+ tuple(citations), status, tuple(grep_result.warnings + pyi_result.warnings)
948
+ )
832
949
 
833
950
 
834
951
  def _grep_exact_target(
@@ -919,7 +1036,8 @@ def _validate_citations(
919
1036
  if normalized is None:
920
1037
  continue
921
1038
  if _citation_has_read_coverage(normalized, evidence.reads):
922
- validated.append(normalized)
1039
+ if _symbol_in_range(normalized, evidence.reads, evidence.symbol):
1040
+ validated.append(normalized)
923
1041
  continue
924
1042
  verified = _verify_citation_by_read(
925
1043
  normalized,
@@ -928,7 +1046,9 @@ def _validate_citations(
928
1046
  evidence=evidence,
929
1047
  warnings=warnings,
930
1048
  )
931
- if verified is not None:
1049
+ if verified is not None and _symbol_in_range(
1050
+ verified, evidence.reads, evidence.symbol
1051
+ ):
932
1052
  validated.append(verified)
933
1053
  return _merge_adjacent_citations(_dedupe_citations(validated))[:MAX_FINAL_CITATIONS]
934
1054
 
@@ -1045,9 +1165,10 @@ def _dedupe_citations(citations: list[Citation]) -> list[Citation]:
1045
1165
 
1046
1166
 
1047
1167
  def _best_current_citations(evidence: EvidenceState) -> list[Citation]:
1168
+ reads = _eligible_reads(evidence)
1048
1169
  if evidence.prefer_source:
1049
1170
  source_reads = [
1050
- read for read in evidence.reads if not _documentation_path(read.path or "")
1171
+ read for read in reads if not _documentation_path(read.path or "")
1051
1172
  ]
1052
1173
  source_citations = [
1053
1174
  citation
@@ -1060,7 +1181,7 @@ def _best_current_citations(evidence: EvidenceState) -> list[Citation]:
1060
1181
  )
1061
1182
  if evidence.citations:
1062
1183
  return evidence.citations[:MAX_FINAL_CITATIONS]
1063
- return _citations_from_reads(evidence.reads)
1184
+ return _citations_from_reads(reads)
1064
1185
 
1065
1186
 
1066
1187
  def _citations_from_reads(observations: list[ToolObservation]) -> list[Citation]:
@@ -1112,7 +1233,7 @@ def _best_narrow_read_citation(
1112
1233
  evidence: EvidenceState,
1113
1234
  settings: Settings,
1114
1235
  ) -> Citation | None:
1115
- for citation in _citations_from_reads(evidence.reads):
1236
+ for citation in _citations_from_reads(_eligible_reads(evidence)):
1116
1237
  for observation in evidence.reads:
1117
1238
  start, end = _parse_line_range(observation.line_range or "")
1118
1239
  if (
@@ -1314,6 +1435,8 @@ def _initial_messages(
1314
1435
  "tool calls in the same turn. "
1315
1436
  "For exact symbol, function, or class names, use repo_grep "
1316
1437
  "before reading broad file ranges. "
1438
+ "Exact-symbol citations must contain the whole case-sensitive "
1439
+ "query token inside the cited lines. "
1317
1440
  "For implementation questions, prefer implementation source over "
1318
1441
  "documentation or Markdown; use documentation when requested or "
1319
1442
  "needed to support the answer. Cite the smallest line ranges "
File without changes
File without changes