resource-pressure 0.1.0__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 (50) hide show
  1. resource_pressure-0.1.0/.github/workflows/ci.yml +34 -0
  2. resource_pressure-0.1.0/CHANGELOG.md +13 -0
  3. resource_pressure-0.1.0/LICENSE +21 -0
  4. resource_pressure-0.1.0/MANIFEST.in +10 -0
  5. resource_pressure-0.1.0/PKG-INFO +385 -0
  6. resource_pressure-0.1.0/README.md +355 -0
  7. resource_pressure-0.1.0/SECURITY.md +20 -0
  8. resource_pressure-0.1.0/docs/ARCHITECTURE.md +123 -0
  9. resource_pressure-0.1.0/docs/PACKAGING_REPORT.md +17 -0
  10. resource_pressure-0.1.0/docs/SOURCES.md +31 -0
  11. resource_pressure-0.1.0/docs/TEST_REPORT.md +74 -0
  12. resource_pressure-0.1.0/docs/test-output.txt +7 -0
  13. resource_pressure-0.1.0/docs/test-results.xml +1 -0
  14. resource_pressure-0.1.0/examples/contained_command.py +20 -0
  15. resource_pressure-0.1.0/examples/contained_dask_launcher.py +28 -0
  16. resource_pressure-0.1.0/examples/dask_local.py +24 -0
  17. resource_pressure-0.1.0/examples/simulated_demo.py +31 -0
  18. resource_pressure-0.1.0/pyproject.toml +49 -0
  19. resource_pressure-0.1.0/setup.cfg +4 -0
  20. resource_pressure-0.1.0/src/resource_pressure/__init__.py +14 -0
  21. resource_pressure-0.1.0/src/resource_pressure/__main__.py +3 -0
  22. resource_pressure-0.1.0/src/resource_pressure/backends/__init__.py +25 -0
  23. resource_pressure-0.1.0/src/resource_pressure/backends/base.py +21 -0
  24. resource_pressure-0.1.0/src/resource_pressure/backends/linux.py +146 -0
  25. resource_pressure-0.1.0/src/resource_pressure/backends/macos.py +155 -0
  26. resource_pressure-0.1.0/src/resource_pressure/backends/windows.py +103 -0
  27. resource_pressure-0.1.0/src/resource_pressure/cli.py +44 -0
  28. resource_pressure-0.1.0/src/resource_pressure/governor.py +373 -0
  29. resource_pressure-0.1.0/src/resource_pressure/integrations/__init__.py +1 -0
  30. resource_pressure-0.1.0/src/resource_pressure/integrations/dask.py +157 -0
  31. resource_pressure-0.1.0/src/resource_pressure/integrations/processkit.py +143 -0
  32. resource_pressure-0.1.0/src/resource_pressure/model.py +42 -0
  33. resource_pressure-0.1.0/src/resource_pressure/py.typed +0 -0
  34. resource_pressure-0.1.0/src/resource_pressure/testing.py +34 -0
  35. resource_pressure-0.1.0/src/resource_pressure.egg-info/PKG-INFO +385 -0
  36. resource_pressure-0.1.0/src/resource_pressure.egg-info/SOURCES.txt +48 -0
  37. resource_pressure-0.1.0/src/resource_pressure.egg-info/dependency_links.txt +1 -0
  38. resource_pressure-0.1.0/src/resource_pressure.egg-info/entry_points.txt +2 -0
  39. resource_pressure-0.1.0/src/resource_pressure.egg-info/requires.txt +15 -0
  40. resource_pressure-0.1.0/src/resource_pressure.egg-info/top_level.txt +1 -0
  41. resource_pressure-0.1.0/tests/conftest.py +31 -0
  42. resource_pressure-0.1.0/tests/test_backends.py +208 -0
  43. resource_pressure-0.1.0/tests/test_cli.py +21 -0
  44. resource_pressure-0.1.0/tests/test_containment.py +138 -0
  45. resource_pressure-0.1.0/tests/test_dask_adapter.py +160 -0
  46. resource_pressure-0.1.0/tests/test_governor.py +260 -0
  47. resource_pressure-0.1.0/tests/test_native.py +17 -0
  48. resource_pressure-0.1.0/tests/test_real_dask.py +18 -0
  49. resource_pressure-0.1.0/tests/test_real_processkit.py +18 -0
  50. resource_pressure-0.1.0/tools/build_dist.py +20 -0
