codebase-brief 0.1.1__tar.gz → 0.1.3__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.
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3
4
+
5
+ - Find task-matching snippets deeper inside selected source files and show their line ranges instead of always starting at the file's beginning.
6
+ - Keep excerpt reads bounded and apply the existing secret redaction before including source snippets.
7
+
8
+ ## 0.1.2
9
+
10
+ - Add `brief --demo`, which generates a real example brief from temporary sample files without reading or modifying the user's project.
11
+ - Clarify how to try the demo and install the CLI when pipx is not already available.
12
+ - Show the menu's actual `0` exit key correctly in the README.
13
+
3
14
  ## 0.1.1
4
15
 
5
16
  - Clarify the product's best-fit use case as browser-based coding chats that cannot inspect a local working tree.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebase-brief
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Local-first CLI that builds task-focused context briefs from code repositories
5
5
  Author: Mohamed Maggy
6
6
  License-Expression: MIT
@@ -53,7 +53,7 @@ The first image is a terminal-style rendering of the app's actual numbered menu.
53
53
 
54
54
  You need Python 3.10 or newer. Git is optional, but required for recent-history and base-diff details.
55
55
 
56
- The current PyPI release is `0.1.1` and includes the reviewed-file feedback summary and importer-aware `--base` selection. Install it with `pipx`:
56
+ The current PyPI release is `0.1.3` and includes the review-feedback summary, importer-aware `--base` selection, and a sample demo. Install it with `pipx`:
57
57
 
58
58
  ```powershell
59
59
  pipx install codebase-brief
@@ -62,6 +62,10 @@ brief "C:\work\my-app" --task "Trace the login flow" --copy
62
62
 
63
63
  The brief is copied to your clipboard for pasting into your assistant. Replace the example path and task with yours. To use the numbered menu, run `brief` with no arguments.
64
64
 
65
+ Want to see a real example before choosing your project? Run `brief --demo`. It uses a tiny temporary example project and does not read or change your files.
66
+
67
+ If you do not have `pipx` installed, skip the command above and use the virtual-environment steps below instead; they work with the project's minimum Python version. To install `pipx` first, follow its [official installation guide](https://pipx.pypa.io/latest/how-to/install-pipx.html). The current pipx installer requires Python 3.11 or newer, while Codebase Brief itself supports Python 3.10 and newer.
68
+
65
69
  Or use a virtual environment:
66
70
 
67
71
  ```powershell
@@ -79,13 +83,15 @@ If the `brief` command is not available, use `python -m context_inject` while th
79
83
 
80
84
  When the menu opens, it uses the folder you launched it from as the starting project. Type a number and press Enter:
81
85
 
82
- 1. **Build a context brief** — describe the coding task, optionally enter a Git base ref, then accept or change the approximate size limit.
83
- 2. **Choose a different project** — point the tool at another local repository or source folder.
84
- 3. **Help and examples** — see the workflow and an example command.
85
- 0. **Exit** — close the menu.
86
+ - **1 — Build a context brief:** Describe the coding task, optionally enter a Git base ref, then accept or change the approximate size limit.
87
+ - **2 — Choose a different project:** Point the tool at another local repository or source folder.
88
+ - **3 — Help and examples:** See the workflow and an example command.
89
+ - **0 — Exit:** Close the menu.
86
90
 
87
91
  In plain terms, Codebase Brief makes a small guide to a project so an AI coding assistant can get oriented before helping with a task. It does not write or fix code. It reads project information locally and prepares a guide for you to review and share.
88
92
 
