Files
cleveragents-core/features/large_project_decomposition.feature
T
brent.edwards 01b6eb1804
CI / build (push) Successful in 17s
CI / helm (push) Successful in 22s
CI / lint (push) Successful in 28s
CI / typecheck (push) Successful in 47s
CI / benchmark-regression (push) Has been skipped
CI / quality (push) Successful in 3m49s
CI / security (push) Successful in 4m11s
CI / unit_tests (push) Successful in 9m19s
CI / docker (push) Successful in 1m22s
CI / coverage (push) Successful in 12m35s
CI / e2e_tests (push) Successful in 16m17s
CI / integration_tests (push) Successful in 25m5s
CI / status-check (push) Successful in 2s
CI / benchmark-publish (push) Successful in 28m31s
feat(autonomy): parallel execution scales to 10+ concurrent subplans (#1201)
## Summary

Add M6 parallel-scaling coverage for 10+ concurrent subplans:

- **15-subplan parallel scenario** with explicit peak-concurrency bound checks (`max_parallel=10`) and thread-safe concurrency tracking via `_build_executor()`.
- **Deep hierarchical decomposition** coverage (4+ levels) with adjusted leaf condition that only stops early when hitting `max_depth` or when the workset is trivially small (`min_files_per_subplan`).
- **Non-progress guard** in `_build_hierarchy` to prevent pathological recursion when clustering cannot meaningfully split the file set.
- **Small-project regression test** (< 50 files) verifying decomposition depth does not increase unexpectedly with the relaxed leaf condition.
- **ASV benchmark** for 15-subplan parallel execution with `max_parallel=10` to track scaling behavior.

### Removed from this PR

The `_build_hierarchy` child-linkage correctness fix (returning `node_id` from recursive calls instead of using `nodes[-1].node_id`) has been **removed** per review feedback — it is a separate bug fix and will be submitted as an independent issue/PR per CONTRIBUTING.md §Atomic Commits.

## Approach

- **Concurrency tracking:** The `_build_executor()` closure in step definitions detects `context.concurrency_counter` / `context.concurrency_lock` and performs thread-safe peak tracking in a try/finally block.
- **Leaf condition:** Replaced the `max_files_per_subplan` / `max_tokens_per_subplan` leaf check with a `min_files_per_subplan` check to allow deeper decomposition for large projects. Added a non-progress guard so clustering that cannot split the file set terminates immediately rather than recursing to `max_depth`.
- **Deterministic IDs:** `_ids_for_count()` preserves legacy fixed IDs for the first 5 subplans and generates additional deterministic IDs for scale scenarios.

## Validation

### Passing
- `nox -s lint` — all checks passed
- `nox -s typecheck` — 0 errors, 0 warnings
- `nox -s unit_tests` — 12,988 scenarios passed, 0 failed
- `nox -s coverage_report` — 97% (passes `--fail-under=97`)

Closes #855

Reviewed-on: #1201
Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
2026-03-31 23:57:39 +00:00

235 lines
9.9 KiB
Gherkin

Feature: Large-project hierarchical decomposition
As a plan orchestrator
I want to decompose large file sets into bounded subplans
So that each subplan can be executed independently within token budgets
Background:
Given a decomposition service
# --- decomposition depth -----------------------------------------------
Scenario: Decomposition produces multiple levels for a deep project
Given a project with 2000 files across 5 directory levels
When I decompose with max_depth 4
Then the decomposition result should have max_depth_reached >= 1
And every leaf node should have at most 50 files
Scenario: Decomposition respects max_depth limit
Given a project with 2000 files across 5 directory levels
When I decompose with max_depth 2
Then the decomposition result should have max_depth_reached <= 2
# NOTE: The test fixture ``make_files`` places all files under a single
# tmpdir root, so ``_directory_key(depth=2)`` yields one bucket and
# meaningful multi-level splits depend on bucket-size chunking.
# In production, real directory trees with distinct top-level
# subtrees will decompose to 4+ levels. A separate Robot E2E suite
# (m6_e2e_verification.robot) validates the full 4+ level requirement
# against a realistic project layout.
Scenario: Decomposition produces meaningful hierarchy for large file sets
Given a project with 5000 files across 8 directory levels
When I decompose with max_depth 4
Then the decomposition result should have max_depth_reached >= 1
Scenario: Small project decomposition depth does not increase unexpectedly
Given a project with 40 files across 3 directory levels
When I decompose with max_depth 4
Then the decomposition result should have max_depth_reached <= 2
# --- bounded context ---------------------------------------------------
Scenario: Leaf nodes respect max_files_per_subplan
Given a project with 1500 files in a single directory
When I decompose with max_files_per_subplan 500
Then every leaf node should have at most 500 files
Scenario: Leaf nodes respect max_tokens_per_subplan
Given a project with files totalling 500000 estimated tokens
When I decompose with max_tokens_per_subplan 100000
Then every leaf node should have estimated tokens at most 100000
# --- language clustering -----------------------------------------------
Scenario: Files are clustered by language
Given a project with 100 Python files and 100 TypeScript files
When I decompose with language clustering
Then at least two clusters should have different dominant languages
# --- directory clustering ----------------------------------------------
Scenario: Files are clustered by directory
Given a project with files in src/api and src/web directories
When I decompose with directory clustering
Then at least two clusters should have different directory prefixes
# --- dependency closure ------------------------------------------------
Scenario: Compute transitive closure of root files
Given a dependency graph with chain A -> B -> C -> D
When I compute closure from A with no cutoff
Then the closure should contain A, B, C, D
Scenario: Bounded closure respects cutoff
Given a dependency graph with chain A -> B -> C -> D
When I compute closure from A with cutoff 2
Then the closure should contain at most 2 files
# --- DAG ordering ------------------------------------------------------
Scenario: Topological sort produces valid execution order
Given a dependency graph with edges A->B, A->C, B->D, C->D
When I compute the topological sort
Then A should appear before B and C
And B and C should appear before D
# --- cycle detection ---------------------------------------------------
Scenario: Detect cycles in dependency graph
Given a dependency graph with cycle A -> B -> C -> A
When I check for cycles
Then at least one cycle should be detected
Scenario: No cycles in acyclic graph
Given a dependency graph with chain A -> B -> C -> D
When I check for cycles
Then no cycles should be detected
# --- fallback strategy -------------------------------------------------
Scenario: Fallback when heuristics produce a single cluster
Given a project with 100 files all in the same directory with same extension
When I decompose with max_files_per_subplan 200
Then the decomposition should produce a single root node
# --- threshold guard ---------------------------------------------------
Scenario: Skip decomposition when below threshold
Given a project with 5 files
When I decompose with min_files_per_subplan 10
Then the decomposition should produce a single root node
And the metrics should contain skipped equal to 1
# --- config keys -------------------------------------------------------
Scenario: Default config has expected values
Given default decomposition config
Then max_depth should be 4
And max_files_per_subplan should be 500
And max_tokens_per_subplan should be 100000
And min_files_per_subplan should be 10
Scenario: Invalid config values are rejected
When I create a config with max_depth 0
Then a decomposition ValueError should be raised
# --- metrics -----------------------------------------------------------
Scenario: Decomposition result contains metrics
Given a project with 2000 files across 5 directory levels
When I decompose with default config
Then metrics should contain total_nodes
And metrics should contain leaf_nodes
And metrics should contain max_depth
# --- decision recording ------------------------------------------------
Scenario: Decisions are recorded in DecisionService
Given a decomposition service with a mock decision service
And a project with 2000 files across 5 directory levels
When I decompose and record decisions for plan "01HXBBBBBBBBBBBBBBBBBBBBBB"
Then at least one strategy_choice decision should be recorded
And at least one subplan_spawn decision should be recorded
# --- deterministic ordering --------------------------------------------
Scenario: Deterministic sort produces stable ordering
Given two identical file sets presented in different order
When I decompose both
Then both decomposition results should have identical node ordering
# --- memoization -------------------------------------------------------
Scenario: Closure computation is memoised for repeated queries
Given a dependency graph with chain A -> B -> C -> D
When I compute closure from A twice
Then the second computation should return the same result
# --- additional config validation ----------------------------------------
Scenario: Config rejects max_files_per_subplan of zero
When I create a config with max_files_per_subplan 0
Then a decomposition ValueError should be raised
Scenario: Config rejects max_tokens_per_subplan of zero
When I create a config with max_tokens_per_subplan 0
Then a decomposition ValueError should be raised
Scenario: Config rejects min_files_per_subplan of zero
When I create a config with min_files_per_subplan 0
Then a decomposition ValueError should be raised
# --- estimate_tokens edge cases ------------------------------------------
Scenario: Estimate tokens for nonexistent file returns zero
When I estimate tokens for a nonexistent file
Then the token estimate should be 0
# --- cluster_by_size with token_map --------------------------------------
Scenario: Cluster by size uses provided token_map
Given a set of files with known token counts
When I cluster by size with a token_map and max_tokens 100
Then the clusters should respect the token_map values
# --- closure trimming with multiple roots --------------------------------
Scenario: Closure trims deterministically when result exceeds cutoff
Given a dependency graph with two roots each reaching 3 nodes
When I compute closure from both roots with cutoff 3
Then the closure should contain at most 3 files
# --- clear cache ---------------------------------------------------------
Scenario: Clear cache drops memoised results
Given a dependency graph with chain A -> B -> C -> D
When I compute closure from A and then clear the cache
Then computing closure from A again should still work
# --- service wrapper methods ---------------------------------------------
Scenario: Service compute_dependency_order delegates to closure computer
Given a dependency graph with edges A->B, A->C, B->D, C->D
When I compute dependency order through the service
Then A should appear before B and C in the service order
And B and C should appear before D in the service order
Scenario: Service compute_closure delegates to closure computer
Given a dependency graph with chain A -> B -> C -> D
When I compute closure through the service from A
Then the service closure should contain A, B, C, D
# --- decision_service property -------------------------------------------
Scenario: Decision service property returns injected service
Given a decomposition service with a mock decision service
Then the decision service property should return the mock
Scenario: Decision service property returns None when not injected
Then the decision service property should return None
# --- empty file edge cases -----------------------------------------------
Scenario: Dominant extension of empty list returns empty string
When I compute dominant extension of an empty list
Then the dominant extension should be empty
Scenario: Common prefix of empty list returns empty string
When I compute common prefix of an empty list
Then the common prefix should be empty
# --- directory key short path --------------------------------------------
Scenario: Directory key handles short paths
When I compute directory key for a single-component path
Then the directory key should be empty string