cli-usereq 0.68.0__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.
Files changed (37) hide show
  1. cli_usereq-0.68.0.dist-info/METADATA +1134 -0
  2. cli_usereq-0.68.0.dist-info/RECORD +37 -0
  3. cli_usereq-0.68.0.dist-info/WHEEL +5 -0
  4. cli_usereq-0.68.0.dist-info/entry_points.txt +4 -0
  5. cli_usereq-0.68.0.dist-info/licenses/LICENSE +674 -0
  6. cli_usereq-0.68.0.dist-info/top_level.txt +1 -0
  7. usereq/__init__.py +62 -0
  8. usereq/__main__.py +17 -0
  9. usereq/cli.py +4963 -0
  10. usereq/compress.py +393 -0
  11. usereq/compress_files.py +134 -0
  12. usereq/doxygen_parser.py +172 -0
  13. usereq/find_constructs.py +394 -0
  14. usereq/generate_markdown.py +158 -0
  15. usereq/resources/docs/Document_Source_Code_in_Doxygen_Style.md +130 -0
  16. usereq/resources/docs/HDT_Test_Authoring_Guide.md +318 -0
  17. usereq/resources/docs/Requirements_Template.md +78 -0
  18. usereq/resources/prompts/analyze.md +191 -0
  19. usereq/resources/prompts/change.md +279 -0
  20. usereq/resources/prompts/check.md +190 -0
  21. usereq/resources/prompts/cover.md +271 -0
  22. usereq/resources/prompts/create.md +134 -0
  23. usereq/resources/prompts/fix.md +273 -0
  24. usereq/resources/prompts/flowchart.md +272 -0
  25. usereq/resources/prompts/implement.md +194 -0
  26. usereq/resources/prompts/new.md +278 -0
  27. usereq/resources/prompts/readme.md +234 -0
  28. usereq/resources/prompts/recreate.md +265 -0
  29. usereq/resources/prompts/refactor.md +265 -0
  30. usereq/resources/prompts/references.md +131 -0
  31. usereq/resources/prompts/renumber.md +150 -0
  32. usereq/resources/prompts/workflow.md +254 -0
  33. usereq/resources/prompts/write.md +129 -0
  34. usereq/resources/prompts-version.txt +1 -0
  35. usereq/source_analyzer.py +2280 -0
  36. usereq/static_check.py +812 -0
  37. usereq/token_counter.py +123 -0
