clew-trace 1.0.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 (101) hide show
  1. clew/__init__.py +96 -0
  2. clew/__main__.py +14 -0
  3. clew/_common.py +173 -0
  4. clew/ast_symbols.py +1781 -0
  5. clew/buildoptions.py +696 -0
  6. clew/call_edges.py +1306 -0
  7. clew/callback_edges.py +1386 -0
  8. clew/cli.py +2464 -0
  9. clew/coverage.py +663 -0
  10. clew/critical_sections.py +597 -0
  11. clew/datamodel.py +1077 -0
  12. clew/declaration.py +616 -0
  13. clew/diagnostics.py +252 -0
  14. clew/dispatch.py +472 -0
  15. clew/dispatch_edges.py +765 -0
  16. clew/dominated_edges.py +356 -0
  17. clew/doxygen.py +1263 -0
  18. clew/enrichment.py +105 -0
  19. clew/errors.py +72 -0
  20. clew/event_edges.py +358 -0
  21. clew/export_command.py +183 -0
  22. clew/external.py +280 -0
  23. clew/filedocs.py +433 -0
  24. clew/gitenv.py +69 -0
  25. clew/guardconfig.py +512 -0
  26. clew/harvest.py +614 -0
  27. clew/harvest_plan.py +186 -0
  28. clew/indexcache.py +538 -0
  29. clew/init_command.py +613 -0
  30. clew/kconfig.py +796 -0
  31. clew/kconfig_gates.py +567 -0
  32. clew/locks.py +1068 -0
  33. clew/mcp_config.py +615 -0
  34. clew/mcp_server/__init__.py +32 -0
  35. clew/mcp_server/__main__.py +13 -0
  36. clew/mcp_server/_sdk.py +105 -0
  37. clew/mcp_server/descriptions/_provenance.md +134 -0
  38. clew/mcp_server/descriptions/_templates/ambiguous.json +33 -0
  39. clew/mcp_server/descriptions/_templates/disambiguate.json +40 -0
  40. clew/mcp_server/descriptions/_templates/provenance.json +39 -0
  41. clew/mcp_server/descriptions/_templates/rows.json +14 -0
  42. clew/mcp_server/descriptions/dossier.json +17 -0
  43. clew/mcp_server/descriptions/index.json +13 -0
  44. clew/mcp_server/descriptions/propose_declaration.json +8 -0
  45. clew/mcp_server/descriptions/search.json +17 -0
  46. clew/mcp_server/descriptions.py +239 -0
  47. clew/mcp_server/emptiness.py +449 -0
  48. clew/mcp_server/freshness.py +417 -0
  49. clew/mcp_server/server.py +1723 -0
  50. clew/mcp_server/state.py +905 -0
  51. clew/mcp_server/tools_query.py +1938 -0
  52. clew/precommit.py +386 -0
  53. clew/preprocessor.py +834 -0
  54. clew/propose/__init__.py +83 -0
  55. clew/propose/astdefs.py +432 -0
  56. clew/propose/command.py +250 -0
  57. clew/propose/context.py +56 -0
  58. clew/propose/dryrun.py +259 -0
  59. clew/propose/model.py +179 -0
  60. clew/propose/notindexed.py +322 -0
  61. clew/propose/registry.py +488 -0
  62. clew/propose/render.py +416 -0
  63. clew/propose/scanning.py +428 -0
  64. clew/propose/sharedkey_detect.py +319 -0
  65. clew/propose/sharedkey_report.py +409 -0
  66. clew/propose/threads_detect.py +443 -0
  67. clew/propose/threads_report.py +350 -0
  68. clew/prose.py +168 -0
  69. clew/py_entrypoints.py +405 -0
  70. clew/pyast.py +805 -0
  71. clew/query/__init__.py +239 -0
  72. clew/query/_common.py +679 -0
  73. clew/query/corpus.py +961 -0
  74. clew/query/dossier.py +638 -0
  75. clew/query/externcalls.py +242 -0
  76. clew/query/graph.py +487 -0
  77. clew/query/kconfig.py +519 -0
  78. clew/query/locks.py +731 -0
  79. clew/query/macros.py +214 -0
  80. clew/query/models.py +1931 -0
  81. clew/query/source.py +387 -0
  82. clew/query/subject.py +798 -0
  83. clew/query/symbols.py +1973 -0
  84. clew/query/traversal.py +412 -0
  85. clew/reachability.py +239 -0
  86. clew/requirements.py +780 -0
  87. clew/scope.py +729 -0
  88. clew/shared_key_edges.py +2452 -0
  89. clew/signature.py +353 -0
  90. clew/stagetimer.py +141 -0
  91. clew/threads.py +1516 -0
  92. clew/tiers.py +564 -0
  93. clew/tomlcompat.py +94 -0
  94. clew/treescan.py +371 -0
  95. clew/vocabulary.py +1080 -0
  96. clew/wire.py +193 -0
  97. clew_trace-1.0.0.dist-info/METADATA +284 -0
  98. clew_trace-1.0.0.dist-info/RECORD +101 -0
  99. clew_trace-1.0.0.dist-info/WHEEL +4 -0
  100. clew_trace-1.0.0.dist-info/entry_points.txt +3 -0
  101. clew_trace-1.0.0.dist-info/licenses/LICENSE +21 -0
