The fix in 644d88ab changed the recovery path from client.delete_collection
to shutil.rmtree (chromadb 1.5.x delete_collection is broken on corrupted
state). The test still asserted the old behavior.
The bug: when the local embedding provider fails to initialize
(e.g. sentence-transformers not installed), RAGEngine.__init__
leaves self.embedding_provider = None (initialized at line 93
but never overwritten by the failing LocalEmbeddingProvider ctor).
The constructor returns. _sync_rag_engine's else branch then
sets status to 'ready' - a lie. The RAG panel shows 'ready'.
The user triggers a retrieval. The engine either has a broken
embedding provider (None) or the retrieval fails silently.
The RAG context never appears in the AI's history.
The fix: in _sync_rag_engine's _task, after RAGEngine(...)
returns, check if engine.embedding_provider is None. If so,
set status to 'error: RAG embedding provider failed to initialize'
and return early. This prevents:
- The engine from being assigned to self.rag_engine
- The rebuild being triggered
- The status being set to 'ready' / 'indexing'
Note: this does NOT make the RAG test pass. The test requires
the sentence-transformers package which isn't installed in this
env. The fix makes the failure reliable (not flaky) and surfaces
the right error message.
TDD: 3 tests added in tests/test_rag_engine_ready_status_bug.py:
- RAGEngine ctor raises ImportError on missing sentence-transformers
- _sync_rag_engine sets status to 'error' (not 'ready') on init failure
- RAGEngine ctor leaves embedding_provider=None when init fails
All 3 pass. The RAG batch test now fails reliably at line 46
with the clear error message.
PR1 follow-up (the actual IM_ASSERT root cause fix).
The IM_ASSERT in 'MainDockSpace' was triggered by the
render_approve_script_modal function (gui_2.py:4895) calling
imgui.checkbox with a None value for app.ui_approve_modal_preview.
The chain of bugs:
1. AppController.__getattr__ returned None for ANY ui_ attribute
(line 1237-1238). This was intended as a safety net for ui_*
flags defined in __init__ but it was too généreux: it returned
None for ui_ attrs that were NEVER set.
2. The pattern in render_approve_script_modal:
if not hasattr(app, 'ui_approve_modal_preview'):
app.ui_approve_modal_preview = False
_, app.ui_approve_modal_preview = imgui.checkbox(..., app.ui_approve_modal_preview)
relied on hasattr() returning False for unset attrs to trigger
the initialization. But the App.__setattr__ checks
hasattr(self.controller, name) to decide where to route
assignments. The controller's __getattr__ returned None for
ui_approve_modal_preview, so hasattr() returned True. The
App.__setattr__ routed the assignment to the controller.
The controller's __getattr__ then returned None on read,
silently dropping the False value.
3. The next line called imgui.checkbox with None, which raised
a TypeError. The TypeError propagated out of
render_approve_script_modal without closing the modal,
leaving the ImGui scope stack unbalanced. The unbalanced
scope triggered IM_ASSERT(Missing End()) on the next frame.
Fix: AppController.__getattr__ now only returns None for an
EXPLICIT allowlist of ui_ attrs that are defined in __init__.
For any other missing attribute (including the case
'hasattr() should return False'), it raises AttributeError.
The App.__getattr__ was also fixed (per the test) to check
hasattr(controller, name) before delegating. This is defense in
depth in case other __getattr__ patterns are added.
Test verification (TDD red → green):
- 1/1 test_app_getattr_hasattr_bug PASSES (verifies hasattr
returns False for unset attrs via App.__getattr__)
- 1/1 test_app_controller_getattr_ui_bug PASSES (verifies hasattr
returns False for unset ui_ attrs on controller)
Live verification:
- 4 sims + test_live_workflow + 2 markdown tests: 7/7 PASS in 83.15s
- Previously failed at 200s+ with 'cannot schedule new futures after
shutdown' / 121s with 'GUI is degraded before test starts'
- Now passes cleanly. The IM_ASSERT no longer fires.
13/13 related unit tests pass (app_controller_* + app_run_* +
app_getattr_*). No regressions in 51/51 io_pool/warmup/sigint/etc.
unit tests.
PR3 of the test_full_live_workflow_imgui_assert fix sequence.
When a prior live_gui test in the same session crashes the GUI (e.g.
via an ImGui IM_ASSERT from cumulative panel state), the controller's
_io_pool gets shut down. The next test starts in a degraded state
but only discovers this 120s later when its project switch times
out with a confusing 'cannot schedule new futures after shutdown'
error.
This commit adds a /api/gui_health pre-flight check at the start of
test_full_live_workflow. If the GUI is degraded, the test fails
fast (within 1s) with a clear, actionable message that includes:
- The exact RuntimeError that caused the degradation
- The full traceback of the last ImGui scope mismatch
- A note that the new test cannot proceed with a dirty state
Per user feedback 2026-06-08: 'I don't want a batch to be too fragile
where I can't restart the app and continue with the next test file
if it fails. Just has to note that the new file didn't get to deal
with a dirty state.'
Also includes the planning documents written earlier in this session:
- TODO_test_full_live_workflow_v2.md (task list)
- test_full_live_workflow_imgui_assert_20260608.md (root cause report)
- test_full_live_workflow_propagation_digest_20260608.md (solutions digest)
- batch_resilience_plan_20260608.md (batch resilience plan)
Verification:
- test_full_live_workflow in isolation: 13.45s PASS (health=True, no degrade)
- 4 sims + test_full_live_workflow in batch: 76.46s (1 FAIL fast, 4 sims PASS)
- Without PR3 fix: 200s FAIL with confusing 120s timeout
- With PR3 fix: 76s FAIL with clear 'GUI is degraded' message
- The fast-fail is observable, not silent (per user's 'wrap might be
worth it if that properly lets us handle the assert')
PR2 of the test_full_live_workflow_imgui_assert fix sequence.
When an ImGui scope mismatch (IM_ASSERT(Missing End())) fires in
immapp.run (e.g. after cumulative state corruption from prior sims'
panel renders), the RuntimeError propagates out of app.run(). The
controller's _io_pool gets shut down via __del__/finalization. The
hook server (separate ThreadingHTTPServer) survives. Subsequent test
clicks fail with 'cannot schedule new futures after shutdown' and
the test times out after 120s with no clear signal of what went
wrong.
This commit:
1. Wraps immapp.run in try/except RuntimeError in gui_2.py:618.
On assertion: logs the error to stderr (NOT silent), records
it on controller._gui_degraded_reason and _last_imgui_assert,
and returns from run() so the hook server keeps serving.
2. Adds _gui_degraded_reason and _last_imgui_assert to
AppController.__init__ (initialized to None).
3. Adds /api/gui_health endpoint in api_hooks.py:148. Returns
{healthy, degraded_reason, last_assert, io_pool_alive}.
4. Adds ApiHookClient.get_gui_health() with the matching unit
tests (3 mocked tests + 1 live test).
Per user feedback 2026-06-08:
- The wrap does NOT silently swallow the error. It logs at ERROR
level and surfaces it via the health endpoint.
- Tests can call client.get_gui_health() to detect a degraded GUI
and fail fast with a clear message.
TDD: tests written first, confirmed to fail, then fix applied.
34/34 unit tests pass. 1/1 live test passes (live_gui health
endpoint reports healthy=True on fresh subprocess).
The 30s wait_for_project_switch timeout was an excessive constraint.
In batch context, prior sims' AI discussion turn workers saturate the
8-worker io_pool, queueing this switch for tens of seconds. The other
defensive waits in the test (warmup 60s, prior switch 60s) already use
60s+, so 30s was the inconsistent outlier.
User confirmed: 'I think not completing in 30s is an excessive constraint
if thats whats going on.'
Verification:
- test_full_live_workflow isolation: 11.69s PASS
- 7-test batch (test_full_live_workflow + 4 extended sims + 2 markdown): 85.83s PASS
Root cause: test_full_live_workflow in batch context (with prior sims
running AI discussion turns) would queue its _do_project_switch behind
the auto-pruner's scan of tests/logs/ (154MB, 6519 files). The 4-worker
pool was saturated, so the switch would never run within 30s.
Fix: bump IO_POOL_MAX_WORKERS from 4 to 8. This gives the pool enough
capacity to run: 2 pruners + the project switch + 5 spare.
Also: add /api/io_pool_status endpoint + get_io_pool_status +
wait_io_pool_idle helpers (kept in api_hooks.py and api_hook_client.py
for the test_api_hook_client_io_pool.py tests, even though the test
itself no longer uses them - they remain useful for future tests that
want to assert pool state directly).
Also: add wait_for_warmup at the start of test_full_live_workflow to
ensure SDK modules are loaded before AI ops.
Test verification:
- test_full_live_workflow in isolation: 11.83s PASS
- test_full_live_workflow in batch (with 4 prior sims): 83.46s PASS
- 30/30 related unit tests PASS
When a prior test in the tier-3-live_gui batch leaves a _do_project_switch
background thread running, the next test's btn_project_new_automated click
sees _project_switch_in_progress=True (from the prior thread) and queues
the new path via _project_switch_pending_path. The queued switch is never
actually submitted to the io_pool, so is_project_stale() stays True and
AI ops (_handle_generate_send) bail with 'project switch in progress;
AI ops disabled'.
Fix: _handle_reset_session now also clears _project_switch_in_progress,
_project_switch_pending_path, and _project_switch_error (under the
existing _project_switch_lock). This way, even if the prior background
thread is still running, the controller reports an idle state and the
new switch can be submitted normally.
Also:
- src/api_hook_client.py: reverted wait_for_project_switch to require
in_progress=False (was relaxed to return on queued path, which misled
the caller into thinking the switch was done)
- tests/test_handle_reset_session_clears_project.py: new test
test_handle_reset_session_clears_project_switch_state asserts
is_project_stale() returns False after reset
- tests/test_api_hook_client_wait_for_project_switch.py: updated
test_wait_for_project_switch_does_not_return_on_queued (in_progress
+ matching path should keep waiting, not return early)
- tests/test_live_workflow.py: added pre-wait for any in-flight switch
before doing btn_reset (so the test waits up to 60s for the prior
switch to complete if needed)
- conductor/todos/TODO_test_full_live_workflow.md: updated Task 4 with
the deeper hang analysis and recommended fix
Known follow-up: test_full_live_workflow still hangs in tier-3 batch
even with this fix, because the new _do_project_switch itself is hung
in the io_pool (likely saturation from prior sims' AI discussion turn
workers). Deeper investigation required.
Replaces the 10x1s blind poll of derived state with a condition-based
wait on /api/project_switch_status. Also adds a defensive file existence
check that fails fast (within 5s) if the click was dropped or the
project creation handler crashed.
The new wait surfaces a clear error message ('Project switch did not
complete in 30s. Last status: ...') instead of the generic 'Project
failed to activate', and exposes _project_switch_error if the controller
reported one.
- tests/test_live_workflow.py: replaced poll loop (lines 57-65) with
wait_for_project_switch + os.path.exists defensive check
Adds a polling helper that blocks until the project switch completes,
errors out, or times out. Replaces the fragile 10x1s blind poll in
test_full_live_workflow with a condition-based wait on the
/api/project_switch_status endpoint.
Features:
- Polls /api/project_switch_status every 200ms (configurable)
- Returns immediately on error (with the error in the result)
- Path matching: exact match OR basename match (handles absolute vs relative)
- Times out with a clear 'timeout' flag instead of a generic assertion
- Optional expected_path: if None, returns on any in_progress=False
- src/api_hook_client.py: new wait_for_project_switch method (37 lines)
- tests/test_api_hook_client_wait_for_project_switch.py: 6 unit tests
with mocked _make_request covering all paths
Task 2 (_handle_reset_session reset) introduced a regression: setting self.active_project_path to empty caused an infinite re-switch loop in _do_project_switch because _flush_to_project writes to active_project_path (raises OSError on empty path), and the finally block re-submitted the failed switch on every iteration. Result: test_context_sim_live saw switching-to status for 5+ seconds and MD-only generation was blocked.
Fix: keep self.active_project_path as-is in _handle_reset_session. Only reset self.project (to a fresh default_project dict) and self.project_paths (to empty list). The stale project state issue is solved by replacing the project dict; the active_project_path stays valid for _flush_to_project.
- src/app_controller.py: refined _handle_reset_session project reset
- tests/test_handle_reset_session_clears_project.py: updated contract test to assert active_project_path is preserved
Stale project state from prior live_gui tests (shared session-scoped
subprocess) was leaking into subsequent tests, causing the
test_full_live_workflow race condition: 'Project not switched' errors
when self.project still claimed to be a different project.
The fix: _handle_reset_session now mirrors the default-project branch
of __init__ (lines 1743-1745), creating a fresh default project dict,
clearing active_project_path and project_paths, and reinitializing
the workspace manager.
- src/app_controller.py: 6 new lines in _handle_reset_session
- tests/test_handle_reset_session_clears_project.py: 3 tests
(active_project_path, project_paths, self.project)
Adds a new endpoint that exposes the project-switch state machine so tests
can poll for completion instead of guessing with timeouts.
- AppController: track _project_switch_error on failure paths
- src/api_hooks.py: GET /api/project_switch_status returns
{in_progress, pending_path, active_path, error}
- src/api_hook_client.py: get_project_switch_status() helper
- tests/test_api_hooks_project_switch.py: 3 unit tests for client + endpoint
shape, 1 live_gui test for the default-idle case
The repo-root manualslop_layout.ini references pre-hub-refactor
window names that no longer exist in the current code
(Projects/Files/Screenshots/Provider/System Prompts/etc.).
HelloImGui silently drops unknown windows when loading the
layout, causing "missing panels" in live_gui tests and in the
user's interactive session.
The previous "Preserve GUI layout for tests" block copied the
stale repo-root layout into the live_gui workspace, infecting
every live_gui test session with stale state.
Fix: skip the copy. HelloImui will generate a fresh layout in
the test workspace on shutdown, which then lives in the
session-scoped workspace and is cleaned up at teardown.
The repo-root manualslop_layout.ini is still TRACKED (I did
not delete it; that's the user's call). They can:
- Delete it manually, or
- Run the existing "Reset Layout" command from the Command Palette
(which deletes both repo-root and live_gui_workspace paths and
forces HelloImGui to regenerate with the current window catalog).
Verified: 6/6 targeted tests pass.
Four test files had patches/monkeypatches that referenced the
removed src.models.load_config or src.models.CONFIG_PATH module
constant. These all stem from the config I/O refactor (commit
7bcb5a8c) that renamed load_config/save_config to private I/O
primitives.
- tests/test_external_editor_gui.py: 2 sites changed from
monkeypatch.setattr(models_module, 'load_config', ...) to
monkeypatch.setattr('src.app_controller.AppController.load_config', ...)
- tests/test_external_mcp_e2e.py: CONFIG_PATH monkeypatch changed
to SLOP_CONFIG env var (the only supported override path)
- tests/test_log_management_ui.py: same CONFIG_PATH -> SLOP_CONFIG fix
- tests/test_gen_send_empty_context.py: _StubController now receives
ui_selected_context_files and _pending_generation_action from the
app_instance BEFORE being assigned as controller (App.__getattr__
delegates to controller, so attrs must be on the stub first)
Also: deleted tests/artifacts/manualslop_layout.ini (gitignored
stale file from March 4 referencing pre-refactor window names like
"Projects"/"Files"/"Screenshots" that no longer exist in the code).
Repo-root manualslop_layout.ini still references the same old
window names; user should run the existing "Reset Layout" command
(or delete it manually) to regenerate with the current window
catalog (Context Hub / AI Settings Hub / Discussion Hub / etc.).
Verified: 13 targeted tests pass:
- test_external_editor_gui.py (5/5)
- test_external_mcp_e2e.py (1/1)
- test_log_management_ui.py (2/2)
- test_gen_send_empty_context.py (5/5)
Eliminates 22 call sites that bypassed the AppController state owner
and read/wrote config.toml directly. AppController is now the single
source of truth for self.config; gui_2.py, commands.py, etc. go
through controller.save_config() / controller.load_config().
Production changes:
- src/models.py: rename load_config -> _load_config_from_disk,
save_config -> _save_config_to_disk (private I/O primitives)
- src/app_controller.py: add public load_config()/save_config() methods
that own the state. Update 3 internal call sites and 3 ConductorEngine
call sites to pass max_workers from self.config
- src/multi_agent_conductor.py: ConductorEngine.__init__ now takes
max_workers as a parameter (caller responsibility, not I/O primitive)
- src/external_editor.py: get_default_launcher() takes config as a
parameter; gui_2.py:1311,4776 pass app.config
- src/gui_2.py: 17 sites of models.save_config(X.config) replaced with
X.save_config() (delegates via __getattr__ to controller)
- src/commands.py: save_all() uses app.save_config()
Test changes (route through controller, not I/O primitive):
- tests/conftest.py: mock_app and app_instance fixtures now patch
AppController.load_config/save_config instead of models I/O primitives
- 18 other test files: patches renamed from models._save_config_to_disk
to AppController.save_config (and same for load_config)
- tests/test_app_controller_mcp.py: use SLOP_CONFIG env var instead of
patching removed CONFIG_PATH module constant
- tests/test_parallel_execution.py: pass max_workers=2 explicitly to
ConductorEngine (caller no longer reads config)
- tests/test_gui_paths.py: add save_config=MagicMock() to MockApp;
assert on controller method, not I/O primitive
- tests/test_models_no_top_level_tomli_w.py: still calls private
_save_config_to_disk directly (the only allowed exception; tests
the lazy-load behavior of the primitive itself)
New files:
- scripts/audit_no_models_config_io.py: enforces the rule (--strict,
--json modes; AST-based docstring detection to avoid false positives)
- conductor/code_styleguides/config_state_owner.md: documents the rule
Verification:
- 67 targeted tests pass
- scripts/audit_no_models_config_io.py --strict returns 0
This is the architectural cleanup that surfaced during the
audit_architectural_cheats_20260607 review. Closes the smoke-gun
CONFIG_PATH module constant (already done in 0c7ebf22) AND the
free-function models.load_config/save_config smell.
[conductor(checkpoint): config-iO-refactor-20260607]
ROOT CAUSE: src/models.py had `CONFIG_PATH = get_config_path()`
at module level. Every test that imported `src.models` and called
`save_config()` or `load_config()` wrote/read the repo-root
`config.toml` via this cached constant. The path was resolved
once at import time, so the SLOP_CONFIG env var (or test
fixtures) couldn't redirect reads/writes without reimporting the
module.
This silently corrupted the user's config.toml on every test
run. The diff between runs showed: 'config.toml changed in
working copy' — caused by tests, not the user.
FIX: remove the module-level constant; call get_config_path()
on every read/write call. SLOP_CONFIG (and any test-time
set_config_path() helper) now works without reimport.
Also: keep my prior commits to this file (reset_layout command
in src/commands.py; the RUN_MMA_INTEGRATION skipif in
test_mma_step_mode_sim.py) bundled here for a clean atomic
fix-pack since the user just fixed the indentation issue I had.
Verified: src.models imports cleanly; load_config/save_config
work as expected. Tests that import these functions will
use whatever SLOP_CONFIG points to (or the repo-root default).
The test had a pre-existing race: it monkeypatched
_rebuild_rag_index and _flush_to_project to no-ops, which made
_do_project_switch complete synchronously inside the io_pool
worker. By the time the test's _api_generate call ran
is_project_stale() was already False (the worker had cleared
_project_switch_in_progress), so the 409 contract was never
exercised.
Fix: replace the no-op lambdas with `lambda: time.sleep(0.5)`.
This keeps the worker busy for 500ms, which is more than enough
window for the test to call _api_generate and observe the
stale flag. _wait_for_switch then drains the rest of the work.
Also: removed the @pytest.mark.skip marker; the underlying issue
is now fixed in the test.
Verified: 9/9 in tests/test_project_switch_persona_preset.py pass
(previously 8 passed + 1 skipped).
The Hook API previously rejected key strings like
'show_windows["Project Settings"]' (and silently returned None on
get). The test_live_gui_filedialog_regression test exercises exactly
this pattern to open the Project Settings window via the Hook API;
it was previously marked skip with "hook server doesn't handle the
dict-key bracket-notation syntax".
Fix in three small places:
1. src/app_controller.py:_handle_set_value
If `item` is not in _settable_fields, try parsing it as
`dict_name[<key>]` notation. If dict_name IS in _settable_fields
and the current attr is a dict, set the inner key.
2. src/api_hooks.py:/api/gui/value (POST get_val)
Mirror the parsing for the field-based get endpoint.
3. src/api_hook_client.py:ApiHookClient.get_value
Mirror the parsing in the client so the dict-key syntax works
through the state endpoint as well (which is what get_value
actually calls by default).
Test fix:
- tests/test_live_gui_filedialog_regression.py: removed the
@pytest.mark.skip marker; the underlying issue is now fixed.
Verified: 1/1 test passes (previously skipped).
WarmupManager._record_success and _record_failure used to set
self._done_event.set() inside the with self._lock: block, BEFORE
calling the user-registered on_complete callbacks. This created
a race: a test thread calling mgr.wait() could observe
mgr.is_done() == True and proceed before the worker thread had
finished firing the callbacks. The mgr.on_complete caller would
then assert on state that the callback was supposed to mutate
(e.g. test_warmup_on_complete_callback_fires' `received` list).
Fix: move self._done_event.set() to AFTER the for cb in callbacks:
loop in both _record_success and _record_failure. The done event
is now set last, so wait() cannot return until all callbacks
have completed (or raised, which is swallowed by the try/except).
ALSO fix the previously-corrupted state of warmup.py (the result
of a misused set_file_slice edit that left orphaned code with no
def line for _record_failure). _record_failure is now a proper
class method with the def line restored.
ALSO fix tests/test_warmup.py:
- test_warmup_on_complete_callback_fires: the test body was
missing the pool/mgr setup. Added the missing lines.
- test_warmup_done_event_set_after_all_complete: removed the
racy `assert not mgr.is_done()` assertion that fires
immediately after submit. On a fast machine, os/sys warmup
completes in microseconds, so is_done() is already True
by the time the assertion runs. The remaining assertion
(`assert mgr.is_done()` after wait) still tests the
semantic that the done event is set after completion.
- Removed both `@pytest.mark.skip` markers; the underlying
issues are now fixed in production code AND the tests.
Verified: 10/10 tests in tests/test_warmup.py pass (previously
2 skipped, 2 failed).
test_gui_events_v2::test_handle_generate_send_pushes_event was
patches 'threading.Thread' but production code in
src/app_controller.py:_handle_generate_send uses
self._io_pool.submit_io(worker) (an AppController method, NOT a
method on the ThreadPoolExecutor). The test never got to its
assertions because the patched attribute was never called.
Fix: update the test to patch `mock_gui.controller.submit_io`
(the AppController method). The `with patch.object(...)` block
replaces submit_io with a MagicMock; calling _handle_generate_send
now runs the worker synchronously (extracted via
mock_submit.call_args[0][0]).
ALSO: initialize _project_switch_in_progress and
_project_switch_pending_path in AppController.__init__. They were
previously set only inside _switch_project and _do_project_switch,
so a fresh AppController() didn't have them and is_project_stale()
would raise AttributeError. is_project_stale is also now
getattr-based (defaulting to False) for additional safety.
ALSO: remove the @pytest.mark.skip marker from the test since
the underlying issue is now fixed.
Verified: tests/test_gui_events_v2.py 3/3 pass (previously 1 skipped).
scripts/audit_license_cve.baseline.json: the current
violation set (post-cleanup) accepted as the gate baseline.
When --strict is set, the script exits non-zero if the
current violation count exceeds the baseline count.
To regenerate the baseline after an intentional change
(e.g., adding a new dep with an acceptable license), run:
uv run python -m scripts.audit_license_cve --dump-baseline
Also fixes the baseline path: it now lives next to the script
(Path(__file__).parent) instead of the wrong location under
docs/reports/scripts/. The script's --report-dir argument is
unaffected - the baseline lives at scripts/audit_license_cve.baseline.json
regardless of the report directory.
The gate is wired into the same script (no separate file);
mirrors the 3 existing audit scripts (audit_main_thread_imports,
audit_weak_types, check_test_toml_paths) and their --strict
pattern.
28 unit + integration tests passing.
scripts/audit_license_cve.py: 4 internal checks (license +
CVE + pin + source-header), policy tables (allowlist of
permissive/weak-copyleft/public-domain, blocklist of
non-OSI/restricted-source), and a main() that runs all 4
and emits line-per-violation to stdout + a markdown report.
Tests (26 unit + integration) cover license classifier (16
variants across MIT, BSD, Apache, LGPL, MPL, CC0, WTFPL,
GPL, AGPL, SSPL, BSL, Commons Clause, Elastic, Anti-996,
Hippocratic, unknown), pin check (3), source-header check
(3), license check via importlib.metadata (1), CVE check
via subprocess pip-audit (2), and a smoke test of the main
loop (1).
No new pip deps in the project: pure stdlib
(importlib.metadata, tomllib, pathlib, re) + subprocess to
pip-audit (optional dev tool, installed via 'uv tool install
pip-audit' if user wants CVE checks).
Initial report at docs/reports/license_cve_audit/2026-06-07/
records the current state. The Phase 2 commit will apply
the fixes (tilde-pin, delete requirements.txt); the Phase 3
commit will add --strict mode + baseline file for CI.