From 0bc0803e9567894f62fd2b22d5776effc8913dce Mon Sep 17 00:00:00 2001 From: Jier Date: Sun, 13 Sep 2026 21:42:12 +0200 Subject: [PATCH 1/2] =?UTF-8?q?conditional=5Ftasks=20=E2=86=92=20graph-der?= =?UTF-8?q?ived=20(same=20pass=20as=20conditional=5Fdecision=5Fpoints):=20?= =?UTF-8?q?the=20duplicate=20flattened-task=20scan=20is=20gone,=20so=20the?= =?UTF-8?q?y=20can't=20drift=20like=20task=5Fincludes=20did.=20error=5Fhan?= =?UTF-8?q?dlers=20fixed:=20was=20dead=20(scanned=20flattened=20tasks=20fo?= =?UTF-8?q?r=20a=20rescue/always=20key=20that=20never=20exists=20=E2=86=92?= =?UTF-8?q?=20always=200).=20Now=20the=20graph=20detects=20block/rescue/al?= =?UTF-8?q?ways=20and=20sets=20error=5Fhandling=20on=20the=20node;=20error?= =?UTF-8?q?=5Fhandlers=20counts=20it.=20Unit-tested=20(rescue=20block=20?= =?UTF-8?q?=E2=86=92=201);=20stays=200=20for=20roles=20genuinely=20without?= =?UTF-8?q?=20rescue=20(CIS,=20openstack).=20Reachability=20is=20now=20a?= =?UTF-8?q?=20complete,=20honest=20partition=20(the=20candidate-10=20findi?= =?UTF-8?q?ng):=20static=5Freachable=20+=20dynamically=5Freachable=20+=20u?= =?UTF-8?q?nreachable=20=3D=3D=20task=5Ffiles.=20openstack=20now=20reads?= =?UTF-8?q?=202=20+=2013=20+=205=20=3D=2020=20(previously=20"static=202=20?= =?UTF-8?q?/=20orphan=205"=20silently=20hid=20the=2013=20dynamic-gated=20f?= =?UTF-8?q?iles).=20orphan=5Ftask=5Ffiles=20renamed=20unreachable=5Ftask?= =?UTF-8?q?=5Ffiles,=20with=20a=20README=20note=20explaining=20the=20tiers?= =?UTF-8?q?=20(and=20that=20files=20behind=20an=20unresolved=20dynamic=20b?= =?UTF-8?q?oundary=20stay=20"unreachable"=20=E2=80=94=20the=20graph=20stil?= =?UTF-8?q?l=20won't=20guess=20{{=20pkg=5Fmgr=20}}.yml).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAIMS.md | 68 +++++++++++-------- .../analyzers/role_analyzer.py | 26 +++---- .../analyzers/complexity_analyzer/models.py | 11 ++- docsible/formatters/text/dry_run.py | 3 +- docsible/graphs/role_execution.py | 15 ++++ .../role/sections/adaptive_diagrams.jinja2 | 7 +- tests/analyzers/complexity/test_analyzer.py | 34 ++++++++++ tests/graphs/test_role_execution.py | 57 ++++++++++++++++ tests/test_complexity_analyzer.py | 6 ++ 9 files changed, 181 insertions(+), 46 deletions(-) diff --git a/CLAIMS.md b/CLAIMS.md index 7e801c4..408b6dc 100644 --- a/CLAIMS.md +++ b/CLAIMS.md @@ -261,28 +261,39 @@ the same fact is never computed twice by two implementations (the `task_includes`/legacy-`include:` drift was the first instance of this class). - Derived from the graph (single source of truth): `task_includes`, - `role_includes`, `static_reachable_task_files`, `dynamic_boundaries`, - `unknown_boundaries`, `external_role_references`, `loop_tasks`, - `notification_edges`, `orphan_task_files`, `conditional_decision_points`. + `role_includes`, `conditional_tasks`, `error_handlers`, + `static_reachable_task_files`, `dynamically_reachable_task_files`, + `unreachable_task_files`, `dynamic_boundaries`, `unknown_boundaries`, + `external_role_references`, `loop_tasks`, `notification_edges`, + `conditional_decision_points`. - Still computed by separate scans of `role_info` (acceptable structural - counts): `total_tasks`, `task_files`, `handlers`, `max_tasks_per_file`, - `avg_tasks_per_file`; meta reads `role_dependencies`, + counts, not graph facts): `total_tasks`, `task_files`, `handlers`, + `max_tasks_per_file`, `avg_tasks_per_file`; meta reads `role_dependencies`, `collection_dependencies`; and the non-graph analyzers `external_integrations` (`detect_integrations`), `file_details` (`analyze_file_complexity`), and the hotspot/inflection detectors. -- Two residual scans are flagged risks, not yet fixed: - - `conditional_tasks` and the graph-derived `conditional_decision_points` - measure the same concept through two implementations. They agree on every - tested role today (CIS: 592 = 592) but can silently drift, exactly like - `task_includes` did before it was made graph-derived. Recommendation: - keep the graph-derived value authoritative and drop or alias the scan. - - `error_handlers` is effectively dead: it counts `task.get("rescue") or - task.get("always")` over the *flattened processed* tasks, but the - flattener emits block/rescue/always as separate rows (with `module`), - never as a `rescue`/`always` key on a task — so it reports **0** even for - a role with ~189 blocks (verified on `UBUNTU22-CIS`). It is both - mis-implemented and a residual scan; the right owner is the graph, which - already walks real block/rescue/always — see Next Graph Milestones #3. +- Resolved this milestone: + - `conditional_tasks` no longer has its own flattened-task scan — it is now + derived from the same graph pass as `conditional_decision_points`, so the + two cannot drift (previously the first duplicate-scan class instance after + `task_includes`). + - `error_handlers` was dead (it scanned flattened tasks for a `rescue`/ + `always` key that is never there, so always 0). It is now graph-derived + from real block/rescue/always detection; a rescue block counts as 1 + (unit-tested), and roles without rescue/always correctly stay 0. +- Reachability is now a complete, honest partition. Previously a file reached + only through a dynamic boundary was counted as neither "static reachable" + nor "orphan" and silently vanished (openstack `ansible-hardening` showed + "static 2 / orphan 5" for a 20-file role). Now `static_reachable + + dynamically_reachable + unreachable == task_files` (openstack: 2 + 13 + 5 + = 20), and the misleading "orphan" field is renamed `unreachable_task_files` + (no inbound edge from any *resolved* boundary), with the graph summary and + README explaining the three tiers. +- Still open (structural depth, not metrics): the graph flags that a block has + rescue/always but does not yet model the block/rescue/always control flow as + first-class nodes/edges, and files reachable only through an *unresolved* + dynamic boundary (e.g. `{{ pkg_mgr }}.yml`) still read as `unreachable` + because the graph deliberately does not guess which concrete file runs. ### Next Graph Milestones @@ -303,10 +314,11 @@ the same fact is never computed twice by two implementations (the per-role graphs via those role edges. This is the concrete building block for the collection milestone and is incremental on the existing model, not a new subsystem. -3. Add graph projections for blocks, rescue/always, and source-linked - variable scopes without claiming static certainty where Ansible defers - resolution. Fixing block/rescue/always representation here also repairs - the dead `error_handlers` metric (see Complexity ownership above). +3. Model block/rescue/always as first-class graph structure (nodes/edges), and + add source-linked variable scopes, without claiming static certainty where + Ansible defers resolution. (The `error_handlers` metric is already + graph-derived from real rescue/always detection; what remains is exposing + the block control flow itself, not just the count.) 4. Make `graph_visualisation` a renderer adapter over this contract, using NetworkX only for renderer-specific layout work. 5. Extend the pinned external corpus before treating the graph contract as @@ -391,11 +403,13 @@ duplication is prioritized by ownership and behavior rather than percentage. and include/role boundary counts are graph-derived (milestone 14). Used identically by `document role`, `document role --collection`, and `scan collection`. -2. Collapse the remaining duplicate complexity scans into the graph so a fact - is computed once: `conditional_tasks` (alias/derive from - `conditional_decision_points`) and `error_handlers` (via real - block/rescue/always projection, Next Graph Milestones #3). Until then they - are two implementations of one concept and can drift. + 2. **Resolved.** The duplicate complexity scans are collapsed into the graph: + `conditional_tasks` is now derived from the same graph pass as + `conditional_decision_points` (one source, cannot drift), and + `error_handlers` is graph-derived from real block/rescue/always detection + (was always 0). Reachability was also made a complete partition + (`static + dynamic-only + unreachable == task_files`) with the misleading + `orphan` renamed to `unreachable`. 3. Role-information *loading* still has one remaining duplicate: the deprecated `RoleInfoBuilder` alongside `RoleInfoLoader` (see Known Limitations). Retire `RoleInfoBuilder` and the deprecated `docsible role` command, and diff --git a/docsible/analyzers/complexity_analyzer/analyzers/role_analyzer.py b/docsible/analyzers/complexity_analyzer/analyzers/role_analyzer.py index 4cbb8cb..52a2b74 100644 --- a/docsible/analyzers/complexity_analyzer/analyzers/role_analyzer.py +++ b/docsible/analyzers/complexity_analyzer/analyzers/role_analyzer.py @@ -145,18 +145,10 @@ def analyze_role_complexity( # Count handlers handlers = len(role_info.get("handlers", [])) - # Count conditional tasks - conditional_tasks = sum( - 1 for tf in tasks_data for task in tf.get("tasks", []) if task.get("when") - ) - - # Count tasks with error handling (rescue or always blocks) - error_handlers = sum( - 1 - for tf in tasks_data - for task in tf.get("tasks", []) - if task.get("rescue") or task.get("always") - ) + # conditional_tasks and error_handlers are derived from the + # RoleExecutionGraph below (single source of truth), instead of a second + # flattened-task scan. The old error_handlers scan read a `rescue`/`always` + # key that flattened tasks never carry, so it was always 0. # Count role dependencies (from meta/main.yml) role_dependencies = len(role_info.get("meta", {}).get("dependencies", [])) @@ -207,10 +199,16 @@ def analyze_role_complexity( # Create metrics (execution_graph already built above; reuse it). phases = execution_graph.execution_phases() + task_nodes = [node for node in execution_graph.nodes.values() if node.kind is NodeKind.TASK] graph_metrics = { + "conditional_tasks": sum("condition" in node.metadata for node in task_nodes), + "error_handlers": sum("error_handling" in node.metadata for node in task_nodes), "static_reachable_task_files": sum( phase["kind"] in {"entrypoint", "static", "conditional"} for phase in phases ), + "dynamically_reachable_task_files": sum( + phase["kind"] == "dynamic" for phase in phases + ), "dynamic_boundaries": sum( edge.resolution is ResolutionStatus.DYNAMIC and edge.kind in {EdgeKind.INCLUDES_TASK_FILE, EdgeKind.IMPORTS_TASK_FILE, EdgeKind.INCLUDES_ROLE, EdgeKind.IMPORTS_ROLE} @@ -232,7 +230,7 @@ def analyze_role_complexity( edge.kind is EdgeKind.NOTIFIES_HANDLER and edge.target_id is not None for edge in execution_graph.edges ), - "orphan_task_files": sum(phase["kind"] == "unreachable" for phase in phases), + "unreachable_task_files": sum(phase["kind"] == "unreachable" for phase in phases), "conditional_decision_points": sum( node.kind is NodeKind.TASK and "condition" in node.metadata for node in execution_graph.nodes.values() @@ -242,8 +240,6 @@ def analyze_role_complexity( total_tasks=total_tasks, task_files=task_files, handlers=handlers, - conditional_tasks=conditional_tasks, - error_handlers=error_handlers, role_dependencies=role_dependencies, collection_dependencies=collection_dependencies, role_includes=role_includes, diff --git a/docsible/analyzers/complexity_analyzer/models.py b/docsible/analyzers/complexity_analyzer/models.py index 11ec194..294096a 100644 --- a/docsible/analyzers/complexity_analyzer/models.py +++ b/docsible/analyzers/complexity_analyzer/models.py @@ -65,13 +65,20 @@ class ComplexityMetrics(BaseModel): task_includes: int = Field(default=0, description="include_tasks/import_tasks count") # Execution graph metrics (source-backed relationships, not runtime claims) - static_reachable_task_files: int = Field(default=0) + static_reachable_task_files: int = Field( + default=0, description="Files reachable via static/conditional boundaries only" + ) + dynamically_reachable_task_files: int = Field( + default=0, description="Files reachable only through an unresolved dynamic boundary" + ) dynamic_boundaries: int = Field(default=0) unknown_boundaries: int = Field(default=0) external_role_references: int = Field(default=0) loop_tasks: int = Field(default=0) notification_edges: int = Field(default=0) - orphan_task_files: int = Field(default=0) + unreachable_task_files: int = Field( + default=0, description="Files with no inbound boundary from any resolved edge" + ) conditional_decision_points: int = Field(default=0) # External integrations diff --git a/docsible/formatters/text/dry_run.py b/docsible/formatters/text/dry_run.py index ac8c4d5..999f06e 100644 --- a/docsible/formatters/text/dry_run.py +++ b/docsible/formatters/text/dry_run.py @@ -119,8 +119,9 @@ def _format_complexity(self, analysis_report, role_info: dict) -> str: lines.append( " Execution graph: " f"{metrics.static_reachable_task_files} static files, " + f"{metrics.dynamically_reachable_task_files} dynamic-only, " f"{metrics.dynamic_boundaries} dynamic boundaries, " - f"{metrics.orphan_task_files} orphans" + f"{metrics.unreachable_task_files} unreachable" ) return "\n".join(lines) diff --git a/docsible/graphs/role_execution.py b/docsible/graphs/role_execution.py index 3142f11..79a7f64 100644 --- a/docsible/graphs/role_execution.py +++ b/docsible/graphs/role_execution.py @@ -247,6 +247,8 @@ def _add_tasks(graph: RoleExecutionGraph, role_name: str, task_file: dict[str, A metadata["loop"] = loop if loop_control := extract_loop_control(task): metadata["loop_control"] = loop_control + if error_handling := _error_handling(task): + metadata["error_handling"] = error_handling graph.add_node(GraphNode(task_id, NodeKind.TASK, str(task.get("name", "Unnamed")), source, metadata)) graph.add_edge(GraphEdge(EdgeKind.CONTAINS, file_ids[file_name], task_id, ResolutionStatus.STATIC, source)) _add_variable_edges(graph, task_id, task, variables, source) @@ -365,3 +367,16 @@ def _loop(task: dict[str, Any]) -> str | None: if "loop" in task: return "loop" return next((key for key in task if key.startswith("with_")), None) + + +def _error_handling(task: dict[str, Any]) -> str | None: + """Return the block error-handling shape ('rescue'/'always'/both) if any.""" + has_rescue = isinstance(task.get("rescue"), list) + has_always = isinstance(task.get("always"), list) + if has_rescue and has_always: + return "rescue + always" + if has_rescue: + return "rescue" + if has_always: + return "always" + return None diff --git a/docsible/templates/role/sections/adaptive_diagrams.jinja2 b/docsible/templates/role/sections/adaptive_diagrams.jinja2 index 6f12dca..cdcde09 100644 --- a/docsible/templates/role/sections/adaptive_diagrams.jinja2 +++ b/docsible/templates/role/sections/adaptive_diagrams.jinja2 @@ -64,12 +64,17 @@ This role contains **{{ complexity_report.metrics.total_tasks }} tasks** across ### Execution Graph Summary - Collection dependencies: {{ complexity_report.metrics.collection_dependencies }} - Conditional decision points: {{ complexity_report.metrics.conditional_decision_points }} +- Error handlers (blocks with rescue/always): {{ complexity_report.metrics.error_handlers }} - Statically reachable task files: {{ complexity_report.metrics.static_reachable_task_files }} +- Dynamically reachable task files: {{ complexity_report.metrics.dynamically_reachable_task_files }} +- Unreachable task files: {{ complexity_report.metrics.unreachable_task_files }} - Dynamic boundaries: {{ complexity_report.metrics.dynamic_boundaries }} - Unknown boundaries: {{ complexity_report.metrics.unknown_boundaries }} - Handler notification edges: {{ complexity_report.metrics.notification_edges }} - Loop-bearing tasks: {{ complexity_report.metrics.loop_tasks }} -- Orphan task files: {{ complexity_report.metrics.orphan_task_files }} + +_Dynamically reachable files are reached only through a boundary whose target is +templated; unreachable files have no inbound edge from any resolved boundary._ {% if architecture_diagram %} ### Component Architecture diff --git a/tests/analyzers/complexity/test_analyzer.py b/tests/analyzers/complexity/test_analyzer.py index f35c2d2..138e448 100644 --- a/tests/analyzers/complexity/test_analyzer.py +++ b/tests/analyzers/complexity/test_analyzer.py @@ -60,6 +60,12 @@ def test_analyze_conditional_percentage(): {"name": "Task 3", "module": "debug"}, # No condition {"name": "Task 4", "module": "debug"}, # No condition ], + "mermaid": [ + {"name": "Task 1", "debug": {}, "when": "condition1"}, + {"name": "Task 2", "debug": {}, "when": "condition2"}, + {"name": "Task 3", "debug": {}}, + {"name": "Task 4", "debug": {}}, + ], } ], "handlers": [], @@ -127,3 +133,31 @@ def test_task_includes_is_graph_authoritative_and_counts_legacy_include(): metrics = analyze_role_complexity(role_info).metrics assert metrics.task_includes == 1 # bare include: counted via the graph assert metrics.role_includes == 0 + + +def test_error_handlers_is_graph_derived_from_rescue_blocks(): + """Regression: error_handlers used to scan flattened tasks for a + `rescue`/`always` key they never carry (always 0). Now derived from the + execution graph's block/rescue detection.""" + role_info = { + "name": "guarded", + "defaults": [], + "vars": [], + "handlers": [], + "meta": {"dependencies": []}, + "tasks": [ + { + "file": "main.yml", + "tasks": [{"name": "Guarded", "module": "block"}], + "mermaid": [ + { + "name": "Guarded", + "block": [{"name": "Try", "debug": {}}], + "rescue": [{"name": "Fallback", "debug": {}}], + } + ], + } + ], + } + metrics = analyze_role_complexity(role_info).metrics + assert metrics.error_handlers == 1 diff --git a/tests/graphs/test_role_execution.py b/tests/graphs/test_role_execution.py index 26c5837..633cbbf 100644 --- a/tests/graphs/test_role_execution.py +++ b/tests/graphs/test_role_execution.py @@ -145,3 +145,60 @@ def test_uses_variable_edge_survives_tokenizer_rewrite(): ) var_edges = [e for e in graph.edges if e.kind is EdgeKind.USES_VARIABLE] assert [e.target_id for e in var_edges] == ["variable:web:defaults:web_port"] + + +def test_rescue_block_recorded_as_error_handling(): + graph = build_role_execution_graph( + { + "name": "r", + "defaults": [], + "vars": [], + "handlers": [], + "tasks": [ + { + "file": "main.yml", + "tasks": [{}], + "mermaid": [ + { + "name": "Guarded", + "block": [{"debug": {}}], + "rescue": [{"debug": {}}], + } + ], + } + ], + } + ) + nodes = [ + n for n in graph.nodes.values() if n.kind is NodeKind.TASK and "error_handling" in n.metadata + ] + assert nodes and nodes[0].metadata["error_handling"] == "rescue" + + +def test_execution_phases_partition_covers_every_file(): + role_info = { + "name": "r", + "defaults": [], + "vars": [], + "handlers": [], + "tasks": [ + { + "file": "main.yml", + "tasks": [{}, {}], + "mermaid": [ + {"name": "static", "import_tasks": "sub.yml"}, + {"name": "dyn", "import_tasks": "{{ variant }}stig/main.yml"}, + ], + }, + {"file": "sub.yml", "tasks": [{}], "mermaid": [{"debug": {}}]}, + {"file": "stig/main.yml", "tasks": [{}], "mermaid": [{"debug": {}}]}, + {"file": "never_referenced.yml", "tasks": [{}], "mermaid": [{"debug": {}}]}, + ], + } + phases = build_role_execution_graph(role_info).execution_phases() + kinds = [p["kind"] for p in phases] + # every file is classified, and the three tiers partition exactly the files + assert len(phases) == 4 + assert kinds.count("unreachable") == 1 # never_referenced.yml + assert kinds.count("dynamic") == 1 # stig/main.yml reached via templated include + assert "entrypoint" in kinds and "static" in kinds diff --git a/tests/test_complexity_analyzer.py b/tests/test_complexity_analyzer.py index 0419ef6..e04da5b 100644 --- a/tests/test_complexity_analyzer.py +++ b/tests/test_complexity_analyzer.py @@ -498,6 +498,12 @@ def test_analyze_conditional_percentage(): {"name": "Task 3", "module": "debug"}, # No condition {"name": "Task 4", "module": "debug"}, # No condition ], + "mermaid": [ + {"name": "Task 1", "debug": {}, "when": "condition1"}, + {"name": "Task 2", "debug": {}, "when": "condition2"}, + {"name": "Task 3", "debug": {}}, + {"name": "Task 4", "debug": {}}, + ], } ], "handlers": [], From 1d29c742dd2e5590b11fccfd04c3a17fa4e73ae0 Mon Sep 17 00:00:00 2001 From: Jier Date: Sun, 13 Sep 2026 22:01:03 +0200 Subject: [PATCH 2/2] update stale agent, readme file and smart defaults --- AGENTS.md | 8 +++---- README.md | 26 +++++++++++++++++++---- tests/defaults/test_smart_defaults_cli.py | 8 +++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1c018dd..48eba18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,10 +34,10 @@ npx --yes jscpd docsible --pattern "**/*.py" `jscpd` is informational: its current baseline is 21 clones and 1.30% duplicated lines, not a zero-threshold gate. Update the baseline only after reviewing intentional duplication. -## Verified Baseline (2026-08-27) +## Verified Baseline (2026-09-13) -- `uv run pytest`: 1156 passed, 10 warnings. -- `uv run ruff check .`: 47 findings. -- `uv run mypy docsible`: 3 errors in 2 files. +- `uv run pytest` passes with 1215 tests (3 xpassed). +- `uv run ruff check .` reports no findings. +- `uv run mypy docsible` reports no issues. Treat these results as a starting point, not permission to introduce additional failures. diff --git a/README.md b/README.md index 8dc30e5..20547a5 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,10 @@ Project home: https://github.com/jier/docsible - Preset system — four built-in presets covering personal, team, enterprise, and consulting use cases - Suppression system — silence false-positive recommendations with audit trail and optional expiry - Interactive setup wizard (`docsible init`) with optional CI/CD workflow generation +- Source-backed `RoleExecutionGraph` — typed include/import, cross-role, notification and variable edges, each with static / dynamic / unknown resolution and a source location +- README "Execution Routes" + "Execution Graph Summary" — role entry point, statically vs dynamically reachable and unreachable files, and dynamic boundaries, instead of filesystem-order phases +- Collections get per-role documentation plus a collection-level complexity overview and a role index sorted by complexity +- Machine-readable `--output-format json` exposes the complexity metrics and the serialized execution graph for CI and downstream renderers ## Installation @@ -125,7 +129,8 @@ docsible scan collection . --fail-on warning --output-format json ### `--output-format json` -Use `--output-format json` with `docsible analyze role` for machine-readable output: +Use `--output-format json` with `docsible analyze role` (also supported by +`validate role` and `document role`) for machine-readable output: ```bash docsible analyze role --role . --output-format json @@ -137,13 +142,26 @@ Output schema: { "role": "my-role", "findings": [ - { "severity": "WARNING", "message": "No example playbook found", "category": "documentation" } + { "severity": "warning", "message": "No example playbook found", "category": "documentation" } ], - "summary": { "total": 3, "critical": 0, "warning": 2, "info": 1 }, - "truncated": false + "summary": { "total": 3, "shown": 3, "critical": 0, "warning": 2, "info": 1 }, + "truncated": false, + "complexity": { + "total_tasks": 3, "task_files": 1, "handlers": 0, + "task_includes": 0, "conditional_tasks": 1, "error_handlers": 0, + "static_reachable_task_files": 1, "dynamically_reachable_task_files": 0, + "unreachable_task_files": 0, "dynamic_boundaries": 0, "loop_tasks": 0, + "notification_edges": 0, "collection_dependencies": 0 + }, + "execution_graph": { "role_id": "role:my-role", "nodes": ["..."], "edges": ["..."] } } ``` +`complexity` is derived from the `RoleExecutionGraph` (the single source of +truth for boundary, loop, notification and reachability counts), and +`execution_graph` is the full serialized node/edge model. `truncated` applies +to `findings`; the graph itself is never truncated. + ### Ready-to-use CI examples See [`examples/ci_pipeline/`](examples/ci_pipeline/) for complete, ready-to-use configurations: diff --git a/tests/defaults/test_smart_defaults_cli.py b/tests/defaults/test_smart_defaults_cli.py index 8521b47..5a4bc10 100644 --- a/tests/defaults/test_smart_defaults_cli.py +++ b/tests/defaults/test_smart_defaults_cli.py @@ -49,13 +49,13 @@ def test_complex_role_gets_graphs_by_default(self, complex_role_fixture, tmp_pat # Complex role should have visualization enabled (smart default) content = output_file.read_text() - # Complex roles may use either Mermaid diagrams OR execution phases + # Complex roles may use either Mermaid diagrams OR execution routes has_mermaid = "```mermaid" in content - has_execution_phases = "Execution Phases" in content + has_execution_routes = "Execution Routes" in content has_architecture = "Architecture Overview" in content - assert has_mermaid or (has_execution_phases and has_architecture), \ - f"Complex role should have visualization (mermaid: {has_mermaid}, phases: {has_execution_phases}, arch: {has_architecture})" + assert has_mermaid or (has_execution_routes and has_architecture), \ + f"Complex role should have visualization (mermaid: {has_mermaid}, routes: {has_execution_routes}, arch: {has_architecture})" def test_user_override_respected(self, simple_role, tmp_path): """User --graph flag should override smart default."""