usereq/compress.py ADDED
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env python3
2
+ """!
3
+ @file compress.py
4
+ @brief Source code compressor for LLM context optimization.
5
+ @details Parses a source file and removes all comments (inline, single-line, multi-line), blank lines, trailing whitespace, and redundant spacing while preserving language semantics (e.g. Python indentation). Leverages LanguageSpec from source_analyzer to correctly identify comment syntax for each supported language.
6
+ @author GitHub Copilot
7
+ @version 0.0.70
8
+ """
9
+
10
+ import os
11
+ import sys
12
+
13
+ from .source_analyzer import build_language_specs
14
+
15
+ # Extension-to-language map (mirrors generate_markdown.py)
16
+ EXT_LANG_MAP = {
17
+ ".py": "python", ".js": "javascript", ".mjs": "javascript", ".ts": "typescript",
18
+ ".rs": "rust", ".go": "go", ".c": "c", ".cpp": "cpp",
19
+ ".h": "c", ".hpp": "cpp", ".java": "java", ".rb": "ruby",
20
+ ".php": "php", ".swift": "swift", ".kt": "kotlin",
21
+ ".scala": "scala", ".lua": "lua", ".sh": "shell",
22
+ ".pl": "perl", ".hs": "haskell", ".zig": "zig",
23
+ ".ex": "elixir", ".cs": "csharp",
24
+ }
25
+ """! @brief Extension-to-language normalization map for compression input."""
26
+
27
+ # Languages where indentation is semantically significant
28
+ INDENT_SIGNIFICANT = {"python", "haskell", "elixir"}
29
+ """! @brief Languages requiring indentation-preserving compression behavior."""
30
+
31
+ _specs_cache = None
32
+ """! @brief Cached language specification dictionary initialized lazily."""
33
+
34
+
35
+ def _get_specs():
36
+ """! @brief Return cached language specifications, initializing once.
37
+ @return Dictionary mapping normalized language keys to language specs.
38
+ @details If cache is empty, calls `build_language_specs()` to populate it.
39
+ """
40
+ global _specs_cache
41
+ if _specs_cache is None:
42
+ _specs_cache = build_language_specs()
43
+ return _specs_cache
44
+
45
+
46
+ def detect_language(filepath: str) -> str | None:
47
+ """! @brief Detect language key from file extension.
48
+ @param filepath Source file path.
49
+ @return Normalized language key, or None when extension is unsupported.
50
+ @details Uses `EXT_LANG_MAP` for lookup. Case-insensitive extension matching.
51
+ """
52
+ _, ext = os.path.splitext(filepath)
53
+ return EXT_LANG_MAP.get(ext.lower())
54
+
55
+
56
+ def _is_in_string(line: str, pos: int, string_delimiters: tuple) -> bool:
57
+ """! @brief Check if position `pos` in `line` is inside a string literal.
58
+ @param line The code line string.
59
+ @param pos The character index to check.
60
+ @param string_delimiters Tuple of string delimiter characters/sequences.
61
+ @return True if `pos` is inside a string, False otherwise.
62
+ @details iterates through the line handling escaped delimiters.
63
+ """
64
+ in_string = None
65
+ i = 0
66
+ sorted_delims = sorted(string_delimiters, key=len, reverse=True)
67
+ while i < pos:
68
+ if in_string is not None:
69
+ if line[i:].startswith(in_string):
70
+ # Check for escaped delimiter (single-char only)
71
+ if len(in_string) == 1 and i > 0 and line[i - 1] == '\\':
72
+ # Count consecutive backslashes
73
+ bs = 0
74
+ j = i - 1
75
+ while j >= 0 and line[j] == '\\':
76
+ bs += 1
77
+ j -= 1
78
+ if bs % 2 == 1:
79
+ i += 1
80
+ continue
81
+ i += len(in_string)
82
+ in_string = None
83
+ continue
84
+ i += 1
85
+ else:
86
+ matched = False
87
+ for delim in sorted_delims:
88
+ if line[i:].startswith(delim):
89
+ in_string = delim
90
+ i += len(delim)
91
+ matched = True
92
+ break
93
+ if not matched:
94
+ i += 1
95
+ return in_string is not None
96
+
97
+
98
+ def _remove_inline_comment(line: str, single_comment: str,
99
+ string_delimiters: tuple) -> str:
100
+ """! @brief Remove trailing single-line comment from a code line.
101
+ @param line The code line string.
102
+ @param single_comment The single-line comment marker (e.g., "//", "#").
103
+ @param string_delimiters Tuple of string delimiters to respect.
104
+ @return The line content before the comment starts.
105
+ @details Respects string literals; does not remove comments inside strings.
106
+ """
107
+ if not single_comment:
108
+ return line
109
+ sc_len = len(single_comment)
110
+ i = 0
111
+ sorted_delims = sorted(string_delimiters, key=len, reverse=True)
112
+ in_string = None
113
+ while i < len(line):
114
+ if in_string is not None:
115
+ if line[i:].startswith(in_string):
116
+ if len(in_string) == 1 and i > 0 and line[i - 1] == '\\':
117
+ bs = 0
118
+ j = i - 1
119
+ while j >= 0 and line[j] == '\\':
120
+ bs += 1
121
+ j -= 1
122
+ if bs % 2 == 1:
123
+ i += 1
124
+ continue
125
+ i += len(in_string)
126
+ in_string = None
127
+ continue
128
+ i += 1
129
+ else:
130
+ if line[i:i + sc_len] == single_comment:
131
+ return line[:i]
132
+ for delim in sorted_delims:
133
+ if line[i:].startswith(delim):
134
+ in_string = delim
135
+ i += len(delim)
136
+ break
137
+ else:
138
+ i += 1
139
+ return line
140
+
141
+
142
+ def _is_python_docstring_line(line: str) -> bool:
143
+ """!
144
+ @brief Check if a line is a standalone Python docstring (triple-quote only).
145
+ @param line The code line string.
146
+ @return True if the line appears to be a standalone triple-quoted string.
147
+ @details Implements the _is_python_docstring_line function behavior with deterministic control flow.
148
+ """
149
+ stripped = line.strip()
150
+ for q in ('"""', "'''"):
151
+ if stripped.startswith(q) and stripped.endswith(q) and len(stripped) >= 6:
152
+ return True
153
+ return False
154
+
155
+
156
+ def _format_result(entries: list[tuple[int, str]],
157
+ include_line_numbers: bool) -> str:
158
+ """!
159
+ @brief Format compressed entries, optionally prefixing original line numbers.
160
+ @param entries List of tuples (line_number, text).
161
+ @param include_line_numbers Boolean flag to enable line prefixes.
162
+ @return Formatted string.
163
+ @details Implements the _format_result function behavior with deterministic control flow.
164
+ """
165
+ if not include_line_numbers:
166
+ return '\n'.join(text for _, text in entries)
167
+ return '\n'.join(f"{lineno}: {text}" for lineno, text in entries)
168
+
169
+
170
+ def compress_source(source: str, language: str,
171
+ include_line_numbers: bool = True) -> str:
172
+ """! @brief Compress source code by removing comments, blank lines, and extra whitespace.
173
+ @param source The source code string.
174
+ @param language Language identifier (e.g. "python", "javascript").
175
+ @param include_line_numbers If True (default), prefix each line with <n>: format.
176
+ @return Compressed source code string.
177
+ @throws ValueError If language is unsupported.
178
+ @details Preserves indentation for indent-significant languages (Python, Haskell, Elixir).
179
+ """
180
+ specs = _get_specs()
181
+ lang_key = language.lower().strip().lstrip(".")
182
+ if lang_key not in specs:
183
+ raise ValueError(f"Unsupported language: {language}")
184
+
185
+ spec = specs[lang_key]
186
+ preserve_indent = lang_key in INDENT_SIGNIFICANT
187
+ lines = source.split('\n')
188
+ result = [] # list of (original_line_number, text)
189
+
190
+ in_multi_comment = False
191
+ mc_end = spec.multi_comment_end
192
+ mc_start = spec.multi_comment_start
193
+ string_delims = spec.string_delimiters
194
+
195
+ # Python: also handle ''' as multi-comment
196
+ is_python = lang_key == "python"
197
+ in_python_docstring = False
198
+ python_docstring_delim = None
199
+
200
+ i = 0
201
+ while i < len(lines):
202
+ line = lines[i]
203
+
204
+ # --- Handle multi-line comment continuation ---
205
+ if in_multi_comment:
206
+ if mc_end and mc_end in line:
207
+ # End of multi-line comment found
208
+ end_pos = line.index(mc_end) + len(mc_end)
209
+ remainder = line[end_pos:]
210
+ in_multi_comment = False
211
+ # Process remainder as a new line
212
+ if remainder.strip():
213
+ lines[i] = remainder
214
+ continue
215
+ i += 1
216
+ continue
217
+
218
+ # --- Python docstrings (""" / ''') used as standalone comments ---
219
+ if is_python and in_python_docstring:
220
+ if python_docstring_delim and python_docstring_delim in line:
221
+ end_pos = line.index(python_docstring_delim) + len(python_docstring_delim)
222
+ remainder = line[end_pos:]
223
+ in_python_docstring = False
224
+ python_docstring_delim = None
225
+ if remainder.strip():
226
+ lines[i] = remainder
227
+ continue
228
+ i += 1
229
+ continue
230
+
231
+ stripped = line.strip()
232
+
233
+ # Skip empty lines
234
+ if not stripped:
235
+ i += 1
236
+ continue
237
+
238
+ # --- Detect multi-line comment start ---
239
+ if mc_start:
240
+ # For Python, triple-quotes can be strings or docstrings.
241
+ # We only strip standalone docstrings (line starts with triple-quote
242
+ # after optional whitespace).
243
+ if is_python:
244
+ for q in ('"""', "'''"):
245
+ if stripped.startswith(q):
246
+ # Single-line docstring: """..."""
247
+ if stripped.count(q) >= 2 and stripped.endswith(q) and len(stripped) > 3:
248
+ # Check it's not a variable assignment like x = """..."""
249
+ leading = line[:len(line) - len(line.lstrip())]
250
+ code_before = line[:line.index(q)].strip()
251
+ if not code_before or code_before.endswith('='):
252
+ # Standalone docstring or assigned — skip if standalone
253
+ if not code_before:
254
+ line = None
255
+ break
256
+ # If code before and not assignment, keep line
257
+ elif stripped.startswith(q) and stripped.count(q) == 1:
258
+ # Multi-line docstring start
259
+ code_before = line[:line.index(q)].strip()
260
+ if not code_before:
261
+ in_python_docstring = True
262
+ python_docstring_delim = q
263
+ line = None
264
+ break
265
+ if line is None:
266
+ i += 1
267
+ continue
268
+ else:
269
+ # Non-Python: check for multi-line comment start
270
+ mc_pos = stripped.find(mc_start)
271
+ if mc_pos != -1:
272
+ # Check if inside a string
273
+ full_pos = line.find(mc_start)
274
+ if not _is_in_string(line, full_pos, string_delims):
275
+ # Check for same-line close
276
+ after_start = line[full_pos + len(mc_start):]
277
+ close_pos = after_start.find(mc_end) if mc_end else -1
278
+ if close_pos != -1 and mc_start != mc_end:
279
+ # Single-line block comment: remove it
280
+ before = line[:full_pos]
281
+ after = after_start[close_pos + len(mc_end):]
282
+ line = before + after
283
+ if not line.strip():
284
+ i += 1
285
+ continue
286
+ # Re-process this reconstructed line
287
+ lines[i] = line
288
+ continue
289
+ else:
290
+ # Multi-line comment starts here
291
+ before = line[:full_pos]
292
+ in_multi_comment = True
293
+ if before.strip():
294
+ line = before
295
+ else:
296
+ i += 1
297
+ continue
298
+
299
+ # --- Full-line single-line comment ---
300
+ if spec.single_comment and stripped.startswith(spec.single_comment):
301
+ # Special: keep shebangs
302
+ if stripped.startswith('#!') and i == 0:
303
+ result.append((i + 1, stripped))
304
+ i += 1
305
+ continue
306
+
307
+ # --- Remove inline comment ---
308
+ if spec.single_comment:
309
+ line = _remove_inline_comment(line, spec.single_comment, string_delims)
310
+
311
+ # --- Clean whitespace ---
312
+ if preserve_indent:
313
+ # Keep leading whitespace, strip trailing
314
+ leading = line[:len(line) - len(line.lstrip())]
315
+ content = line.strip()
316
+ if not content:
317
+ i += 1
318
+ continue
319
+ # Collapse internal multiple spaces (but not in strings)
320
+ line = leading + content
321
+ else:
322
+ line = line.strip()
323
+ if not line:
324
+ i += 1
325
+ continue
326
+
327
+ # Remove trailing whitespace
328
+ line = line.rstrip()
329
+
330
+ if line:
331
+ result.append((i + 1, line))
332
+
333
+ i += 1
334
+
335
+ return _format_result(result, include_line_numbers)
336
+
337
+
338
+ def compress_file(filepath: str, language: str | None = None,
339
+ include_line_numbers: bool = True) -> str:
340
+ """!
341
+ @brief Compress a source file by removing comments and extra whitespace.
342
+ @param filepath Path to the source file.
343
+ @param language Optional language override. Auto-detected if None.
344
+ @param include_line_numbers If True (default), prefix each line with <n>: format.
345
+ @return Compressed source code string.
346
+ @throws ValueError If language cannot be detected.
347
+ @details Implements the compress_file function behavior with deterministic control flow.
348
+ """
349
+ if language is None:
350
+ language = detect_language(filepath)
351
+ if language is None:
352
+ raise ValueError(
353
+ f"Cannot detect language for '{filepath}'. "
354
+ "Use --lang to specify explicitly.")
355
+
356
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
357
+ source = f.read()
358
+
359
+ return compress_source(source, language, include_line_numbers)
360
+
361
+
362
+ def main():
363
+ """!
364
+ @brief Execute the standalone compression CLI.
365
+ @details Parses command-line arguments and invokes `compress_file`, printing the result to stdout or errors to stderr.
366
+ @return {None} Function return value.
367
+ """
368
+ import argparse
369
+ parser = argparse.ArgumentParser(
370
+ description="Compress source code for LLM context optimization.")
371
+ parser.add_argument("file", help="Source file to compress.")
372
+ parser.add_argument("--lang", default=None,
373
+ help="Language override (auto-detected from extension).")
374
+ parser.add_argument("--enable-line-numbers", action="store_true",
375
+ default=False,
376
+ help="Enable line number prefixes (<n>:) in output.")
377
+ args = parser.parse_args()
378
+
379
+ if not os.path.isfile(args.file):
380
+ print(f"Error: file not found: {args.file}", file=sys.stderr)
381
+ sys.exit(1)
382
+
383
+ try:
384
+ compressed = compress_file(args.file, args.lang,
385
+ args.enable_line_numbers)
386
+ print(compressed)
387
+ except ValueError as e:
388
+ print(f"Error: {e}", file=sys.stderr)
389
+ sys.exit(1)
390
+
391
+
392
+ if __name__ == "__main__":
393
+ main()
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """!
3
+ @file compress_files.py
4
+ @brief Compress and concatenate multiple source files.
5
+ @details Uses the compress module to strip comments and whitespace from each input file, then concatenates results with a compact header per file for unique identification by an LLM agent.
6
+ @author GitHub Copilot
7
+ @version 0.0.70
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from .compress import compress_file, detect_language
15
+
16
+
17
+ def _extract_line_range(compressed_with_line_numbers: str) -> tuple[int, int]:
18
+ """! @brief Extract source line interval from compressed output with <n>: prefixes.
19
+ @param compressed_with_line_numbers Compressed payload generated with include_line_numbers=True.
20
+ @return Tuple (line_start, line_end) derived from preserved <n>: prefixes; returns (0, 0) when no prefixed lines exist.
21
+ @details Parses the first token of each line as an integer line number.
22
+ """
23
+ line_numbers: list[int] = []
24
+ for line in compressed_with_line_numbers.splitlines():
25
+ marker, sep, _ = line.partition(":")
26
+ if sep and marker.isdigit():
27
+ line_numbers.append(int(marker))
28
+
29
+ if not line_numbers:
30
+ return 0, 0
31
+
32
+ return line_numbers[0], line_numbers[-1]
33
+
34
+
35
+ def _format_output_path(filepath: str, output_base: Path | None) -> str:
36
+ """!
37
+ @brief Build the header-visible path for one compressed source file.
38
+ @param filepath Absolute or relative source file path.
39
+ @param output_base Project-home base used to relativize output paths.
40
+ @return Original filepath when output_base is None; otherwise POSIX relative path from output_base.
41
+ @details Implements the _format_output_path function behavior with deterministic control flow.
42
+ """
43
+ if output_base is None:
44
+ return filepath
45
+ absolute_path = Path(filepath).resolve()
46
+ return Path(os.path.relpath(absolute_path, output_base)).as_posix()
47
+
48
+
49
+ def compress_files(filepaths: list[str],
50
+ include_line_numbers: bool = True,
51
+ verbose: bool = False,
52
+ output_base: Path | None = None) -> str:
53
+ """! @brief Compress multiple source files and concatenate with identifying headers.
54
+ @param filepaths List of source file paths.
55
+ @param include_line_numbers If True (default), keep <n>: prefixes in code block lines.
56
+ @param verbose If True, emits progress status messages on stderr.
57
+ @param output_base Project-home base used to render header paths as relative paths.
58
+ @return Concatenated compressed output string.
59
+ @throws ValueError If no files could be processed.
60
+ @details Each file is compressed and emitted as: header line `@@@ <path> | <lang>`, line-range metadata `> Lines: <start>-<end>`, and fenced code block delimited by triple backticks. Line range is derived from the already computed <n>: prefixes to preserve existing numbering logic. Files are separated by a blank line.
61
+ """
62
+ parts = []
63
+ ok_count = 0
64
+ fail_count = 0
65
+ resolved_output_base = output_base.resolve() if output_base is not None else None
66
+
67
+ for fpath in filepaths:
68
+ if not os.path.isfile(fpath):
69
+ if verbose:
70
+ print(f" SKIP {fpath} (not found)", file=sys.stderr)
71
+ continue
72
+
73
+ lang = detect_language(fpath)
74
+ if not lang:
75
+ if verbose:
76
+ print(f" SKIP {fpath} (unsupported extension)", file=sys.stderr)
77
+ continue
78
+
79
+ try:
80
+ compressed_with_line_numbers = compress_file(fpath, lang, True)
81
+ line_start, line_end = _extract_line_range(compressed_with_line_numbers)
82
+ compressed = (
83
+ compressed_with_line_numbers
84
+ if include_line_numbers
85
+ else compress_file(fpath, lang, False)
86
+ )
87
+ output_path = _format_output_path(fpath, resolved_output_base)
88
+ header = f"@@@ {output_path} | {lang}"
89
+ parts.append(
90
+ f"{header}\n> Lines: {line_start}-{line_end}\n```\n{compressed}\n```"
91
+ )
92
+ ok_count += 1
93
+ if verbose:
94
+ print(f" OK {fpath}", file=sys.stderr)
95
+ except Exception as e:
96
+ if verbose:
97
+ print(f" FAIL {fpath} ({e})", file=sys.stderr)
98
+ fail_count += 1
99
+
100
+ if not parts:
101
+ raise ValueError("No valid source files processed")
102
+
103
+ if verbose:
104
+ print(f"\n Compressed: {ok_count} ok, {fail_count} failed",
105
+ file=sys.stderr)
106
+
107
+ return "\n\n".join(parts)
108
+
109
+
110
+ def main():
111
+ """!
112
+ @brief Execute the multi-file compression CLI command.
113
+ @details Parses command-line arguments, calls `compress_files`, and prints output or errors.
114
+ @return {None} Function return value.
115
+ """
116
+ import argparse
117
+ parser = argparse.ArgumentParser(
118
+ description="Compress and concatenate source files for LLM context.")
119
+ parser.add_argument("files", nargs="+", help="Source files to compress.")
120
+ parser.add_argument("--enable-line-numbers", action="store_true",
121
+ default=False,
122
+ help="Enable line number prefixes (<n>:) in output.")
123
+ args = parser.parse_args()
124
+
125
+ try:
126
+ output = compress_files(args.files, args.enable_line_numbers)
127
+ print(output)
128
+ except ValueError as e:
129
+ print(f"Error: {e}", file=sys.stderr)
130
+ sys.exit(1)
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()