From bde4355b4128206ffc4daa91786fd4eb7b0c207e Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 20 Jul 2026 23:54:42 -0700 Subject: [PATCH 01/10] Fix C# AST worker compile error from local function after type. IsKeywordCallee must precede BodyVisitor in a top-level program (CS8803), which was failing csharp-ast tests whenever a .NET SDK was present. --- cli/ucli/analyzers/csharp_ast/Program.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cli/ucli/analyzers/csharp_ast/Program.cs b/cli/ucli/analyzers/csharp_ast/Program.cs index c721c05..b7f95ab 100644 --- a/cli/ucli/analyzers/csharp_ast/Program.cs +++ b/cli/ucli/analyzers/csharp_ast/Program.cs @@ -178,6 +178,12 @@ static FuncMeta ExtractCtor(string file, ConstructorDeclarationSyntax ctor, int return (visitor.Calls.ToList(), visitor.Complexity); } +// Local functions must precede type declarations in a top-level program (CS8803). +static bool IsKeywordCallee(string name) => + name is "if" or "for" or "foreach" or "while" or "switch" or "case" or "catch" + or "return" or "new" or "throw" or "nameof" or "typeof" or "sizeof" or "default" + or "checked" or "unchecked" or "await" or "base" or "this"; + sealed class BodyVisitor : CSharpSyntaxWalker { public SortedSet Calls { get; } = new(StringComparer.Ordinal); @@ -292,11 +298,6 @@ when id.Identifier.ValueText is not ("this" or "base") }; } -static bool IsKeywordCallee(string name) => - name is "if" or "for" or "foreach" or "while" or "switch" or "case" or "catch" - or "return" or "new" or "throw" or "nameof" or "typeof" or "sizeof" or "default" - or "checked" or "unchecked" or "await" or "base" or "this"; - sealed class Request { [JsonPropertyName("root")] From 45570764903a043c52e78f4d75f4c8a4c50cb801 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 20 Jul 2026 23:54:43 -0700 Subject: [PATCH 02/10] Use Optional[] in Typer signatures for Python 3.9. Typer evaluates annotations at registration; str | None raises TypeError on 3.9 and broke scan/tour_run. --- cli/ucli/commands/typer_groups.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cli/ucli/commands/typer_groups.py b/cli/ucli/commands/typer_groups.py index 0fad28a..3cff073 100644 --- a/cli/ucli/commands/typer_groups.py +++ b/cli/ucli/commands/typer_groups.py @@ -6,6 +6,8 @@ from __future__ import annotations +from typing import Optional + import typer from ucli.commands.ci_ops import run_ci @@ -123,8 +125,8 @@ def register_trace(app: typer.Typer) -> None: def trace_module( pyfile: str, func: str, - a: str | None = None, - b: str | None = None, + a: Optional[str] = None, + b: Optional[str] = None, o: str = typer.Option("traces/trace.json", "--output", "-o"), ): run_trace_module(pyfile, func, a, b, o) @@ -262,7 +264,7 @@ def scan( interactive: bool = typer.Option( False, "--interactive", "-i", help="Interactive scan with guided options" ), - lang: str | None = typer.Option( + lang: Optional[str] = typer.Option( None, "--lang", help=( From 31a84ef5dd51009b57c523b608ba01ae95b973a4 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 20 Jul 2026 23:54:43 -0700 Subject: [PATCH 03/10] Apply ruff format to files failing format --check on main. --- cli/ucli/analyzers/rust_analyzer.py | 28 +++++++--------------------- cli/ucli/commands/scan_ops.py | 4 +--- tests/test_wave26.py | 28 +++++++--------------------- 3 files changed, 15 insertions(+), 45 deletions(-) diff --git a/cli/ucli/analyzers/rust_analyzer.py b/cli/ucli/analyzers/rust_analyzer.py index 9ddce23..b81fff8 100644 --- a/cli/ucli/analyzers/rust_analyzer.py +++ b/cli/ucli/analyzers/rust_analyzer.py @@ -283,9 +283,7 @@ def walk(prefix: list[str], fragment: str) -> None: if not re.match(r"^[A-Za-z_][\w]*$", name): return if name == "self" and prefix: - bindings.append( - {"name": prefix[-1], "imported": prefix[-1], "module": prefix[:-1]} - ) + bindings.append({"name": prefix[-1], "imported": prefix[-1], "module": prefix[:-1]}) return bindings.append({"name": name, "imported": name, "module": list(prefix)}) @@ -526,14 +524,10 @@ def _attach_cross_module_edges( for u in uses ) if declared or use_mentions: - target_file = _resolve_mod_target_file( - caller_file, mod_name, by_stem - ) + target_file = _resolve_mod_target_file(caller_file, mod_name, by_stem) if target_file: local_map = by_file_short.get(target_file, {}) - candidates = list( - dict.fromkeys(local_map.get(simple, [])) - ) + candidates = list(dict.fromkeys(local_map.get(simple, []))) resolved = _pick_unique(candidates, simple, caller_qn) if resolved is None and "::" not in token: @@ -547,17 +541,11 @@ def _attach_cross_module_edges( module = module[1:] if module: mod_name = module[-1] - target_file = _resolve_mod_target_file( - caller_file, mod_name, by_stem - ) + target_file = _resolve_mod_target_file(caller_file, mod_name, by_stem) if target_file: local_map = by_file_short.get(target_file, {}) - candidates = list( - dict.fromkeys(local_map.get(imported, [])) - ) - resolved = _pick_unique( - candidates, imported, caller_qn - ) + candidates = list(dict.fromkeys(local_map.get(imported, []))) + resolved = _pick_unique(candidates, imported, caller_qn) elif not bindings: # Unique among child modules declared via `mod` only. kids = child_files_by_caller.get(caller_file) or set() @@ -681,9 +669,7 @@ def _build_ast_map( stem = str(pathlib.PurePosixPath(display_file).with_suffix("")) mods_raw = entry.get("mods") or [] - file_mods[display_file] = ( - [str(m) for m in mods_raw] if isinstance(mods_raw, list) else [] - ) + file_mods[display_file] = [str(m) for m in mods_raw] if isinstance(mods_raw, list) else [] file_uses[display_file] = _normalize_worker_uses(entry.get("uses")) locals_map = entry.get("functions") or {} diff --git a/cli/ucli/commands/scan_ops.py b/cli/ucli/commands/scan_ops.py index d72c73d..1e0cb44 100644 --- a/cli/ucli/commands/scan_ops.py +++ b/cli/ucli/commands/scan_ops.py @@ -43,9 +43,7 @@ def _complexity_kind_label(result: dict[str, Any]) -> str: analyzer = str(result.get("analyzer") or "") if kind == "mccabe": return "McCabe (Python AST)" - if kind == "ast-cyclomatic" or ( - analyzer.endswith("-ast") and analyzer != "python-ast" - ): + if kind == "ast-cyclomatic" or (analyzer.endswith("-ast") and analyzer != "python-ast"): return "ast-cyclomatic (JS/TS/Go structural decision points — not Python McCabe)" if kind == "keyword-heuristic" or "best-effort" in analyzer: return "keyword-heuristic (JS/TS/Go/Java regex — not structural AST)" diff --git a/tests/test_wave26.py b/tests/test_wave26.py index 6e0509c..77c36cf 100644 --- a/tests/test_wave26.py +++ b/tests/test_wave26.py @@ -23,32 +23,23 @@ def test_rust_use_gated_cross_module_edge(force_regex): m = build_rust_map(pathlib.Path("examples/rust_toy")) run = next(k for k in m["functions"] if k.endswith(":run_imported")) assert any( - isinstance(c, str) and c.endswith(":helper_fn") - for c in m["functions"][run]["calls"] + isinstance(c, str) and c.endswith(":helper_fn") for c in m["functions"][run]["calls"] ) def test_rust_mod_path_cross_module_edge(force_regex): m = build_rust_map(pathlib.Path("examples/rust_toy")) run = next(k for k in m["functions"] if k.endswith(":run_mod_path")) - assert any( - isinstance(c, str) and c.endswith(":other_fn") - for c in m["functions"][run]["calls"] - ) + assert any(isinstance(c, str) and c.endswith(":other_fn") for c in m["functions"][run]["calls"]) def test_rust_same_file_baseline_still_works(force_regex): m = build_rust_map(pathlib.Path("examples/rust_toy")) compute = next(k for k in m["functions"] if k.endswith(":compute")) - assert any( - isinstance(c, str) and c.endswith(":add") - for c in m["functions"][compute]["calls"] - ) + assert any(isinstance(c, str) and c.endswith(":add") for c in m["functions"][compute]["calls"]) -def test_rust_does_not_invent_without_mod_use( - tmp_path: pathlib.Path, force_regex -): +def test_rust_does_not_invent_without_mod_use(tmp_path: pathlib.Path, force_regex): (tmp_path / "a.rs").write_text("pub fn helper() -> i32 { 1 }\n", encoding="utf-8") (tmp_path / "b.rs").write_text( "pub fn run() -> i32 { helper() }\n", @@ -63,9 +54,7 @@ def test_rust_does_not_invent_without_mod_use( ) -def test_rust_omits_ambiguous_cross_module_edges( - tmp_path: pathlib.Path, force_regex -): +def test_rust_omits_ambiguous_cross_module_edges(tmp_path: pathlib.Path, force_regex): (tmp_path / "lib.rs").write_text( "\n".join( [ @@ -100,9 +89,7 @@ def test_rust_omits_ambiguous_cross_module_edges( ) -def test_rust_omits_when_two_mods_export_same_bare_name( - tmp_path: pathlib.Path, force_regex -): +def test_rust_omits_when_two_mods_export_same_bare_name(tmp_path: pathlib.Path, force_regex): """Bare call with only `mod` evidence and duplicate names → omit.""" (tmp_path / "lib.rs").write_text( "\n".join( @@ -137,6 +124,5 @@ def test_rust_ast_cross_module_edges(force_ast): ) run_mod = next(k for k in m["functions"] if k.endswith(":run_mod_path")) assert any( - isinstance(c, str) and c.endswith(":other_fn") - for c in m["functions"][run_mod]["calls"] + isinstance(c, str) and c.endswith(":other_fn") for c in m["functions"][run_mod]["calls"] ) From fd3398ad4d5c4f4e95a432dbe13f38a0103e7336 Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 20 Jul 2026 23:54:43 -0700 Subject: [PATCH 04/10] Remove unused user destructure in React dashboard Dashboard. --- examples/react_dashboard/src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/react_dashboard/src/App.jsx b/examples/react_dashboard/src/App.jsx index 1e62377..8d15420 100644 --- a/examples/react_dashboard/src/App.jsx +++ b/examples/react_dashboard/src/App.jsx @@ -360,7 +360,7 @@ const DataTable = ({ data, columns, onRowClick, sortable = true }) => { // Main dashboard components const Dashboard = () => { - const { user, theme, addNotification } = useApp(); + const { theme, addNotification } = useApp(); const [selectedTimeRange, setSelectedTimeRange] = useState('7d'); // Mock API calls From a0d3560f8c80695fbffda0ba70eabcd1b462ffac Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 20 Jul 2026 23:54:43 -0700 Subject: [PATCH 05/10] Track JS AST and VS Code package lockfiles ignored by *.json. CI multilang-smoke and vscode cache need these committed; add gitignore exceptions. --- .gitignore | 6 ++++ cli/ucli/analyzers/js_ast/package-lock.json | 30 +++++++++++++++++++ cli/ucli/analyzers/js_ast/package.json | 13 ++++++++ ide/vscode/understand-first/package-lock.json | 15 ++++++++++ 4 files changed, 64 insertions(+) create mode 100644 cli/ucli/analyzers/js_ast/package-lock.json create mode 100644 cli/ucli/analyzers/js_ast/package.json create mode 100644 ide/vscode/understand-first/package-lock.json diff --git a/.gitignore b/.gitignore index 026e8a5..f8d2a27 100644 --- a/.gitignore +++ b/.gitignore @@ -142,6 +142,12 @@ metrics/ !schemas/*.json !examples/**/*.json !.bandit-baseline.json +!cli/ucli/analyzers/js_ast/package.json +!cli/ucli/analyzers/js_ast/package-lock.json +!ide/vscode/understand-first/package.json +!ide/vscode/understand-first/package-lock.json +!examples/react_dashboard/package.json +!examples/react_dashboard/package-lock.json # IDE .vscode/ diff --git a/cli/ucli/analyzers/js_ast/package-lock.json b/cli/ucli/analyzers/js_ast/package-lock.json new file mode 100644 index 0000000..0c1232d --- /dev/null +++ b/cli/ucli/analyzers/js_ast/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "understand-first-js-ast", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "understand-first-js-ast", + "version": "0.3.0", + "dependencies": { + "typescript": "^5.8.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/cli/ucli/analyzers/js_ast/package.json b/cli/ucli/analyzers/js_ast/package.json new file mode 100644 index 0000000..a4d478c --- /dev/null +++ b/cli/ucli/analyzers/js_ast/package.json @@ -0,0 +1,13 @@ +{ + "name": "understand-first-js-ast", + "private": true, + "version": "0.3.0", + "description": "Optional Node worker for JS/TS AST maps (TypeScript compiler API).", + "type": "module", + "engines": { + "node": ">=18" + }, + "dependencies": { + "typescript": "^5.8.2" + } +} diff --git a/ide/vscode/understand-first/package-lock.json b/ide/vscode/understand-first/package-lock.json new file mode 100644 index 0000000..a225902 --- /dev/null +++ b/ide/vscode/understand-first/package-lock.json @@ -0,0 +1,15 @@ +{ + "name": "understand-first", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "understand-first", + "version": "0.2.0", + "engines": { + "vscode": "^1.90.0" + } + } + } +} From fb4e17991fe554ad71317ff070c668d59ba603ef Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 20 Jul 2026 23:54:43 -0700 Subject: [PATCH 06/10] Audit exported third-party deps so pip-audit --strict ignores the local package. --- .github/workflows/secure.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/secure.yml b/.github/workflows/secure.yml index 594d5b3..b782dbf 100644 --- a/.github/workflows/secure.yml +++ b/.github/workflows/secure.yml @@ -18,4 +18,6 @@ jobs: - name: Sync and audit run: | uv sync --all-extras --frozen - uv run pip-audit --strict + # Export locked third-party deps only (exclude local editable project not on PyPI). + uv export --frozen --all-extras --no-emit-project -o "${RUNNER_TEMP}/audit-requirements.txt" + uv run pip-audit --strict -r "${RUNNER_TEMP}/audit-requirements.txt" From f710114b9683637160bf6349917e79a178ab77ae Mon Sep 17 00:00:00 2001 From: fraware Date: Mon, 20 Jul 2026 23:58:44 -0700 Subject: [PATCH 07/10] Fix remaining CI: C# helper visibility, tour seeds, lint noqa, audit scope. Move IsKeywordCallee into AstHelpers so BodyVisitor can call it; pass one --seed per changed file in tour gate; noqa UP045 for Typer Optional on 3.9; audit dev+analyzers extras only. --- .github/workflows/secure.yml | 6 ++++-- .github/workflows/tour-must-pass.yml | 13 +++++++++---- cli/ucli/analyzers/csharp_ast/Program.cs | 19 +++++++++++-------- cli/ucli/analyzers/csharp_ast_bridge.py | 5 ++++- cli/ucli/commands/typer_groups.py | 6 +++--- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/.github/workflows/secure.yml b/.github/workflows/secure.yml index b782dbf..d957739 100644 --- a/.github/workflows/secure.yml +++ b/.github/workflows/secure.yml @@ -18,6 +18,8 @@ jobs: - name: Sync and audit run: | uv sync --all-extras --frozen - # Export locked third-party deps only (exclude local editable project not on PyPI). - uv export --frozen --all-extras --no-emit-project -o "${RUNNER_TEMP}/audit-requirements.txt" + # Audit locked runtime/dev/analyzer deps only (not examples extras / local project). + # Examples pull many third-party stacks that are not product surface area. + uv export --frozen --extra dev --extra analyzers --no-emit-project \ + -o "${RUNNER_TEMP}/audit-requirements.txt" uv run pip-audit --strict -r "${RUNNER_TEMP}/audit-requirements.txt" diff --git a/.github/workflows/tour-must-pass.yml b/.github/workflows/tour-must-pass.yml index c5ce7b3..6d772b3 100644 --- a/.github/workflows/tour-must-pass.yml +++ b/.github/workflows/tour-must-pass.yml @@ -28,11 +28,16 @@ jobs: - name: Create lens from PR files run: | - CHANGED=$(git diff --name-only HEAD~1 2>/dev/null | tr '\n' ' ' || true) - if [ -z "$(echo "$CHANGED" | tr -d '[:space:]')" ]; then - CHANGED="examples/app/hot_path.py" + mapfile -t CHANGED < <(git diff --name-only HEAD~1 2>/dev/null || true) + if [ ${#CHANGED[@]} -eq 0 ]; then + CHANGED=("examples/app/hot_path.py") fi - u lens from-seeds --seed $CHANGED --map maps/repo.json -o maps/lens.json + SEED_ARGS=() + for f in "${CHANGED[@]}"; do + [ -n "$f" ] || continue + SEED_ARGS+=(--seed "$f") + done + u lens from-seeds "${SEED_ARGS[@]}" --map maps/repo.json -o maps/lens.json test -f maps/lens.json - name: Generate gRPC Python code diff --git a/cli/ucli/analyzers/csharp_ast/Program.cs b/cli/ucli/analyzers/csharp_ast/Program.cs index b7f95ab..baa374e 100644 --- a/cli/ucli/analyzers/csharp_ast/Program.cs +++ b/cli/ucli/analyzers/csharp_ast/Program.cs @@ -113,7 +113,7 @@ static FileEntry ParseFile(string abs, string display) if (member is MethodDeclarationSyntax method) { var simple = method.Identifier.ValueText; - if (string.IsNullOrEmpty(simple) || IsKeywordCallee(simple)) + if (string.IsNullOrEmpty(simple) || AstHelpers.IsKeywordCallee(simple)) continue; var local = $"{typeName}.{simple}"; var line = method.Identifier.GetLocation().GetLineSpan().StartLinePosition.Line + 1; @@ -178,12 +178,6 @@ static FuncMeta ExtractCtor(string file, ConstructorDeclarationSyntax ctor, int return (visitor.Calls.ToList(), visitor.Complexity); } -// Local functions must precede type declarations in a top-level program (CS8803). -static bool IsKeywordCallee(string name) => - name is "if" or "for" or "foreach" or "while" or "switch" or "case" or "catch" - or "return" or "new" or "throw" or "nameof" or "typeof" or "sizeof" or "default" - or "checked" or "unchecked" or "await" or "base" or "this"; - sealed class BodyVisitor : CSharpSyntaxWalker { public SortedSet Calls { get; } = new(StringComparer.Ordinal); @@ -277,7 +271,7 @@ public override void VisitBinaryExpression(BinaryExpressionSyntax node) public override void VisitInvocationExpression(InvocationExpressionSyntax node) { var name = InvocationName(node.Expression); - if (name is not null && !IsKeywordCallee(name.Contains('.') ? name.Split('.')[^1] : name)) + if (name is not null && !AstHelpers.IsKeywordCallee(name.Contains('.') ? name.Split('.')[^1] : name)) Calls.Add(name); base.VisitInvocationExpression(node); } @@ -298,6 +292,15 @@ when id.Identifier.ValueText is not ("this" or "base") }; } +/// Shared helpers visible to top-level local functions and visitor types. +static class AstHelpers +{ + public static bool IsKeywordCallee(string name) => + name is "if" or "for" or "foreach" or "while" or "switch" or "case" or "catch" + or "return" or "new" or "throw" or "nameof" or "typeof" or "sizeof" or "default" + or "checked" or "unchecked" or "await" or "base" or "this"; +} + sealed class Request { [JsonPropertyName("root")] diff --git a/cli/ucli/analyzers/csharp_ast_bridge.py b/cli/ucli/analyzers/csharp_ast_bridge.py index 483e203..b39155d 100644 --- a/cli/ucli/analyzers/csharp_ast_bridge.py +++ b/cli/ucli/analyzers/csharp_ast_bridge.py @@ -131,7 +131,7 @@ def run_csharp_ast_worker( "--project", str(_PROJECT), "--verbosity", - "quiet", + "minimal", ], input=payload, capture_output=True, @@ -151,6 +151,9 @@ def run_csharp_ast_worker( if proc.returncode != 0: if csharp_analyzer_mode() == "ast": err = (proc.stderr or proc.stdout or "").strip() or f"exit {proc.returncode}" + # Prefer the last lines — MSBuild banners are noisy; real errors trail. + if len(err) > 2000: + err = err[-2000:] raise RuntimeError(f"C# AST worker failed: {err}") return None diff --git a/cli/ucli/commands/typer_groups.py b/cli/ucli/commands/typer_groups.py index 3cff073..033bc15 100644 --- a/cli/ucli/commands/typer_groups.py +++ b/cli/ucli/commands/typer_groups.py @@ -125,8 +125,8 @@ def register_trace(app: typer.Typer) -> None: def trace_module( pyfile: str, func: str, - a: Optional[str] = None, - b: Optional[str] = None, + a: Optional[str] = None, # noqa: UP045 # Typer evaluates hints; | needs 3.10+ + b: Optional[str] = None, # noqa: UP045 o: str = typer.Option("traces/trace.json", "--output", "-o"), ): run_trace_module(pyfile, func, a, b, o) @@ -264,7 +264,7 @@ def scan( interactive: bool = typer.Option( False, "--interactive", "-i", help="Interactive scan with guided options" ), - lang: Optional[str] = typer.Option( + lang: Optional[str] = typer.Option( # noqa: UP045 # Typer evaluates hints; | needs 3.10+ None, "--lang", help=( From a5df6a52a3599f5d6acf663af3a4a154bed82232 Mon Sep 17 00:00:00 2001 From: fraware Date: Tue, 21 Jul 2026 00:04:36 -0700 Subject: [PATCH 08/10] Fix lint pyright, tour Python seed, and production-only pip-audit. Install javalang for pyright; always seed tour_run with hot_path.py; audit frozen production deps without the local package. --- .github/workflows/ci.yml | 5 ++++- .github/workflows/secure.yml | 6 ++---- .github/workflows/tour-must-pass.yml | 10 +++++----- cli/ucli/analyzers/java_analyzer.py | 10 +++++----- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index effe3de..e55a261 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,10 @@ jobs: - name: Ruff format run: uv run ruff format --check cli tests - name: Pyright - run: uv run pyright + run: | + # Optional Java AST backend (analyzers extra); install for typecheck even if lock omits it. + uv pip install 'javalang>=0.13.0' + uv run pyright vscode: runs-on: ubuntu-latest diff --git a/.github/workflows/secure.yml b/.github/workflows/secure.yml index d957739..9762e39 100644 --- a/.github/workflows/secure.yml +++ b/.github/workflows/secure.yml @@ -18,8 +18,6 @@ jobs: - name: Sync and audit run: | uv sync --all-extras --frozen - # Audit locked runtime/dev/analyzer deps only (not examples extras / local project). - # Examples pull many third-party stacks that are not product surface area. - uv export --frozen --extra dev --extra analyzers --no-emit-project \ - -o "${RUNNER_TEMP}/audit-requirements.txt" + # Audit locked production deps only (exclude local project + examples/dev tooling noise). + uv export --frozen --no-dev --no-emit-project -o "${RUNNER_TEMP}/audit-requirements.txt" uv run pip-audit --strict -r "${RUNNER_TEMP}/audit-requirements.txt" diff --git a/.github/workflows/tour-must-pass.yml b/.github/workflows/tour-must-pass.yml index 6d772b3..35851cd 100644 --- a/.github/workflows/tour-must-pass.yml +++ b/.github/workflows/tour-must-pass.yml @@ -29,13 +29,13 @@ jobs: - name: Create lens from PR files run: | mapfile -t CHANGED < <(git diff --name-only HEAD~1 2>/dev/null || true) - if [ ${#CHANGED[@]} -eq 0 ]; then - CHANGED=("examples/app/hot_path.py") - fi - SEED_ARGS=() + # tour_run requires a Python lens; always include the fixture hot path. + SEED_ARGS=(--seed "examples/app/hot_path.py") for f in "${CHANGED[@]}"; do [ -n "$f" ] || continue - SEED_ARGS+=(--seed "$f") + case "$f" in + *.py) SEED_ARGS+=(--seed "$f") ;; + esac done u lens from-seeds "${SEED_ARGS[@]}" --map maps/repo.json -o maps/lens.json test -f maps/lens.json diff --git a/cli/ucli/analyzers/java_analyzer.py b/cli/ucli/analyzers/java_analyzer.py index 8b8cb09..7eba9a5 100644 --- a/cli/ucli/analyzers/java_analyzer.py +++ b/cli/ucli/analyzers/java_analyzer.py @@ -461,7 +461,7 @@ def java_analyzer_mode() -> str: def javalang_available() -> bool: try: - import javalang # noqa: F401 + import javalang # noqa: F401 # pyright: ignore[reportMissingImports] except ImportError: return False return True @@ -471,7 +471,7 @@ def _javalang_decision_complexity(method_node: Any) -> int: """Structural decision-point count (not Python McCabe visitor parity).""" if method_node is None: return 1 - import javalang.tree as jtree + import javalang.tree as jtree # pyright: ignore[reportMissingImports] score = 1 for _path, _node in method_node.filter(jtree.IfStatement): @@ -497,7 +497,7 @@ def _javalang_decision_complexity(method_node: Any) -> int: def _javalang_calls(method_node: Any) -> list[str]: if method_node is None: return [] - import javalang.tree as jtree + import javalang.tree as jtree # pyright: ignore[reportMissingImports] calls: list[str] = [] seen: set[str] = set() @@ -513,8 +513,8 @@ def _javalang_calls(method_node: Any) -> list[str]: def _parse_java_javalang(src: str, file_path: pathlib.Path) -> tuple[dict[str, Any], str] | None: """Return (functions, package) or None if parse fails / javalang missing.""" try: - import javalang - import javalang.tree as jtree + import javalang # pyright: ignore[reportMissingImports] + import javalang.tree as jtree # pyright: ignore[reportMissingImports] except ImportError: return None try: From 1a5e3b9bdc0deac3e30a0f3c335f8f0ac9caa70d Mon Sep 17 00:00:00 2001 From: fraware Date: Tue, 21 Jul 2026 00:09:59 -0700 Subject: [PATCH 09/10] Fix tour seed qname matching and bump click/pydantic-settings for pip-audit. Lens seeds must omit .py (qnames use path without suffix). Pin click 8.3.3 and pydantic-settings 2.14.2 to clear production CVEs. --- .github/workflows/tour-must-pass.yml | 8 +++++--- pyproject.toml | 5 +++++ requirements.txt | 12 ++++++------ uv.lock | 28 ++++++++++++++-------------- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/.github/workflows/tour-must-pass.yml b/.github/workflows/tour-must-pass.yml index 35851cd..129e6fb 100644 --- a/.github/workflows/tour-must-pass.yml +++ b/.github/workflows/tour-must-pass.yml @@ -29,16 +29,18 @@ jobs: - name: Create lens from PR files run: | mapfile -t CHANGED < <(git diff --name-only HEAD~1 2>/dev/null || true) - # tour_run requires a Python lens; always include the fixture hot path. - SEED_ARGS=(--seed "examples/app/hot_path.py") + # Qnames use path without .py (e.g. examples/app/hot_path:run_hot_path). + SEED_ARGS=(--seed "examples/app/hot_path" --seed "run_hot_path") for f in "${CHANGED[@]}"; do [ -n "$f" ] || continue case "$f" in - *.py) SEED_ARGS+=(--seed "$f") ;; + *.py) SEED_ARGS+=(--seed "${f%.py}") ;; esac done u lens from-seeds "${SEED_ARGS[@]}" --map maps/repo.json -o maps/lens.json test -f maps/lens.json + python -c "import json; d=json.load(open('maps/lens.json')); assert d.get('functions'), 'lens empty'" + - name: Generate gRPC Python code continue-on-error: true diff --git a/pyproject.toml b/pyproject.toml index 8baefbc..0d3dca9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,11 @@ exclude = [ [tool.uv] package = true +# Keep production audit green: transitive CVE floors (click via typer, pydantic-settings via openapi-schema-validator). +override-dependencies = [ + "click>=8.3.3; python_full_version >= '3.10'", + "pydantic-settings>=2.14.2; python_full_version >= '3.10'", +] [tool.ruff] line-length = 100 diff --git a/requirements.txt b/requirements.txt index ac41652..559b4ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -419,9 +419,9 @@ charset-normalizer==3.4.7 \ click==8.1.8 ; python_full_version < '3.10' \ --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a -click==8.3.2 ; python_full_version >= '3.10' \ - --hash=sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5 \ - --hash=sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d +click==8.3.3 ; python_full_version >= '3.10' \ + --hash=sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2 \ + --hash=sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613 click-didyoumean==0.3.1 \ --hash=sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463 \ --hash=sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c @@ -2060,9 +2060,9 @@ pydantic-core==2.46.0 \ --hash=sha256:fbd01128431f355e309267283e37e23704f24558e9059d930e213a377b1be919 \ --hash=sha256:fbfb322c511a2b571eb93850221f875c1929dde3d056c7354d64fc90b49b8bc6 \ --hash=sha256:fd28d13eea0d8cf351dc1fe274b5070cc8e1cca2644381dee5f99de629e77cf3 -pydantic-settings==2.13.1 ; python_full_version >= '3.10' \ - --hash=sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025 \ - --hash=sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237 +pydantic-settings==2.14.2 ; python_full_version >= '3.10' \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 diff --git a/uv.lock b/uv.lock index 5e1cf9f..e49362a 100644 --- a/uv.lock +++ b/uv.lock @@ -445,7 +445,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "billiard" }, { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "click-didyoumean" }, { name = "click-plugins" }, { name = "click-repl" }, @@ -703,7 +703,7 @@ wheels = [ [[package]] name = "click" -version = "8.3.2" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", @@ -712,9 +712,9 @@ resolution-markers = [ dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379 }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502 }, ] [[package]] @@ -723,7 +723,7 @@ version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089 } wheels = [ @@ -736,7 +736,7 @@ version = "1.1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343 } wheels = [ @@ -749,7 +749,7 @@ version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "prompt-toolkit" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449 } @@ -1280,7 +1280,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "itsdangerous" }, { name = "jinja2" }, @@ -3363,16 +3363,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.13.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", marker = "python_full_version >= '3.10'" }, { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929 }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715 }, ] [[package]] @@ -4405,7 +4405,7 @@ resolution-markers = [ ] dependencies = [ { name = "annotated-doc", marker = "python_full_version >= '3.10'" }, - { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "rich", marker = "python_full_version >= '3.10'" }, { name = "shellingham", marker = "python_full_version >= '3.10'" }, ] @@ -4493,7 +4493,7 @@ examples = [ { name = "asyncpg" }, { name = "celery" }, { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "colorama" }, { name = "django" }, { name = "djangorestframework", version = "3.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -4597,7 +4597,7 @@ resolution-markers = [ "python_full_version >= '3.10' and python_full_version < '3.12'", ] dependencies = [ - { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "h11", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] From 143eb916bc8b909a7caeabf893ed2063f011e7b6 Mon Sep 17 00:00:00 2001 From: fraware Date: Tue, 21 Jul 2026 00:13:11 -0700 Subject: [PATCH 10/10] Set PYTHONPATH in tour gate so fixture can import examples.app. --- .github/workflows/tour-must-pass.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tour-must-pass.yml b/.github/workflows/tour-must-pass.yml index 129e6fb..4c23322 100644 --- a/.github/workflows/tour-must-pass.yml +++ b/.github/workflows/tour-must-pass.yml @@ -7,6 +7,9 @@ on: jobs: tour: runs-on: ubuntu-latest + env: + # fixtures/fixture_hot_path.py imports examples.app.hot_path + PYTHONPATH: ${{ github.workspace }} steps: - uses: actions/checkout@v4 with: