codeupipe 0.1.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.
- codeupipe/__init__.py +39 -0
- codeupipe/cli.py +1502 -0
- codeupipe/converter/__init__.py +9 -0
- codeupipe/converter/config.py +119 -0
- codeupipe/converter/filters/__init__.py +21 -0
- codeupipe/converter/filters/analyze.py +60 -0
- codeupipe/converter/filters/classify.py +52 -0
- codeupipe/converter/filters/classify_files.py +61 -0
- codeupipe/converter/filters/generate_export.py +187 -0
- codeupipe/converter/filters/generate_import.py +229 -0
- codeupipe/converter/filters/parse_config.py +26 -0
- codeupipe/converter/filters/scan_project.py +52 -0
- codeupipe/converter/pipelines/__init__.py +8 -0
- codeupipe/converter/pipelines/export_pipeline.py +40 -0
- codeupipe/converter/pipelines/import_pipeline.py +40 -0
- codeupipe/converter/taps/__init__.py +7 -0
- codeupipe/converter/taps/conversion_log.py +46 -0
- codeupipe/core/__init__.py +20 -0
- codeupipe/core/filter.py +27 -0
- codeupipe/core/hook.py +34 -0
- codeupipe/core/payload.py +94 -0
- codeupipe/core/pipeline.py +231 -0
- codeupipe/core/state.py +78 -0
- codeupipe/core/stream_filter.py +33 -0
- codeupipe/core/tap.py +27 -0
- codeupipe/core/valve.py +52 -0
- codeupipe/linter/__init__.py +57 -0
- codeupipe/linter/assemble_doc_report.py +97 -0
- codeupipe/linter/assemble_report.py +140 -0
- codeupipe/linter/check_bundle.py +47 -0
- codeupipe/linter/check_index.py +88 -0
- codeupipe/linter/check_naming.py +47 -0
- codeupipe/linter/check_protocols.py +73 -0
- codeupipe/linter/check_structure.py +41 -0
- codeupipe/linter/check_symbols.py +116 -0
- codeupipe/linter/check_tests.py +48 -0
- codeupipe/linter/coverage_pipeline.py +39 -0
- codeupipe/linter/detect_drift.py +44 -0
- codeupipe/linter/detect_orphans.py +106 -0
- codeupipe/linter/doc_check_pipeline.py +28 -0
- codeupipe/linter/git_history.py +130 -0
- codeupipe/linter/lint_pipeline.py +51 -0
- codeupipe/linter/map_coverage.py +84 -0
- codeupipe/linter/report_gaps.py +68 -0
- codeupipe/linter/report_pipeline.py +49 -0
- codeupipe/linter/resolve_refs.py +48 -0
- codeupipe/linter/scan_components.py +95 -0
- codeupipe/linter/scan_directory.py +123 -0
- codeupipe/linter/scan_docs.py +62 -0
- codeupipe/linter/scan_tests.py +104 -0
- codeupipe/py.typed +1 -0
- codeupipe/testing.py +344 -0
- codeupipe/utils/__init__.py +10 -0
- codeupipe/utils/error_handling.py +68 -0
- codeupipe-0.1.0.dist-info/METADATA +216 -0
- codeupipe-0.1.0.dist-info/RECORD +60 -0
- codeupipe-0.1.0.dist-info/WHEEL +5 -0
- codeupipe-0.1.0.dist-info/entry_points.txt +2 -0
- codeupipe-0.1.0.dist-info/licenses/LICENSE +190 -0
- codeupipe-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pipeline: The Orchestrator
|
|
3
|
+
|
|
4
|
+
The Pipeline orchestrates filter execution with hooks, taps, and state tracking.
|
|
5
|
+
Filters run in sequence; Valves provide conditional flow control;
|
|
6
|
+
Taps provide observation points; Hooks provide lifecycle integration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, Set, Tuple, TypeVar, Generic, Union
|
|
11
|
+
from .payload import Payload
|
|
12
|
+
from .filter import Filter
|
|
13
|
+
from .stream_filter import StreamFilter
|
|
14
|
+
from .tap import Tap
|
|
15
|
+
from .hook import Hook
|
|
16
|
+
from .state import State
|
|
17
|
+
|
|
18
|
+
__all__ = ["Pipeline"]
|
|
19
|
+
|
|
20
|
+
TInput = TypeVar('TInput')
|
|
21
|
+
TOutput = TypeVar('TOutput')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Pipeline(Generic[TInput, TOutput]):
|
|
25
|
+
"""
|
|
26
|
+
Orchestrator — runs filters in sequence with hooks, taps, and state tracking.
|
|
27
|
+
|
|
28
|
+
Build a pipeline by adding filters (.add_filter), taps (.add_tap),
|
|
29
|
+
and hooks (.use_hook). Run it with .run(payload).
|
|
30
|
+
After execution, inspect .state for execution metadata.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self._steps: List[Tuple[str, Union[Filter, Tap], str]] = [] # (name, step, type)
|
|
35
|
+
self._hooks: List[Hook] = []
|
|
36
|
+
self._state: State = State()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def state(self) -> State:
|
|
40
|
+
"""Access pipeline execution state after run()."""
|
|
41
|
+
return self._state
|
|
42
|
+
|
|
43
|
+
def add_filter(self, filter: Filter[TInput, TOutput], name: Optional[str] = None) -> None:
|
|
44
|
+
"""Add a filter to the pipeline."""
|
|
45
|
+
filter_name = name or filter.__class__.__name__
|
|
46
|
+
self._steps.append((filter_name, filter, "filter"))
|
|
47
|
+
|
|
48
|
+
def add_tap(self, tap: Tap, name: Optional[str] = None) -> None:
|
|
49
|
+
"""Add a tap (observation point) to the pipeline."""
|
|
50
|
+
tap_name = name or tap.__class__.__name__
|
|
51
|
+
self._steps.append((tap_name, tap, "tap"))
|
|
52
|
+
|
|
53
|
+
def use_hook(self, hook: Hook) -> None:
|
|
54
|
+
"""Attach a lifecycle hook."""
|
|
55
|
+
self._hooks.append(hook)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
async def _invoke(fn, *args):
|
|
59
|
+
"""Call fn(*args), awaiting the result only if it is a coroutine."""
|
|
60
|
+
result = fn(*args)
|
|
61
|
+
if inspect.isawaitable(result):
|
|
62
|
+
result = await result
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
async def run(self, initial_payload: Payload[TInput]) -> Payload[TOutput]:
|
|
66
|
+
"""Execute the pipeline — flow payload through all filters and taps."""
|
|
67
|
+
# Check for StreamFilters — .run() is 1→1, StreamFilters are 0..N
|
|
68
|
+
for name, step, step_type in self._steps:
|
|
69
|
+
if step_type == "filter" and self._is_stream_filter(step):
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Pipeline contains StreamFilter '{name}'. "
|
|
72
|
+
f"Use pipeline.stream(source) with an async generator instead. "
|
|
73
|
+
f"Example: async for result in pipeline.stream(async_generator_of_payloads): ..."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self._state = State()
|
|
77
|
+
payload = initial_payload
|
|
78
|
+
|
|
79
|
+
# Hook: pipeline start
|
|
80
|
+
for hook in self._hooks:
|
|
81
|
+
await self._invoke(hook.before, None, payload)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
for name, step, step_type in self._steps:
|
|
85
|
+
|
|
86
|
+
if step_type == "tap":
|
|
87
|
+
await self._invoke(step.observe, payload) # type: ignore
|
|
88
|
+
self._state.mark_executed(name)
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
# It's a filter (or valve — valves conform to Filter protocol)
|
|
92
|
+
for hook in self._hooks:
|
|
93
|
+
await self._invoke(hook.before, step, payload)
|
|
94
|
+
|
|
95
|
+
payload = await self._invoke(step.call, payload) # type: ignore
|
|
96
|
+
|
|
97
|
+
# Track valve skips via Valve's own tracking
|
|
98
|
+
if hasattr(step, '_last_skipped') and step._last_skipped:
|
|
99
|
+
self._state.mark_skipped(name)
|
|
100
|
+
else:
|
|
101
|
+
self._state.mark_executed(name)
|
|
102
|
+
|
|
103
|
+
for hook in self._hooks:
|
|
104
|
+
await self._invoke(hook.after, step, payload)
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
for hook in self._hooks:
|
|
108
|
+
await self._invoke(hook.on_error, None, e, payload)
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
# Hook: pipeline end
|
|
112
|
+
for hook in self._hooks:
|
|
113
|
+
await self._invoke(hook.after, None, payload)
|
|
114
|
+
|
|
115
|
+
return payload # type: ignore
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
# Streaming
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _is_stream_filter(step) -> bool:
|
|
123
|
+
"""Check if a step implements the StreamFilter protocol (has .stream())."""
|
|
124
|
+
return hasattr(step, 'stream') and callable(getattr(step, 'stream'))
|
|
125
|
+
|
|
126
|
+
async def stream(
|
|
127
|
+
self,
|
|
128
|
+
source: AsyncIterator[Payload[TInput]],
|
|
129
|
+
) -> AsyncIterator[Payload[TOutput]]:
|
|
130
|
+
"""
|
|
131
|
+
Stream payloads through the pipeline, one chunk at a time.
|
|
132
|
+
|
|
133
|
+
source: An async iterable of Payload chunks.
|
|
134
|
+
Yields: Transformed Payload chunks as they flow out.
|
|
135
|
+
|
|
136
|
+
- Regular Filters are auto-adapted: 1 chunk in → 1 chunk out.
|
|
137
|
+
- StreamFilters can yield 0, 1, or N chunks per input.
|
|
138
|
+
- Valves gate per-chunk (predicate evaluated on each chunk).
|
|
139
|
+
- Taps observe each chunk.
|
|
140
|
+
- Hooks fire once per filter at stream-start and stream-end.
|
|
141
|
+
- State tracks chunks_processed per step.
|
|
142
|
+
"""
|
|
143
|
+
self._state = State()
|
|
144
|
+
|
|
145
|
+
# Hook: pipeline start (payload=None-ish, use empty payload as sentinel)
|
|
146
|
+
sentinel = Payload()
|
|
147
|
+
for hook in self._hooks:
|
|
148
|
+
await self._invoke(hook.before, None, sentinel)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# Build the processing chain as nested async generators
|
|
152
|
+
async def _source_gen():
|
|
153
|
+
async for chunk in source:
|
|
154
|
+
yield chunk
|
|
155
|
+
|
|
156
|
+
current = _source_gen()
|
|
157
|
+
|
|
158
|
+
for name, step, step_type in self._steps:
|
|
159
|
+
current = self._wrap_step(current, name, step, step_type)
|
|
160
|
+
|
|
161
|
+
# Drain the chain, yielding results to the caller
|
|
162
|
+
async for result in current:
|
|
163
|
+
yield result
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
for hook in self._hooks:
|
|
167
|
+
await self._invoke(hook.on_error, None, e, sentinel)
|
|
168
|
+
raise
|
|
169
|
+
|
|
170
|
+
# Hook: pipeline end
|
|
171
|
+
for hook in self._hooks:
|
|
172
|
+
await self._invoke(hook.after, None, sentinel)
|
|
173
|
+
|
|
174
|
+
async def _wrap_step(
|
|
175
|
+
self,
|
|
176
|
+
upstream: AsyncIterator[Payload],
|
|
177
|
+
name: str,
|
|
178
|
+
step,
|
|
179
|
+
step_type: str,
|
|
180
|
+
) -> AsyncIterator[Payload]:
|
|
181
|
+
"""Wrap a single step around an upstream async iterator."""
|
|
182
|
+
|
|
183
|
+
# --- Tap: observe each chunk, pass through unchanged ---
|
|
184
|
+
if step_type == "tap":
|
|
185
|
+
if name not in self._state.executed:
|
|
186
|
+
self._state.mark_executed(name)
|
|
187
|
+
async for chunk in upstream:
|
|
188
|
+
await self._invoke(step.observe, chunk) # type: ignore
|
|
189
|
+
self._state.increment_chunks(name)
|
|
190
|
+
yield chunk
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# --- Filter or Valve ---
|
|
194
|
+
# Fire hook.before once at the start of this step's stream
|
|
195
|
+
for hook in self._hooks:
|
|
196
|
+
await self._invoke(hook.before, step, Payload())
|
|
197
|
+
|
|
198
|
+
is_valve = hasattr(step, '_predicate')
|
|
199
|
+
is_stream = self._is_stream_filter(step)
|
|
200
|
+
|
|
201
|
+
if name not in self._state.executed and name not in self._state.skipped:
|
|
202
|
+
self._state.mark_executed(name)
|
|
203
|
+
|
|
204
|
+
async for chunk in upstream:
|
|
205
|
+
# Valve gating — per-chunk predicate
|
|
206
|
+
if is_valve:
|
|
207
|
+
if not step._predicate(chunk):
|
|
208
|
+
self._state.increment_chunks(name) # counted but skipped
|
|
209
|
+
yield chunk
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if is_stream:
|
|
213
|
+
# StreamFilter: yield 0..N chunks per input
|
|
214
|
+
stream_result = step.stream(chunk)
|
|
215
|
+
if inspect.isasyncgen(stream_result):
|
|
216
|
+
async for out in stream_result:
|
|
217
|
+
self._state.increment_chunks(name)
|
|
218
|
+
yield out
|
|
219
|
+
else:
|
|
220
|
+
for out in stream_result:
|
|
221
|
+
self._state.increment_chunks(name)
|
|
222
|
+
yield out
|
|
223
|
+
else:
|
|
224
|
+
# Regular Filter: 1 chunk in → 1 chunk out
|
|
225
|
+
result = await self._invoke(step.call, chunk) # type: ignore
|
|
226
|
+
self._state.increment_chunks(name)
|
|
227
|
+
yield result
|
|
228
|
+
|
|
229
|
+
# Fire hook.after once at the end of this step's stream
|
|
230
|
+
for hook in self._hooks:
|
|
231
|
+
await self._invoke(hook.after, step, Payload())
|
codeupipe/core/state.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
State: Pipeline Execution Metadata
|
|
3
|
+
|
|
4
|
+
State tracks what happened during pipeline execution — which filters ran,
|
|
5
|
+
which were skipped, timing data, and errors encountered.
|
|
6
|
+
Access it after pipeline.run() via pipeline.state.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
__all__ = ["State"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class State:
|
|
15
|
+
"""
|
|
16
|
+
Pipeline execution state — tracks filter execution, timing, and errors.
|
|
17
|
+
|
|
18
|
+
Provides visibility into what happened during a pipeline run:
|
|
19
|
+
- Which filters executed and in what order
|
|
20
|
+
- Which filters were skipped (by valves)
|
|
21
|
+
- Errors encountered during execution
|
|
22
|
+
- Arbitrary metadata for custom tracking
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.executed: List[str] = []
|
|
27
|
+
self.skipped: List[str] = []
|
|
28
|
+
self.errors: List[Tuple[str, Exception]] = []
|
|
29
|
+
self.metadata: Dict[str, Any] = {}
|
|
30
|
+
self.chunks_processed: Dict[str, int] = {}
|
|
31
|
+
|
|
32
|
+
def mark_executed(self, name: str) -> None:
|
|
33
|
+
"""Record that a filter executed."""
|
|
34
|
+
self.executed.append(name)
|
|
35
|
+
|
|
36
|
+
def mark_skipped(self, name: str) -> None:
|
|
37
|
+
"""Record that a filter was skipped."""
|
|
38
|
+
self.skipped.append(name)
|
|
39
|
+
|
|
40
|
+
def increment_chunks(self, name: str, count: int = 1) -> None:
|
|
41
|
+
"""Increment the chunk counter for a streaming step."""
|
|
42
|
+
self.chunks_processed[name] = self.chunks_processed.get(name, 0) + count
|
|
43
|
+
|
|
44
|
+
def record_error(self, name: str, error: Exception) -> None:
|
|
45
|
+
"""Record an error from a filter."""
|
|
46
|
+
self.errors.append((name, error))
|
|
47
|
+
|
|
48
|
+
def set(self, key: str, value: Any) -> None:
|
|
49
|
+
"""Store arbitrary metadata."""
|
|
50
|
+
self.metadata[key] = value
|
|
51
|
+
|
|
52
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
53
|
+
"""Retrieve metadata."""
|
|
54
|
+
return self.metadata.get(key, default)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def has_errors(self) -> bool:
|
|
58
|
+
"""Whether any errors were recorded."""
|
|
59
|
+
return len(self.errors) > 0
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def last_error(self) -> Optional[Exception]:
|
|
63
|
+
"""The most recent error, or None."""
|
|
64
|
+
return self.errors[-1][1] if self.errors else None
|
|
65
|
+
|
|
66
|
+
def reset(self) -> None:
|
|
67
|
+
"""Reset state for a fresh run."""
|
|
68
|
+
self.executed.clear()
|
|
69
|
+
self.skipped.clear()
|
|
70
|
+
self.errors.clear()
|
|
71
|
+
self.metadata.clear()
|
|
72
|
+
self.chunks_processed.clear()
|
|
73
|
+
|
|
74
|
+
def __repr__(self) -> str:
|
|
75
|
+
return (
|
|
76
|
+
f"State(executed={self.executed}, skipped={self.skipped}, "
|
|
77
|
+
f"errors={len(self.errors)}, chunks={self.chunks_processed})"
|
|
78
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StreamFilter Protocol: Chunk-at-a-Time Processing
|
|
3
|
+
|
|
4
|
+
A StreamFilter processes one Payload chunk and yields zero or more output chunks.
|
|
5
|
+
This enables filtering (drop), mapping (1→1), and fan-out (1→N) at constant memory.
|
|
6
|
+
|
|
7
|
+
Regular Filters are auto-adapted for streaming (1 chunk in → 1 chunk out).
|
|
8
|
+
StreamFilters opt in to the richer yield-based interface.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import AsyncIterator, Protocol, TypeVar
|
|
12
|
+
from .payload import Payload
|
|
13
|
+
|
|
14
|
+
__all__ = ["StreamFilter"]
|
|
15
|
+
|
|
16
|
+
TInput = TypeVar('TInput')
|
|
17
|
+
TOutput = TypeVar('TOutput')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StreamFilter(Protocol[TInput, TOutput]):
|
|
21
|
+
"""
|
|
22
|
+
Streaming processing unit — receives one chunk, yields zero or more output chunks.
|
|
23
|
+
|
|
24
|
+
Use for:
|
|
25
|
+
- Filtering: yield nothing to drop a chunk
|
|
26
|
+
- Mapping: yield one transformed chunk (same as a regular Filter)
|
|
27
|
+
- Fan-out: yield multiple chunks from one input
|
|
28
|
+
- Batching/windowing: accumulate internally, yield when ready
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
async def stream(self, chunk: Payload[TInput]) -> AsyncIterator[Payload[TOutput]]:
|
|
32
|
+
"""Process a single chunk and yield output chunks."""
|
|
33
|
+
...
|
codeupipe/core/tap.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tap: Observation Point
|
|
3
|
+
|
|
4
|
+
A Tap is a non-modifying observation point in the pipeline.
|
|
5
|
+
It receives the payload for inspection (logging, metrics, debugging)
|
|
6
|
+
but never modifies it. Think of it as a pressure gauge on a pipe.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, TypeVar
|
|
10
|
+
from .payload import Payload
|
|
11
|
+
|
|
12
|
+
__all__ = ["Tap"]
|
|
13
|
+
|
|
14
|
+
T = TypeVar('T')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Tap(Protocol[T]):
|
|
18
|
+
"""
|
|
19
|
+
Non-modifying observation point — inspect the payload without changing it.
|
|
20
|
+
|
|
21
|
+
Use Taps for logging, metrics, debugging, and auditing.
|
|
22
|
+
The pipeline calls observe() and discards the return value.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
async def observe(self, payload: Payload[T]) -> None:
|
|
26
|
+
"""Observe the payload. Must not modify it."""
|
|
27
|
+
...
|
codeupipe/core/valve.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Valve: Conditional Flow Control
|
|
3
|
+
|
|
4
|
+
A Valve wraps a Filter with a predicate — the inner filter only executes
|
|
5
|
+
when the predicate evaluates to True. Otherwise the payload passes through unchanged.
|
|
6
|
+
Valves conform to the Filter protocol, so they compose seamlessly into Pipelines.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
from typing import Callable, Generic, TypeVar
|
|
11
|
+
from .payload import Payload
|
|
12
|
+
from .filter import Filter
|
|
13
|
+
|
|
14
|
+
__all__ = ["Valve"]
|
|
15
|
+
|
|
16
|
+
TInput = TypeVar('TInput')
|
|
17
|
+
TOutput = TypeVar('TOutput')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Valve(Generic[TInput, TOutput]):
|
|
21
|
+
"""
|
|
22
|
+
Conditional flow control — gates a Filter with a predicate.
|
|
23
|
+
|
|
24
|
+
If the predicate returns True, the inner filter processes the payload.
|
|
25
|
+
If False, the payload passes through unchanged.
|
|
26
|
+
|
|
27
|
+
Conforms to the Filter protocol so it can be used anywhere a Filter is expected.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
name: str,
|
|
33
|
+
inner: Filter[TInput, TOutput],
|
|
34
|
+
predicate: Callable[[Payload[TInput]], bool],
|
|
35
|
+
):
|
|
36
|
+
self.name = name
|
|
37
|
+
self._inner = inner
|
|
38
|
+
self._predicate = predicate
|
|
39
|
+
|
|
40
|
+
async def call(self, payload: Payload[TInput]) -> Payload[TOutput]:
|
|
41
|
+
"""Execute the inner filter only if the predicate passes."""
|
|
42
|
+
if self._predicate(payload):
|
|
43
|
+
self._last_skipped = False
|
|
44
|
+
result = self._inner.call(payload)
|
|
45
|
+
if inspect.isawaitable(result):
|
|
46
|
+
result = await result
|
|
47
|
+
return result
|
|
48
|
+
self._last_skipped = True
|
|
49
|
+
return payload # type: ignore — pass through unchanged
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
return f"Valve({self.name!r})"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public API for linter package.
|
|
3
|
+
|
|
4
|
+
Auto-generated by: cup bundle
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .assemble_doc_report import AssembleDocReport
|
|
8
|
+
from .assemble_report import AssembleReport
|
|
9
|
+
from .check_bundle import CheckBundle
|
|
10
|
+
from .check_index import CheckIndex
|
|
11
|
+
from .check_naming import CheckNaming
|
|
12
|
+
from .check_protocols import CheckProtocols
|
|
13
|
+
from .check_structure import CheckStructure
|
|
14
|
+
from .check_symbols import CheckSymbols
|
|
15
|
+
from .check_tests import CheckTests
|
|
16
|
+
from .coverage_pipeline import build_coverage_pipeline
|
|
17
|
+
from .detect_drift import DetectDrift
|
|
18
|
+
from .detect_orphans import DetectOrphans
|
|
19
|
+
from .doc_check_pipeline import build_doc_check_pipeline
|
|
20
|
+
from .git_history import GitHistory
|
|
21
|
+
from .lint_pipeline import build_lint_pipeline
|
|
22
|
+
from .map_coverage import MapCoverage
|
|
23
|
+
from .report_gaps import ReportGaps
|
|
24
|
+
from .report_pipeline import build_report_pipeline
|
|
25
|
+
from .resolve_refs import ResolveRefs
|
|
26
|
+
from .scan_components import ScanComponents
|
|
27
|
+
from .scan_directory import ScanDirectory, analyze_file, classify_class
|
|
28
|
+
from .scan_docs import ScanDocs
|
|
29
|
+
from .scan_tests import ScanTests
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AssembleDocReport",
|
|
33
|
+
"AssembleReport",
|
|
34
|
+
"CheckBundle",
|
|
35
|
+
"CheckIndex",
|
|
36
|
+
"CheckNaming",
|
|
37
|
+
"CheckProtocols",
|
|
38
|
+
"CheckStructure",
|
|
39
|
+
"CheckSymbols",
|
|
40
|
+
"CheckTests",
|
|
41
|
+
"DetectDrift",
|
|
42
|
+
"DetectOrphans",
|
|
43
|
+
"GitHistory",
|
|
44
|
+
"MapCoverage",
|
|
45
|
+
"ReportGaps",
|
|
46
|
+
"ResolveRefs",
|
|
47
|
+
"ScanComponents",
|
|
48
|
+
"ScanDirectory",
|
|
49
|
+
"ScanDocs",
|
|
50
|
+
"ScanTests",
|
|
51
|
+
"analyze_file",
|
|
52
|
+
"build_coverage_pipeline",
|
|
53
|
+
"build_doc_check_pipeline",
|
|
54
|
+
"build_lint_pipeline",
|
|
55
|
+
"build_report_pipeline",
|
|
56
|
+
"classify_class",
|
|
57
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AssembleDocReport: Merge all doc-check findings into a structured report.
|
|
3
|
+
|
|
4
|
+
Combines results from ScanDocs, ResolveRefs, CheckSymbols, DetectDrift,
|
|
5
|
+
and CheckIndex into a single structured report with summary counts and
|
|
6
|
+
detail items.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from codeupipe import Payload
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AssembleDocReport:
|
|
13
|
+
"""
|
|
14
|
+
Filter (sync): Assemble the final doc-check report.
|
|
15
|
+
|
|
16
|
+
Input keys:
|
|
17
|
+
- doc_refs (list[dict]): from ScanDocs
|
|
18
|
+
- resolved_refs (list[dict]): from ResolveRefs
|
|
19
|
+
- drifted_refs (list[dict]): from DetectDrift
|
|
20
|
+
- symbol_issues (list[dict]): from CheckSymbols
|
|
21
|
+
- index_issues (list[dict]): from CheckIndex (optional)
|
|
22
|
+
|
|
23
|
+
Output keys (added):
|
|
24
|
+
- doc_report (dict): structured report with:
|
|
25
|
+
total_refs, drifted, missing_symbols, missing_files,
|
|
26
|
+
unmapped_files, status ("ok"|"stale"), details (list)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def call(self, payload: Payload) -> Payload:
|
|
30
|
+
doc_refs = payload.get("doc_refs", [])
|
|
31
|
+
resolved = payload.get("resolved_refs", [])
|
|
32
|
+
drifted = payload.get("drifted_refs", [])
|
|
33
|
+
symbol_issues = payload.get("symbol_issues", [])
|
|
34
|
+
index_issues = payload.get("index_issues", [])
|
|
35
|
+
|
|
36
|
+
missing_files = sum(1 for r in resolved if not r.get("exists", True))
|
|
37
|
+
|
|
38
|
+
details = []
|
|
39
|
+
|
|
40
|
+
for d in drifted:
|
|
41
|
+
details.append({
|
|
42
|
+
"type": "drift",
|
|
43
|
+
"file": d["file"],
|
|
44
|
+
"doc_path": d["doc_path"],
|
|
45
|
+
"line": d["line"],
|
|
46
|
+
"message": (
|
|
47
|
+
f"Hash drift: stored={d['stored_hash']}, "
|
|
48
|
+
f"current={d['current_hash']}"
|
|
49
|
+
),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
for s in symbol_issues:
|
|
53
|
+
details.append({
|
|
54
|
+
"type": "missing_symbol",
|
|
55
|
+
"file": s["file"],
|
|
56
|
+
"doc_path": s["doc_path"],
|
|
57
|
+
"line": s["line"],
|
|
58
|
+
"message": f"Symbol '{s['symbol']}' not found in {s['file']}",
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
for r in resolved:
|
|
62
|
+
if not r.get("exists", True):
|
|
63
|
+
details.append({
|
|
64
|
+
"type": "missing_file",
|
|
65
|
+
"file": r["file"],
|
|
66
|
+
"doc_path": r["doc_path"],
|
|
67
|
+
"line": r["line"],
|
|
68
|
+
"message": f"Referenced file '{r['file']}' does not exist",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
for idx in index_issues:
|
|
72
|
+
details.append({
|
|
73
|
+
"type": "unmapped_file",
|
|
74
|
+
"file": idx["file"],
|
|
75
|
+
"doc_path": "INDEX.md",
|
|
76
|
+
"line": 0,
|
|
77
|
+
"message": idx["message"],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
has_issues = (
|
|
81
|
+
len(drifted) > 0
|
|
82
|
+
or len(symbol_issues) > 0
|
|
83
|
+
or missing_files > 0
|
|
84
|
+
or len(index_issues) > 0
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
report = {
|
|
88
|
+
"total_refs": len(doc_refs),
|
|
89
|
+
"drifted": len(drifted),
|
|
90
|
+
"missing_symbols": len(symbol_issues),
|
|
91
|
+
"missing_files": missing_files,
|
|
92
|
+
"unmapped_files": len(index_issues),
|
|
93
|
+
"status": "stale" if has_issues else "ok",
|
|
94
|
+
"details": details,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return payload.insert("doc_report", report)
|