93
+ For selected source files, it looks for code near words in your task and labels each excerpt with its source line range. This is a bounded, best-effort search: if it finds no clear match, it falls back to a general excerpt. Check the cited lines in your editor before relying on them.
94
+
89
95
  Before generating the brief, the tool shows suggested files and why they were selected. Press Enter to keep the checked files, type file numbers to toggle selections, or use `add path/to/file.py` to include another safe text file from the repository. Type `none` for an overview without source excerpts. After the brief is shown, saved, or copied, Codebase Brief prints a copyable summary of files kept, removed, added from suggestions, and manually added. To help improve file selection, paste that summary into the [tester feedback form](https://github.com/mohamed1111111m/codebase-brief/issues/new?template=tester-feedback.yml) along with your task and whether the brief helped. Do not include private source code or secrets. The menu's save-here choice writes `context.md` in the folder where you launched the menu; if that is the target repository, the file will be created there. Choose the show or copy option to avoid writing a file into that folder.
90
96
 
91
97
  For your first try, choose a project you know and a task you understand. Check whether the suggested files are the ones you would have shown a coding assistant yourself.
@@ -28,7 +28,7 @@ The first image is a terminal-style rendering of the app's actual numbered menu.
28
28
 
29
29
  You need Python 3.10 or newer. Git is optional, but required for recent-history and base-diff details.
30
30
 
31
- The current PyPI release is `0.1.1` and includes the reviewed-file feedback summary and importer-aware `--base` selection. Install it with `pipx`:
31
+ The current PyPI release is `0.1.3` and includes the review-feedback summary, importer-aware `--base` selection, and a sample demo. Install it with `pipx`:
32
32
 
33
33
  ```powershell
34
34
  pipx install codebase-brief
@@ -37,6 +37,10 @@ brief "C:\work\my-app" --task "Trace the login flow" --copy
37
37
 
38
38
  The brief is copied to your clipboard for pasting into your assistant. Replace the example path and task with yours. To use the numbered menu, run `brief` with no arguments.
39
39
 
40
+ Want to see a real example before choosing your project? Run `brief --demo`. It uses a tiny temporary example project and does not read or change your files.
41
+
42
+ If you do not have `pipx` installed, skip the command above and use the virtual-environment steps below instead; they work with the project's minimum Python version. To install `pipx` first, follow its [official installation guide](https://pipx.pypa.io/latest/how-to/install-pipx.html). The current pipx installer requires Python 3.11 or newer, while Codebase Brief itself supports Python 3.10 and newer.
43
+
40
44
  Or use a virtual environment:
41
45
 
42
46
  ```powershell
@@ -54,13 +58,15 @@ If the `brief` command is not available, use `python -m context_inject` while th
54
58
 
55
59
  When the menu opens, it uses the folder you launched it from as the starting project. Type a number and press Enter:
56
60
 
57
- 1. **Build a context brief** — describe the coding task, optionally enter a Git base ref, then accept or change the approximate size limit.
58
- 2. **Choose a different project** — point the tool at another local repository or source folder.
59
- 3. **Help and examples** — see the workflow and an example command.
60
- 0. **Exit** — close the menu.
61
+ - **1 — Build a context brief:** Describe the coding task, optionally enter a Git base ref, then accept or change the approximate size limit.
62
+ - **2 — Choose a different project:** Point the tool at another local repository or source folder.
63
+ - **3 — Help and examples:** See the workflow and an example command.
64
+ - **0 — Exit:** Close the menu.
61
65
 
62
66
  In plain terms, Codebase Brief makes a small guide to a project so an AI coding assistant can get oriented before helping with a task. It does not write or fix code. It reads project information locally and prepares a guide for you to review and share.
63
67
 
68
+ For selected source files, it looks for code near words in your task and labels each excerpt with its source line range. This is a bounded, best-effort search: if it finds no clear match, it falls back to a general excerpt. Check the cited lines in your editor before relying on them.
69
+
64
70
  Before generating the brief, the tool shows suggested files and why they were selected. Press Enter to keep the checked files, type file numbers to toggle selections, or use `add path/to/file.py` to include another safe text file from the repository. Type `none` for an overview without source excerpts. After the brief is shown, saved, or copied, Codebase Brief prints a copyable summary of files kept, removed, added from suggestions, and manually added. To help improve file selection, paste that summary into the [tester feedback form](https://github.com/mohamed1111111m/codebase-brief/issues/new?template=tester-feedback.yml) along with your task and whether the brief helped. Do not include private source code or secrets. The menu's save-here choice writes `context.md` in the folder where you launched the menu; if that is the target repository, the file will be created there. Choose the show or copy option to avoid writing a file into that folder.
65
71
 
66
72
  For your first try, choose a project you know and a task you understand. Check whether the suggested files are the ones you would have shown a coding assistant yourself.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebase-brief
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Local-first CLI that builds task-focused context briefs from code repositories
5
5
  Author: Mohamed Maggy
6
6
  License-Expression: MIT
@@ -53,7 +53,7 @@ The first image is a terminal-style rendering of the app's actual numbered menu.
53
53
 
54
54
  You need Python 3.10 or newer. Git is optional, but required for recent-history and base-diff details.
55
55
 
56
- The current PyPI release is `0.1.1` and includes the reviewed-file feedback summary and importer-aware `--base` selection. Install it with `pipx`:
56
+ The current PyPI release is `0.1.3` and includes the review-feedback summary, importer-aware `--base` selection, and a sample demo. Install it with `pipx`:
57
57
 
58
58
  ```powershell
59
59
  pipx install codebase-brief
@@ -62,6 +62,10 @@ brief "C:\work\my-app" --task "Trace the login flow" --copy
62
62
 
63
63
  The brief is copied to your clipboard for pasting into your assistant. Replace the example path and task with yours. To use the numbered menu, run `brief` with no arguments.
64
64
 
65
+ Want to see a real example before choosing your project? Run `brief --demo`. It uses a tiny temporary example project and does not read or change your files.
66
+
67
+ If you do not have `pipx` installed, skip the command above and use the virtual-environment steps below instead; they work with the project's minimum Python version. To install `pipx` first, follow its [official installation guide](https://pipx.pypa.io/latest/how-to/install-pipx.html). The current pipx installer requires Python 3.11 or newer, while Codebase Brief itself supports Python 3.10 and newer.
68
+
65
69
  Or use a virtual environment:
66
70
 
67
71
  ```powershell
@@ -79,13 +83,15 @@ If the `brief` command is not available, use `python -m context_inject` while th
79
83
 
80
84
  When the menu opens, it uses the folder you launched it from as the starting project. Type a number and press Enter:
81
85
 
82
- 1. **Build a context brief** — describe the coding task, optionally enter a Git base ref, then accept or change the approximate size limit.
83
- 2. **Choose a different project** — point the tool at another local repository or source folder.
84
- 3. **Help and examples** — see the workflow and an example command.
85
- 0. **Exit** — close the menu.
86
+ - **1 — Build a context brief:** Describe the coding task, optionally enter a Git base ref, then accept or change the approximate size limit.
87
+ - **2 — Choose a different project:** Point the tool at another local repository or source folder.
88
+ - **3 — Help and examples:** See the workflow and an example command.
89
+ - **0 — Exit:** Close the menu.
86
90
 
87
91
  In plain terms, Codebase Brief makes a small guide to a project so an AI coding assistant can get oriented before helping with a task. It does not write or fix code. It reads project information locally and prepares a guide for you to review and share.
88
92
 
93
+ For selected source files, it looks for code near words in your task and labels each excerpt with its source line range. This is a bounded, best-effort search: if it finds no clear match, it falls back to a general excerpt. Check the cited lines in your editor before relying on them.
94
+
89
95
  Before generating the brief, the tool shows suggested files and why they were selected. Press Enter to keep the checked files, type file numbers to toggle selections, or use `add path/to/file.py` to include another safe text file from the repository. Type `none` for an overview without source excerpts. After the brief is shown, saved, or copied, Codebase Brief prints a copyable summary of files kept, removed, added from suggestions, and manually added. To help improve file selection, paste that summary into the [tester feedback form](https://github.com/mohamed1111111m/codebase-brief/issues/new?template=tester-feedback.yml) along with your task and whether the brief helped. Do not include private source code or secrets. The menu's save-here choice writes `context.md` in the folder where you launched the menu; if that is the target repository, the file will be created there. Choose the show or copy option to avoid writing a file into that folder.
90
96
 
91
97
  For your first try, choose a project you know and a task you understand. Check whether the suggested files are the ones you would have shown a coding assistant yourself.
@@ -11,6 +11,7 @@ import re
11
11
  import shutil
12
12
  import subprocess
13
13
  import sys
14
+ import tempfile
14
15
  from pathlib import Path
15
16
 
16
17
  CHARS_PER_TOKEN = 4 # rough estimate; real token counts vary by model and text
@@ -23,7 +24,7 @@ MAX_KEY_FILES = 12
23
24
  MAX_SELECTION_CHOICES = 30
24
25
  MAX_DISCOVERED_FILES = 20_000
25
26
  IMPORT_SUFFIXES = {".py", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"}
26
- VERSION = "0.1.1"
27
+ VERSION = "0.1.3"
27
28
  MANIFESTS = (
28
29
  "package.json", "pyproject.toml", "requirements.txt", "Cargo.toml",
29
30
  "go.mod", "Gemfile", "composer.json", "pom.xml",
@@ -202,6 +203,103 @@ def task_words(task: str) -> set[str]:
202
203
  return {word.lower() for word in re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", task)}
203
204
 
204
205
 
206
+ _TASK_STOP_WORDS = {
207
+ "about", "after", "also", "and", "are", "does", "from", "give", "help",
208
+ "how", "into", "its", "make", "show", "that", "the", "this", "what", "with",
209
+ }
210
+
211
+
212
+ def read_task_excerpt(
213
+ root: Path, relative: str, task: str, limit: int = 1050,
214
+ max_bytes: int = 96_000,
215
+ ) -> str | None:
216
+ """Find compact, task-related source windows, including later definitions.
217
+
218
+ File ranking alone is not enough: returning only the first lines of a large
219
+ file can omit the function that made it relevant. Reads remain bounded and
220
+ pass through the same path and secret safeguards as ordinary excerpts.
221
+ """
222
+ path = safe_path(root, relative)
223
+ if path is None:
224
+ return None
225
+ try:
226
+ with path.open("rb") as source:
227
+ raw = source.read(max_bytes + 1)
228
+ except OSError:
229
+ return None
230
+ if b"\0" in raw[:4096]:
231
+ return None
232
+
233
+ truncated = len(raw) > max_bytes
234
+ text = raw[:max_bytes].decode("utf-8", errors="replace")
235
+ all_lines = text.splitlines()
236
+ if not all_lines:
237
+ return None
238
+ lines = [line[:500] for line in all_lines[:1500]]
239
+ truncated = truncated or len(all_lines) > len(lines) or any(len(line) > 500 for line in all_lines[:1500])
240
+ terms = {term for term in task_words(task) if term not in _TASK_STOP_WORDS}
241
+ # Split snake_case and hyphenated identifiers so "demo" can match
242
+ # generate_demo_brief without requiring an exact function-name query.
243
+ tokenized_lines = [
244
+ {part.lower() for part in re.findall(r"[A-Za-z][A-Za-z0-9]*", line)}
245
+ for line in lines
246
+ ]
247
+ frequencies = {
248
+ term: sum(term in line_terms for line_terms in tokenized_lines)
249
+ for term in terms
250
+ }
251
+ scores: list[float] = []
252
+ for line_terms in tokenized_lines:
253
+ scores.append(sum(
254
+ 1 + math.log((len(lines) + 1) / (frequencies[term] + 1))
255
+ for term in terms if term in line_terms
256
+ ))
257
+ for index, line in enumerate(lines):
258
+ if scores[index] and re.match(r"\s*(?:async\s+)?(?:def|class)\s+", line):
259
+ # In source files, a matching function/class declaration is a
260
+ # stronger anchor than a help string that merely mentions the task.
261
+ scores[index] += 8
262
+
263
+ if not any(scores):
264
+ return compact(redact("\n".join(lines)), limit)
265
+
266
+ # Make short windows around the strongest matching lines. Merge overlaps,
267
+ # then take the best windows until the excerpt budget is used.
268
+ candidates = sorted(range(len(lines)), key=lambda index: (-scores[index], index))
269
+ windows: list[tuple[int, int]] = []
270
+ for index in candidates:
271
+ if scores[index] == 0:
272
+ break
273
+ start, end = max(0, index - 4), min(len(lines), index + 11)
274
+ if any(start <= old_end and end >= old_start for old_start, old_end in windows):
275
+ continue
276
+ windows.append((start, end))
277
+ selected: list[tuple[int, int, str]] = []
278
+ used = 0
279
+ for start, end in windows:
280
+ body = "\n".join(lines[start:end]).strip()
281
+ snippet = f"Lines {start + 1}–{end}:\n{body}"
282
+ if not body:
283
+ continue
284
+ remaining = limit - used
285
+ if remaining <= 0:
286
+ break
287
+ if len(snippet) > remaining:
288
+ break
289
+ selected.append((start, end, snippet))
290
+ used += len(snippet) + 2
291
+ if not selected:
292
+ return compact("\n".join(lines), limit)
293
+
294
+ selected.sort(key=lambda item: item[0])
295
+ result = "\n…\n".join(snippet for _start, _end, snippet in selected)
296
+ if truncated:
297
+ result += "\nMore of this file was left out to keep the brief short."
298
+ elif len(selected) < len(windows):
299
+ result += "\nOther matching sections were left out to keep the brief short."
300
+ return redact(result)
301
+
302
+
205
303
  def discover_files(root: Path) -> list[str]:
206
304
  found: list[str] = []
207
305
  visited_dirs = 0
@@ -815,12 +913,14 @@ def gather(
815
913
  if label and label != "additional project file":
816
914
  key.append(f"{relative} is already included above; it was also {label}.")
817
915
  continue
818
- excerpt = read_excerpt(root, relative)
916
+ excerpt = read_task_excerpt(root, relative, task)
917
+ if excerpt is None:
918
+ fallback = read_excerpt(root, relative)
919
+ excerpt = fallback[0] if fallback else None
819
920
  if excerpt:
820
- content, truncated = excerpt
821
- content = compact(content, 1050)
921
+ content = compact(excerpt, 1050)
822
922
  label = reasons.get(relative, "selected by project signals")
823
- shortened_note = "" if "Some lines were left out" in content else ("\nMore of this file was left out to keep the brief short." if truncated else "")
923
+ shortened_note = "" if "Some lines were left out" in content or "More of this file was left out" in content else ""
824
924
  key.append(f"{relative} — {label}\n{excerpt_for_brief(relative, content)}" + shortened_note)
825
925
  if not key:
826
926
  if selected_files is not None and not key_files:
@@ -943,6 +1043,46 @@ def terminal_supports_color() -> bool:
943
1043
  return False
944
1044
 
945
1045
 
1046
+ DEMO_TASK = "Trace how a sign-in request is checked"
1047
+ DEMO_FILES = {
1048
+ "README.md": (
1049
+ "# Sample Shop\n\n"
1050
+ "A tiny example storefront. Its sign-in route checks a password through a small auth helper.\n"
1051
+ ),
1052
+ "pyproject.toml": (
1053
+ "[project]\nname = 'sample-shop'\nversion = '0.1.0'\n"
1054
+ "requires-python = '>=3.10'\ndependencies = []\n"
1055
+ ),
1056
+ "src/sample_shop/auth.py": (
1057
+ "def check_password(saved_password: str, supplied_password: str) -> bool:\n"
1058
+ " return saved_password == supplied_password\n"
1059
+ ),
1060
+ "src/sample_shop/routes.py": (
1061
+ "from .auth import check_password\n\n"
1062
+ "def sign_in(saved_password: str, supplied_password: str) -> str:\n"
1063
+ " if check_password(saved_password, supplied_password):\n"
1064
+ " return 'Welcome back'\n"
1065
+ " return 'Password did not match'\n"
1066
+ ),
1067
+ "tests/test_auth.py": (
1068
+ "from sample_shop.auth import check_password\n\n"
1069
+ "def test_matching_password_is_accepted():\n"
1070
+ " assert check_password('correct horse', 'correct horse')\n"
1071
+ ),
1072
+ }
1073
+
1074
+
1075
+ def generate_demo_brief() -> str:
1076
+ """Generate a real sample brief without reading or changing the user's project."""
1077
+ with tempfile.TemporaryDirectory(prefix="codebase-brief-demo-") as temporary:
1078
+ root = Path(temporary)
1079
+ for relative, content in DEMO_FILES.items():
1080
+ path = root / relative
1081
+ path.parent.mkdir(parents=True, exist_ok=True)
1082
+ path.write_text(content, encoding="utf-8")
1083
+ return gather(root, DEMO_TASK, budget_tokens=1_200)
1084
+
1085
+
946
1086
  def interactive() -> int:
947
1087
  """Run the guided, numbered terminal interface."""
948
1088
  repo = Path.cwd().resolve()
@@ -1036,6 +1176,7 @@ def interactive() -> int:
1036
1176
  print("\nA base ref only helps when your current Git commit differs from that ref. Matching covers static imports, literal dynamic imports, and simple TS/JS aliases; runtime-built names may be missed.")
1037
1177
  print(f"The default guide size is about {DEFAULT_BUDGET_TOKENS:,} tokens ({CHARS_PER_TOKEN} characters per token, roughly).")
1038
1178
  print("Save creates a Markdown file; copy puts the guide on your clipboard only when you choose it.")
1179
+ print("Run 'brief --demo' to see a sample brief without using your project.")
1039
1180
  print('\nExample: codebase-brief "C:\\work\\my-app" --task "Find login handling" --base main --budget 2400 --copy')
1040
1181
  input("\nPress Enter to return to the menu...")
1041
1182
  continue
@@ -1135,7 +1276,14 @@ def main(argv: list[str] | None = None) -> int:
1135
1276
  parser.add_argument("--output", help="Write Markdown to this path (default: stdout)")
1136
1277
  parser.add_argument("--copy", action="store_true", help="Copy the generated Markdown to your system clipboard")
1137
1278
  parser.add_argument("-i", "--interactive", action="store_true", help="Open the numbered interactive menu")
1279
+ parser.add_argument("--demo", action="store_true", help="Print a real sample brief without reading your project")
1138
1280
  args = parser.parse_args(argv)
1281
+ if args.demo:
1282
+ if args.interactive or args.repo_path or args.task or args.base or args.output or args.copy or args.budget != DEFAULT_BUDGET_TOKENS:
1283
+ parser.error("--demo cannot be combined with repo_path, --task, --base, --budget, --output, --copy, or --interactive")
1284
+ print("Preview from a built-in sample project. Your project folder was not read or changed.\n")
1285
+ sys.stdout.write(generate_demo_brief())
1286
+ return 0
1139
1287
  if args.interactive:
1140
1288
  if args.repo_path or args.task or args.base or args.output or args.copy or args.budget != DEFAULT_BUDGET_TOKENS:
1141
1289
  parser.error("--interactive cannot be combined with repo_path, --task, --base, --budget, --output, or --copy")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codebase-brief"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "Local-first CLI that builds task-focused context briefs from code repositories"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -6,7 +6,7 @@ import unittest
6
6
  from pathlib import Path
7
7
  from unittest.mock import patch
8
8
 
9
- from context_inject.cli import DEFAULT_BUDGET_TOKENS, MAX_CHARS, MAX_FILE_BYTES, MAX_KEY_FILES, choose_files, compact, copy_to_clipboard, gather, interactive, local_imports, main, read_excerpt, redact, review_feedback, safe_path, suggest_file_details, suggest_files
9
+ from context_inject.cli import DEFAULT_BUDGET_TOKENS, MAX_CHARS, MAX_FILE_BYTES, MAX_KEY_FILES, choose_files, compact, copy_to_clipboard, gather, interactive, local_imports, main, read_excerpt, read_task_excerpt, redact, review_feedback, safe_path, suggest_file_details, suggest_files
10
10
 
11
11
 
12
12
  class ContextInjectTests(unittest.TestCase):
@@ -75,6 +75,21 @@ class ContextInjectTests(unittest.TestCase):
75
75
  self.assertLess(output.index("Files that may help"), output.index("Technology and commands"))
76
76
  self.assertLessEqual(len(output), 1000 * 4)
77
77
 
78
+ def test_task_excerpt_finds_relevant_function_beyond_file_prefix(self):
79
+ filler = "\n".join(f"def unrelated_{index}(): return {index}" for index in range(180))
80
+ source = filler + "\n\ndef build_demo_brief():\n return gather(sample_files, task)\n"
81
+ self.put("src/app.py", source)
82
+
83
+ excerpt = read_task_excerpt(
84
+ self.root, "src/app.py",
85
+ "Explain how the built-in demo creates a sample context brief",
86
+ )
87
+
88
+ self.assertIsNotNone(excerpt)
89
+ self.assertIn("def build_demo_brief", excerpt)
90
+ self.assertIn("return gather(sample_files, task)", excerpt)
91
+ self.assertNotIn("def unrelated_0", excerpt)
92
+
78
93
  def test_excerpt_shortening_stops_on_a_word_boundary(self):
79
94
  excerpt = compact("This is a long sentence that should end on a complete word. " * 3, 90)
80
95
  self.assertTrue(excerpt.startswith("This is a long sentence that should\nSome lines were left out"))
@@ -222,6 +237,26 @@ class ContextInjectTests(unittest.TestCase):
222
237
  self.assertIn("First use:", stdout.getvalue())
223
238
  self.assertIn("--budget", stdout.getvalue())
224
239
  self.assertIn("--copy", stdout.getvalue())
240
+ self.assertIn("--demo", stdout.getvalue())
241
+
242
+ def test_demo_generates_real_brief_without_reading_or_changing_current_folder(self):
243
+ output = io.StringIO()
244
+ before = set(self.root.rglob("*"))
245
+ with patch("pathlib.Path.cwd", return_value=self.root), contextlib.redirect_stdout(output):
246
+ self.assertEqual(main(["--demo"]), 0)
247
+ brief = output.getvalue()
248
+ self.assertIn("Trace how a sign-in request is checked", brief)
249
+ self.assertIn("src/sample_shop/auth.py", brief)
250
+ self.assertIn("src/sample_shop/routes.py", brief)
251
+ self.assertIn("check_password", brief)
252
+ self.assertEqual(set(self.root.rglob("*")), before)
253
+
254
+ def test_demo_rejects_combined_project_arguments(self):
255
+ stderr = io.StringIO()
256
+ with contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit) as error:
257
+ main([str(self.root), "--demo"])
258
+ self.assertEqual(error.exception.code, 2)
259
+ self.assertIn("--demo cannot be combined", stderr.getvalue())
225
260
 
226
261
  def test_cli_copy_generates_a_brief_and_calls_clipboard(self):
227
262
  self.put("README.md", "# Clipboard fixture")
@@ -466,7 +501,7 @@ class ContextInjectTests(unittest.TestCase):
466
501
  plain = __import__("re").sub(r"\x1b\[[0-9;]*m", "", output.getvalue())
467
502
  self.assertIn("CODEBASE", plain)
468
503
  self.assertIn("BRIEF", plain)
469
- self.assertIn("v0.1.1", plain)
504
+ self.assertIn("v0.1.3", plain)
470
505
  self.assertIn("Meet Briefy - your repo guide", plain)
471
506
  self.assertIn("WHAT THIS DOES", plain)
472
507
  self.assertIn("Makes a short guide to your project for an AI coding assistant", plain)
File without changes
File without changes