clew/__init__.py ADDED
@@ -0,0 +1,96 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Build a doxygen SQLite knowledge database from source code.
3
+
4
+ Public entry point: `python -m clew --doxyfile <path>
5
+ --output <path> [--enrich ...] [--repo-root ...]`.
6
+
7
+ The pipeline:
8
+
9
+ 1. doxygen runs with GENERATE_SQLITE3 forced on
10
+ (see `doxygen.run_doxygen`).
11
+ 2. Generated DB copied into place; doxygen STRIP_FROM_PATH paths
12
+ are restored to repo-root-relative form
13
+ (`doxygen.fix_doxygen_paths`).
14
+ 3. README/CHANGELOG/docs/*.md ingested into FTS5
15
+ (`prose.ingest_supplementary_docs`).
16
+ 4. Two layers of call-edge import populate `call_edges`:
17
+ `call_edges.build_call_edges` (doxygen sqlite3 inline xrefs),
18
+ `call_edges.import_ast_call_edges` (tree-sitter AST walk).
19
+ 5. Reachability BFS marks every function `live` or `orphan`
20
+ (`reachability.mark_reachability`).
21
+ 6. Stats printed (`cli.report_stats`).
22
+
23
+ Each module is independently testable; previously this was one
24
+ ~1200-line file. Split landed in Phase C PR-2.
25
+
26
+ @brief Doxygen → SQLite knowledge-database build pipeline.
27
+ @version 3
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ # Re-export the public functions so the flat-module API keeps working
33
+ # for callers importing `clew.X`, in a checkout and from the
34
+ # wheel alike. Relative imports work in both contexts.
35
+ from .call_edges import (
36
+ build_call_edges,
37
+ import_ast_call_edges,
38
+ )
39
+ from .cli import main, report_stats
40
+ from .dispatch import load_dispatch_manifest, shared_key_document
41
+ from .dispatch_edges import import_declared_dispatch_edges
42
+ from .doxygen import (
43
+ copy_database,
44
+ fix_doxygen_paths,
45
+ run_doxygen,
46
+ )
47
+ from .enrichment import enrich_database
48
+ from .filedocs import extract_file_doc, ingest_file_docs
49
+ from .kconfig import import_kconfig
50
+ from .kconfig_gates import import_kconfig_gates
51
+ from .prose import ingest_supplementary_docs
52
+ from .reachability import (
53
+ DEFAULT_ENTRY_PATTERNS,
54
+ ENTRY_PATTERN_FACTS,
55
+ ENTRY_PATTERN_HEURISTICS,
56
+ mark_reachability,
57
+ )
58
+ from .shared_key_edges import (
59
+ import_mqtt_dispatch_edges,
60
+ import_shared_key_edges_declared,
61
+ import_shared_key_edges_inferred,
62
+ )
63
+ from .threads import (
64
+ DEFAULT_SPAWN_PATTERNS,
65
+ annotate_thread_boundaries,
66
+ extract_threads,
67
+ )
68
+
69
+ __all__ = [
70
+ "DEFAULT_ENTRY_PATTERNS",
71
+ "DEFAULT_SPAWN_PATTERNS",
72
+ "ENTRY_PATTERN_FACTS",
73
+ "ENTRY_PATTERN_HEURISTICS",
74
+ "annotate_thread_boundaries",
75
+ "build_call_edges",
76
+ "copy_database",
77
+ "enrich_database",
78
+ "extract_file_doc",
79
+ "extract_threads",
80
+ "fix_doxygen_paths",
81
+ "import_ast_call_edges",
82
+ "import_declared_dispatch_edges",
83
+ "import_kconfig",
84
+ "import_kconfig_gates",
85
+ "import_mqtt_dispatch_edges",
86
+ "import_shared_key_edges_declared",
87
+ "import_shared_key_edges_inferred",
88
+ "ingest_file_docs",
89
+ "ingest_supplementary_docs",
90
+ "load_dispatch_manifest",
91
+ "main",
92
+ "mark_reachability",
93
+ "report_stats",
94
+ "run_doxygen",
95
+ "shared_key_document",
96
+ ]
clew/__main__.py ADDED
@@ -0,0 +1,14 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Module entry point: `python -m clew`.
3
+
4
+ Defers to `cli.main`. Exists so a caller can run the package via
5
+ `python -m clew ...` without needing a wrapper script.
6
+
7
+ @brief Module entry-point shim.
8
+ @version 1
9
+ """
10
+
11
+ from .cli import main
12
+
13
+ if __name__ == "__main__":
14
+ main()
clew/_common.py ADDED
@@ -0,0 +1,173 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Shared helpers for the clew package.
3
+
4
+ The Rich Progress factory, the module logger, and the one seam that decides
5
+ WHERE pipeline output goes. Kept tiny; if more shared state grows, move those
6
+ helpers here in their own modules.
7
+
8
+ @brief Shared helpers for clew submodules.
9
+ @version 2
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import io
15
+ import logging
16
+ import os
17
+ import threading
18
+ from collections.abc import Iterator
19
+ from contextlib import contextmanager
20
+ from contextvars import ContextVar
21
+
22
+ from rich.console import Console
23
+ from rich.progress import (
24
+ BarColumn,
25
+ MofNCompleteColumn,
26
+ Progress,
27
+ SpinnerColumn,
28
+ TaskProgressColumn,
29
+ TextColumn,
30
+ TimeElapsedColumn,
31
+ )
32
+
33
+ logger = logging.getLogger("clew")
34
+ console = Console()
35
+
36
+ ## The console pipeline output is rendered to for the duration of ONE call.
37
+ ##
38
+ ## A ContextVar rather than a module-level rebind, because a caller silencing its
39
+ ## own build must not silence anyone else's: a build dispatched to a worker thread
40
+ ## runs beside a thread that may be writing to the real stdout, and each thread
41
+ ## carries its own context, so a value set inside the worker is invisible there.
42
+ ## None means "the process console", which is what a plain CLI build wants.
43
+ _render_target: ContextVar[Console | None] = ContextVar("clew_render_target", default=None)
44
+
45
+
46
+ ## @brief os.environ + NO_COLOR/TERM=dumb so pipeline subprocesses emit no ANSI.
47
+ ## @version 2
48
+ ## @req REQ-DDB-PIPE-006
49
+ ## @return Copy of os.environ with NO_COLOR=1, TERM=dumb, and CLICOLOR=0 set.
50
+ def clean_subprocess_env() -> dict[str, str]:
51
+ """Return a copy of the environment with color/interactive TTY hints forced
52
+ off (NO_COLOR=1, TERM=dumb, CLICOLOR=0). Passed to every pipeline subprocess
53
+ (e.g. doxygen) so captured build output carries no SGR escape sequences.
54
+
55
+ @brief Build a color-suppressing environment for pipeline subprocesses.
56
+ @version 2
57
+ """
58
+ env = dict(os.environ)
59
+ env["NO_COLOR"] = "1"
60
+ env["TERM"] = "dumb"
61
+ env["CLICOLOR"] = "0"
62
+ return env
63
+
64
+
65
+ ## @brief The console every pipeline writer renders to right now.
66
+ ## @return The console injected by `captured_output`, else the process console.
67
+ ## @version 1
68
+ ## @utility
69
+ def active_console() -> Console:
70
+ """Read at write time rather than captured at import, so a caller can redirect
71
+ the whole pipeline for one call without swapping `sys.stdout` — which is
72
+ process-global and would take the frames of any other thread with it.
73
+
74
+ @brief Resolve the current render target for pipeline output.
75
+ @return The console to write to.
76
+ @version 1
77
+ """
78
+ return _render_target.get() or console
79
+
80
+
81
+ ## @brief A log handler that only accepts records raised on the thread that made it.
82
+ ## @version 1
83
+ ## @utility
84
+ class _OwnThreadHandler(logging.StreamHandler):
85
+ """The render target is context-local, so two concurrent runs cannot see each
86
+ other's bars. A logger is not: it is one object shared by every thread, and a
87
+ handler added to it would collect the other run's records too. Matching on the
88
+ creating thread restores the same isolation for the log half.
89
+
90
+ @brief Stream handler scoped to one thread's records.
91
+ @version 1
92
+ """
93
+
94
+ ## @brief Bind the handler to a stream and to the creating thread.
95
+ ## @param stream Stream the records are written to.
96
+ ## @version 1
97
+ ## @dg_internal
98
+ def __init__(self, stream: io.StringIO) -> None:
99
+ """@brief Record which thread's output this handler accepts."""
100
+ super().__init__(stream)
101
+ self.owner = threading.get_ident()
102
+
103
+ ## @brief Emit only when the record came from the owning thread.
104
+ ## @param record The record being handled.
105
+ ## @version 1
106
+ ## @dg_internal
107
+ def emit(self, record: logging.LogRecord) -> None:
108
+ """@brief Drop records raised on any other thread."""
109
+ if record.thread == self.owner:
110
+ super().emit(record)
111
+
112
+
113
+ ## @brief Send every pipeline writer into a buffer instead of the process stdout.
114
+ ## @return Context manager yielding the buffer the output lands in.
115
+ ## @version 2
116
+ ## @utility
117
+ @contextmanager
118
+ def captured_output() -> Iterator[io.StringIO]:
119
+ """The seam a caller uses when its `sys.stdout` is a protocol transport: the
120
+ progress bars, the doxygen warning lines, the build summary and the pipeline's
121
+ own log records all land in the yielded buffer, and stdout is untouched for
122
+ anyone else.
123
+
124
+ The LOG half is here because the log is where the pipeline explains itself —
125
+ which Doxyfile it resolved, which scope tier it fell back to, what it refused.
126
+ Capturing the rendered output alone would hand a caller a table of row counts
127
+ and drop the sentence saying why the counts are what they are.
128
+
129
+ The buffer stays readable after the block ends, so the partial output of a run
130
+ that raised is still available to report.
131
+
132
+ @brief Capture pipeline output and logging for the duration of a call.
133
+ @return The buffer receiving everything the pipeline emits.
134
+ @version 2
135
+ """
136
+ buffer = io.StringIO()
137
+ handler = _OwnThreadHandler(buffer)
138
+ handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
139
+ level = logger.level
140
+ token = _render_target.set(Console(file=buffer, no_color=True, width=100))
141
+ logger.addHandler(handler)
142
+ logger.setLevel(logging.INFO)
143
+ try:
144
+ yield buffer
145
+ finally:
146
+ logger.setLevel(level)
147
+ logger.removeHandler(handler)
148
+ _render_target.reset(token)
149
+
150
+
151
+ ## @brief Single consistent Progress factory across all DB-build stages.
152
+ ## @version 2
153
+ ## @return A configured rich Progress instance with the standard column layout.
154
+ ## @utility
155
+ def make_progress(known_total: bool = True) -> Progress:
156
+ """Single consistent Progress factory across all DB-build stages, bound to
157
+ whichever console is active when the bar is created.
158
+
159
+ @brief Progress factory used by doxygen, AST, and XML stages.
160
+ @version 2
161
+ """
162
+ cols = [
163
+ SpinnerColumn(),
164
+ TextColumn(" [bold]{task.description:<14}"),
165
+ BarColumn(bar_width=28),
166
+ ]
167
+ if known_total:
168
+ cols.append(TaskProgressColumn())
169
+ cols.append(MofNCompleteColumn())
170
+ else:
171
+ cols.append(TextColumn("[cyan]{task.completed}[/] items"))
172
+ cols.append(TimeElapsedColumn())
173
+ return Progress(*cols, console=active_console(), transient=False)