@@ -0,0 +1,34 @@
1
+ name: tests
2
+ on: [push, pull_request, workflow_dispatch]
3
+ permissions:
4
+ contents: read
5
+ jobs:
6
+ unit:
7
+ strategy:
8
+ fail-fast: false
9
+ matrix:
10
+ os: [ubuntu-latest, windows-latest, macos-latest]
11
+ python: ["3.10", "3.12", "3.13", "3.14"]
12
+ runs-on: ${{ matrix.os }}
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python }}
18
+ - run: python -m pip install -e ".[dev]"
19
+ - run: python -m pytest -q -ra
20
+ - name: Native initialization and teardown (not induced pressure)
21
+ if: runner.os != 'Linux'
22
+ env:
23
+ RESOURCE_PRESSURE_NATIVE: "1"
24
+ run: python -m pytest tests/test_native.py -q -ra
25
+ - run: python -m build
26
+ dependency-compatibility:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: "3.13"
33
+ - run: python -m pip install -e ".[dev,all]"
34
+ - run: python -m pytest tests/test_real_dask.py tests/test_real_processkit.py -q -ra
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-20
4
+
5
+ Initial alpha implementation: native Windows low/high memory-resource notification
6
+ backend; Linux PSI trigger backend with explicit stall-time policy and multiple
7
+ scopes; macOS libdispatch backend with native semantic state bootstrap; sync and
8
+ async admission leases; explicit sensor/containment failures; public-API Dask
9
+ submission and bounded streaming; optional processkit containment facade; CLI,
10
+ examples, deterministic tests, and opt-in real integration smoke tests.
11
+
12
+ This source/wheel delivery is not a published PyPI release. The test report
13
+ separates local deterministic validation from native/third-party validation.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Resource Pressure contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ include LICENSE README.md CHANGELOG.md SECURITY.md
2
+ recursive-include docs *.md *.txt *.xml
3
+ recursive-include examples *.py
4
+ recursive-include tests *.py
5
+ recursive-include tools *.py
6
+ include .github/workflows/ci.yml
7
+ recursive-include src/resource_pressure py.typed
8
+ prune build
9
+ prune dist
10
+ global-exclude *.pyc *.pyo __pycache__
@@ -0,0 +1,385 @@
1
+ Metadata-Version: 2.4
2
+ Name: resource-pressure
3
+ Version: 0.1.0
4
+ Summary: Native memory-pressure signals, bounded admission, and optional kernel-backed process containment
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/adnahmed/resource-pressure
7
+ Project-URL: Repository, https://github.com/adnahmed/resource-pressure
8
+ Project-URL: Issues, https://github.com/adnahmed/resource-pressure/issues
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: containment
19
+ Requires-Dist: processkit-py<2,>=1.0; extra == "containment"
20
+ Provides-Extra: dask
21
+ Requires-Dist: distributed>=2025.1; extra == "dask"
22
+ Provides-Extra: all
23
+ Requires-Dist: processkit-py<2,>=1.0; extra == "all"
24
+ Requires-Dist: distributed>=2025.1; extra == "all"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: build>=1; extra == "dev"
28
+ Requires-Dist: ruff>=0.11; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # resource-pressure
32
+
33
+ **Native memory-pressure signals → bounded task admission → optional native process containment.**
34
+
35
+ Python 3.10+. The monitoring/admission core uses only the standard library.
36
+ No `psutil`, RAM percentages, free-memory reserves, per-worker memory estimates,
37
+ or adaptive thread-pool algorithm. No GLib, PyGObject, MSYS2, or compiler is
38
+ needed for the core wheel.
39
+
40
+ **Version 0.1.0 is an alpha implementation, not a production-validated release.**
41
+ The supplied source and wheel have not been published to PyPI. Use the local
42
+ paths below; the distribution name is not a claim of ownership of a PyPI name.
43
+ See `docs/TEST_REPORT.md` for what actually ran and what remains unverified.
44
+
45
+ ## Install
46
+
47
+ Core:
48
+
49
+ ```bash
50
+ pip install resource-pressure
51
+ ```
52
+ or:
53
+ ```bash
54
+ uv add resource-pressure
55
+ ```
56
+ With Dask + process containment:
57
+
58
+ ```bash
59
+ pip install "resource-pressure[all]"
60
+ ```
61
+ or:
62
+
63
+ ```bash
64
+ uv add "resource-pressure[all]"
65
+ ```
66
+
67
+ After extracting the archive, from its parent directory:
68
+
69
+ ```bash
70
+ # Core native sensors and admission.
71
+ uv add ./resource-pressure
72
+
73
+ # Also install Dask and the processkit containment adapter dependencies.
74
+ uv add ./resource-pressure --extra all
75
+ ```
76
+
77
+ For an editable source checkout:
78
+
79
+ ```bash
80
+ uv add --editable ./resource-pressure --extra all
81
+ ```
82
+
83
+ Or install the supplied universal core wheel:
84
+
85
+ ```bash
86
+ python -m pip install ./resource_pressure-0.1.0-py3-none-any.whl
87
+ ```
88
+
89
+ The wheel itself has no mandatory runtime dependencies. The optional
90
+ `containment` extra uses `processkit-py>=1.0,<2`; its own native wheel availability
91
+ and platform restrictions still apply. The `dask` extra installs `distributed`.
92
+
93
+ ## The API
94
+
95
+ ```python
96
+ import asyncio
97
+ from resource_pressure import PressureGovernor
98
+
99
+
100
+ async def main():
101
+ async with PressureGovernor.auto(max_in_flight=4) as governor:
102
+ async with governor.slot():
103
+ await perform_one_heavy_operation()
104
+
105
+
106
+ # asyncio.run(main()) # Define your own perform_one_heavy_operation first.
107
+ ```
108
+
109
+ Synchronous code uses the same context-manager surface:
110
+
111
+ ```python
112
+ from resource_pressure import PressureGovernor
113
+
114
+ with PressureGovernor.auto(max_in_flight=4) as governor:
115
+ with governor.slot(timeout=30):
116
+ result = perform_one_heavy_operation()
117
+ ```
118
+
119
+ A runnable simulation with no native prerequisites is included:
120
+
121
+ ```bash
122
+ uv run python examples/simulated_demo.py
123
+ ```
124
+
125
+ `max_in_flight` is a fixed ceiling on admitted operations, **not** a RAM threshold.
126
+ When omitted it defaults to the available process CPU count (or CPU count on
127
+ older Python). It is an operational concurrency limit, not a safe-memory estimate.
128
+
129
+ The admission rule is intentionally small:
130
+
131
+ ```text
132
+ UNKNOWN → admit nothing while initializing
133
+ NORMAL → admit up to max_in_flight operations
134
+ PRESSURED → admit nothing new; existing operations finish
135
+ CRITICAL → admit nothing new; existing operations finish
136
+ sensor error / closed governor → wake waiters and raise
137
+ ```
138
+
139
+ Recovery reopens admission to the same ceiling. The package does not kill,
140
+ suspend, shrink, or restart already-running tasks on a pressure transition.
141
+ It does not calculate an optimal worker count.
142
+
143
+ For observation without reserving a slot:
144
+
145
+ ```python
146
+ await governor.wait_until_safe()
147
+ # Or governor.wait_until_safe_sync() in blocking code.
148
+ ```
149
+
150
+ **That call is a state check, not a reservation.** Use `slot()` or
151
+ `acquire()`/`acquire_sync()` when admitting work. Hold a lease until the work
152
+ finishes; releasing immediately after submission defeats the bound.
153
+
154
+ ## What each platform actually does
155
+
156
+ | Platform | Pressure sensor | Native states and mapping | Containment extra |
157
+ |---|---|---|---|
158
+ | Windows | `CreateMemoryResourceNotification`, query, native wait | High → NORMAL; low → PRESSURED; no fabricated CRITICAL tier | Job Objects through processkit |
159
+ | Linux | PSI trigger registration and `poll(POLLPRI)` | Library maps `some` to PRESSURED and `full` to CRITICAL | cgroup v2 through processkit, when available |
160
+ | macOS | libdispatch memory-pressure source | Native normal/warn/critical → NORMAL/PRESSURED/CRITICAL | POSIX process groups only, explicit opt-in; no whole-tree memory cap |
161
+
162
+ OS details are documented in the primary references in `docs/SOURCES.md`.
163
+ The levels are useful common labels, **not calibrated equivalents across OSes**.
164
+
165
+ ### Linux: honest policy, not pretend OS severity
166
+
167
+ PSI reports memory-related execution stalls and accepts user-specified stall-time
168
+ triggers. Linux does **not** label these as NORMAL/PRESSURED/CRITICAL for us.
169
+ The library registers these defaults, on separate file descriptors:
170
+
171
+ ```text
172
+ some 150000 2000000
173
+ full 50000 2000000
174
+ ```
175
+
176
+ These are **library-selected defaults**: 150 ms of some-stall or 50 ms of full-stall
177
+ within a two-second window. They are not percentages of RAM and are not asserted
178
+ to be optimal for every workload. PSI has no recovery event; this implementation
179
+ infers recovery after four seconds without either trigger. CRITICAL is retained
180
+ until its own quiet interval expires, even if less severe events arrive.
181
+ Initial admission waits through one observation window.
182
+
183
+ Application code needs no RAM policy, but the underlying Linux policy cannot be
184
+ eliminated. It is explicit and configurable:
185
+
186
+ ```python
187
+ from resource_pressure import PSIConfig, PressureGovernor
188
+
189
+ governor = PressureGovernor.auto(
190
+ psi=PSIConfig(
191
+ some_stall_us=150_000, full_stall_us=50_000, window_us=2_000_000, quiet_seconds=4.0
192
+ )
193
+ )
194
+ ```
195
+
196
+ Default scope is `/proc/pressure/memory` only. To monitor a deployment-provided
197
+ cgroup and the host, explicitly supply both paths:
198
+
199
+ ```python
200
+ governor = PressureGovernor.auto(
201
+ psi_paths=[
202
+ "/proc/pressure/memory",
203
+ "/sys/fs/cgroup/YOUR_DELEGATED_GROUP/memory.pressure",
204
+ ]
205
+ )
206
+ ```
207
+
208
+ All requested paths must support writable PSI triggers. Merely reading PSI
209
+ counters is insufficient for this event-only backend. Missing PSI, read-only
210
+ mounts, permission failures, or disappearing cgroups raise errors; there is no
211
+ fallback to polling RAM or silently dropping a requested scope. Two-second
212
+ window multiples satisfy the kernel's unprivileged window rule, but do not grant
213
+ filesystem permissions. Administrators should provision access rather than
214
+ running an entire application as root just for the sensor.
215
+
216
+ **A system-wide signal does not guarantee warning before a cgroup-specific
217
+ memory limit is hit.** Per-cgroup sensing is explicit; the processkit adapter
218
+ does not discover or export its private cgroup path into the sensor automatically.
219
+
220
+ ### Windows: use the OS's decision
221
+
222
+ The backend waits for low-memory while NORMAL, and for high-memory after
223
+ pressure. It does not wait repeatedly on an already-signalled handle. The
224
+ neutral region where neither object is signalled retains the previous decision;
225
+ at startup, a neutral state conservatively blocks until high is observed.
226
+ Timeouts only allow orderly thread shutdown; they do not sample RAM.
227
+
228
+ ### macOS: native events, with a bootstrap qualification
229
+
230
+ libdispatch supplies normal, warning, and critical events. A native semantic
231
+ sysctl, `kern.memorystatus_vm_pressure_level`, bootstraps initial state and resolves
232
+ coalesced events whose ordering is ambiguous. This sysctl exists in Apple's
233
+ published XNU implementation, but is **not a promised stable public API**.
234
+ Failure to read it makes startup fail explicitly. The ctypes callback lifetime,
235
+ cancellation, and queue-barrier cleanup still require actual macOS validation.
236
+
237
+ ## Dask
238
+
239
+ Keep admission in the **producer**, before submitting independent heavy tasks.
240
+ The supplied adapter uses public `Client.submit` and Future APIs, not scheduler
241
+ internals or worker monkey-patches. Keep Dask's existing memory protections.
242
+
243
+ ```python
244
+ from contextlib import closing
245
+ from distributed import Client, LocalCluster
246
+ from resource_pressure import PressureGovernor
247
+ from resource_pressure.integrations.dask import DaskAdmission
248
+
249
+
250
+ def heavy_task(item):
251
+ return item * item
252
+
253
+
254
+ def main():
255
+ with LocalCluster(n_workers=4, threads_per_worker=1) as cluster:
256
+ with Client(cluster) as client, PressureGovernor.auto(max_in_flight=4) as governor:
257
+ admitted = DaskAdmission(client, governor)
258
+ with closing(admitted.map_unordered(heavy_task, range(100))) as results:
259
+ for result in results:
260
+ print(result)
261
+
262
+
263
+ if __name__ == "__main__":
264
+ main()
265
+ ```
266
+
267
+ `map_unordered` is the recommended bounded streaming interface. It retains at
268
+ most `max_in_flight` submitted futures plus one input lookahead, drains completed
269
+ results even during pressure, and releases futures as results are consumed.
270
+ Closing the iterator cancels/releases the remaining futures and releases leases.
271
+ Large results retained by your application are still your application's memory.
272
+ Input iteration should be cheap; one lookahead can happen while admission is shut.
273
+
274
+ `submit()` and `await asubmit()` return ordinary Dask futures. Their logical lease
275
+ lasts until success, failure, or cancellation. They bound unfinished admitted
276
+ futures, **not** an ever-growing list of completed results retained by the caller.
277
+ Use the streaming API rather than building a giant list of futures.
278
+
279
+ Important boundaries:
280
+
281
+ - This sensor observes the producer's machine/configured PSI scopes, not every
282
+ remote worker. Multi-host global pressure aggregation is not implemented.
283
+ - It does not resize Dask workers, gate existing queued graphs, cover submissions
284
+ through another client, or govern arbitrary dependent graph nodes.
285
+ - Future cancellation does not guarantee that already-running Python code stops.
286
+ Thus the bound is on logical admissions, not an inviolable physical-work count
287
+ after cancellation or worker failure.
288
+ - A completed remote task can retain data. A pressure gate is not proof of freed
289
+ RAM; release results and keep Dask's spill/pause/termination mechanisms enabled.
290
+
291
+ Never put a blocking governor inside every Dask worker task indiscriminately;
292
+ blocked workers can occupy the very slots needed by dependencies that would
293
+ finish and free memory.
294
+
295
+ ## Unified containment
296
+
297
+ The same governor can create a pressure-gated process runner:
298
+
299
+ ```python
300
+ import sys
301
+ from resource_pressure import PressureGovernor
302
+
303
+ with PressureGovernor.auto(max_in_flight=2) as governor:
304
+ with governor.process_group(max_memory=512 * 1024 * 1024) as children:
305
+ print(children.mechanism) # must be job_object or cgroup_v2 by default
306
+ result = children.run(sys.executable, ["-c", "print('hello')"], timeout=20)
307
+ print(result.stdout)
308
+ ```
309
+
310
+ `run()`/`await arun()` acquire admission before process execution and release it
311
+ when the command's output operation completes. They return a
312
+ `processkit.ProcessResult`; nonzero exit codes are data and should be checked.
313
+ Captured output is bounded to 1 MiB by default (`output_limit=` changes this).
314
+ Arguments are passed as a sequence, never implicitly through a shell.
315
+
316
+ `max_memory` is an **optional, explicit deployment budget** for the whole group,
317
+ not automatically chosen from system RAM. Omit it for lifecycle containment
318
+ without a memory cap. `max_processes` is also optional. Fixed limits are independent
319
+ of pressure signals; no automatic `memory.high` tuning is performed.
320
+
321
+ The adapter rejects process-group fallback unless
322
+ `allow_process_group_fallback=True` is explicit. Such fallback cannot enforce the
323
+ requested whole-tree memory/process limits. It is not a security sandbox, and
324
+ session-changing descendants can escape POSIX process groups.
325
+
326
+ **Only children started through this runner are covered.** Merely constructing a
327
+ group does not contain your existing process, browser, or Dask cluster.
328
+ To contain Dask's hierarchy, launch the supervisor inside a group **before** it
329
+ creates workers. `examples/contained_dask_launcher.py` demonstrates that outer
330
+ boundary; its child runs `examples/dask_local.py` with per-task admission.
331
+ Already-running workers are not adopted automatically. Finish all concurrent
332
+ `run`/`arun` calls before leaving the group context.
333
+
334
+ A root command can exit while descendants remain; the shared group's final
335
+ context exit handles its owned tree. An admission lease does not count every
336
+ remaining descendant. Teardown and abrupt-parent-death guarantees are those of
337
+ the reported processkit mechanism; do not assume Linux/macOS match Windows.
338
+
339
+ ## Diagnostics and events
340
+
341
+ ```bash
342
+ uv run resource-pressure doctor
343
+ uv run resource-pressure watch
344
+ uv run resource-pressure doctor --psi-path /path/to/delegated/memory.pressure
345
+ ```
346
+
347
+ `doctor` registers the actual native monitor, reports its backend and initial
348
+ state, and closes it. It does not claim to test containment or induce pressure.
349
+ Missing/failed native support exits with code 2, not a fake NORMAL status.
350
+
351
+ ```python
352
+ unsubscribe = governor.subscribe(lambda event: print(event.level.name, event.backend, event.reason))
353
+ # Later: unsubscribe()
354
+ ```
355
+
356
+ Subscribers run on the monitor thread and must be short/nonblocking. Exceptions
357
+ are logged and isolated. Use `loop.call_soon_threadsafe` to notify an asyncio
358
+ loop; do not perform expensive memory reclamation inside the callback.
359
+ Subscribers receive level transitions, not a durable audit log. Call
360
+ `check_health()` for explicit status; admission methods automatically check it.
361
+
362
+ ## Development and validation
363
+
364
+ ```bash
365
+ uv sync --extra dev
366
+ uv run pytest -q -ra
367
+ uv build
368
+ ```
369
+
370
+ Or with an existing Python environment:
371
+
372
+ ```bash
373
+ python -m pip install -e ".[dev]"
374
+ python -m pytest -q -ra
375
+ python -m build
376
+ ```
377
+
378
+ The included CI workflow defines Linux, Windows, and macOS jobs. It has **not**
379
+ been run on hosted CI as part of this delivery. Real sensor tests require
380
+ `RESOURCE_PRESSURE_NATIVE=1`; dependency tests run only when their extras are
381
+ installed. No automated test deliberately exhausts host memory.
382
+
383
+ Read `docs/ARCHITECTURE.md`, `docs/TEST_REPORT.md`, and `docs/SOURCES.md` before
384
+ production deployment. This is a pressure-responsive admission library, **not an
385
+ OOM-proof memory manager or a security boundary**.