Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions chutoro-cli/src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,10 @@ where
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let summary = ExecutionSummary {
/// data_source: "demo".into(),
/// result: ClusteringResult::from_assignments(vec![
/// result: ClusteringResult::try_from_assignments(vec![
/// ClusterId::new(0),
/// ClusterId::new(1),
/// ]),
/// ])?,
/// };
/// let mut buffer = Cursor::new(Vec::new());
/// render_summary(&summary, &mut buffer)?;
Expand Down
4 changes: 2 additions & 2 deletions chutoro-cli/src/cli/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,10 @@ fn run_command_rejects_zero_min_cluster_size() -> TestResult {
fn render_summary_outputs_assignments() -> TestResult {
let summary = ExecutionSummary {
data_source: "demo".into(),
result: ClusteringResult::from_assignments(vec![
result: ClusteringResult::try_from_assignments(vec![
chutoro_core::ClusterId::new(0),
chutoro_core::ClusterId::new(1),
]),
])?,
};
let mut buffer = Vec::new();
render_summary(&summary, &mut buffer)?;
Expand Down
25 changes: 15 additions & 10 deletions chutoro-core/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ fn exceeds_pointer_width(value: u64) -> bool {
/// ```
/// use chutoro_core::{ClusteringResult, ClusterId};
///
/// let result = ClusteringResult::from_assignments(vec![ClusterId::new(0), ClusterId::new(1)]);
/// let result = ClusteringResult::try_from_assignments(vec![
/// ClusterId::new(0),
/// ClusterId::new(1),
/// ])
/// .expect("assignments are contiguous");
/// assert_eq!(result.assignments().len(), 2);
/// assert_eq!(result.cluster_count(), 2);
/// ```
Expand Down Expand Up @@ -52,15 +56,14 @@ impl ClusteringResult {
/// Cluster identifiers must start at zero and be contiguous. Use
/// [`Self::try_from_assignments`] to handle arbitrary identifiers.
///
/// # Examples
/// ```
/// use chutoro_core::{ClusteringResult, ClusterId};
/// # Panics
///
/// let result = ClusteringResult::from_assignments(vec![ClusterId::new(0)]);
/// assert_eq!(result.cluster_count(), 1);
/// ```
/// Panics when identifiers do not start at zero and are not contiguous.
/// Use [`Self::try_from_assignments`] as the public fallible constructor
/// for untrusted input.
#[cfg(feature = "cpu")]
#[must_use]
pub fn from_assignments(assignments: Vec<ClusterId>) -> Self {
pub(crate) fn from_assignments(assignments: Vec<ClusterId>) -> Self {
match Self::try_from_assignments(assignments) {
Ok(result) => result,
Err(err) => panic!("cluster identifiers must start at zero and be contiguous: {err}"),
Expand Down Expand Up @@ -146,7 +149,8 @@ impl ClusteringResult {
/// ```
/// use chutoro_core::{ClusteringResult, ClusterId};
///
/// let result = ClusteringResult::from_assignments(vec![ClusterId::new(0)]);
/// let result = ClusteringResult::try_from_assignments(vec![ClusterId::new(0)])
/// .expect("assignments are contiguous");
/// assert_eq!(result.assignments()[0].get(), 0);
/// ```
#[must_use]
Expand All @@ -160,7 +164,8 @@ impl ClusteringResult {
/// ```
/// use chutoro_core::{ClusteringResult, ClusterId};
///
/// let result = ClusteringResult::from_assignments(vec![ClusterId::new(0), ClusterId::new(0)]);
/// let result = ClusteringResult::try_from_assignments(vec![ClusterId::new(0), ClusterId::new(0)])
/// .expect("assignments are contiguous");
/// assert_eq!(result.cluster_count(), 1);
/// ```
#[must_use]
Expand Down
3 changes: 2 additions & 1 deletion chutoro-core/tests/chutoro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ fn cluster_count_matches_unique_assignments(
#[case] assignments: Vec<ClusterId>,
#[case] expected: usize,
) {
let result = ClusteringResult::from_assignments(assignments);
let result = ClusteringResult::try_from_assignments(assignments)
.expect("test assignments must be contiguous");
assert_eq!(result.cluster_count(), expected);
}

Expand Down
7 changes: 7 additions & 0 deletions chutoro-core/tests/result_api_surface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Compile-time checks for the public `ClusteringResult` API surface.

#[test]
fn clustering_result_panicking_constructor_is_internal() {
let cases = trybuild::TestCases::new();
cases.compile_fail("tests/trybuild/clustering_result_from_assignments_private.rs");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Compile-fail fixture proving the panicking constructor is crate-private.

use chutoro_core::{ClusterId, ClusteringResult};

fn main() {
let _ = ClusteringResult::from_assignments(vec![ClusterId::new(0)]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
error[E0624]: associated function `from_assignments` is private
--> tests/trybuild/clustering_result_from_assignments_private.rs:6:31
|
6 | let _ = ClusteringResult::from_assignments(vec![ClusterId::new(0)]);
| ^^^^^^^^^^^^^^^^ private associated function
|
::: src/result.rs
|
| pub(crate) fn from_assignments(assignments: Vec<ClusterId>) -> Self {
| ------------------------------------------------------------------- private associated function defined here
14 changes: 8 additions & 6 deletions docs/chutoro-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1900,12 +1900,14 @@ accelerator backend; requesting `GpuPreferred` continues to surface
#13).

`ClusteringResult` caches the number of unique clusters and exposes
`try_from_assignments` so callers can surface non-contiguous identifiers
instead of panicking. The helper returns a `NonContiguousClusterIds` enum to
differentiate missing zero, gap, and overflow conditions. The CPU pipeline
emits contiguous identifiers by construction, keeping the result surface stable
while allowing future work to introduce explicit noise modelling or membership
probabilities without breaking the identifier invariants.
`try_from_assignments` as its public fallible constructor so callers can surface
non-contiguous identifiers instead of panicking. The constructor returns a
`NonContiguousClusterIds` enum to differentiate missing zero, gap, duplicate,
and overflow conditions. The CPU pipeline emits contiguous identifiers by
construction and may use the `pub(crate)` `from_assignments` convenience,
keeping the result surface stable while allowing future work to introduce
explicit noise modelling or membership probabilities without breaking the
identifier invariants.

```

Expand Down
119 changes: 119 additions & 0 deletions docs/debugging/debugging-plan-2026-08-24T153310+0200.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Debugging Plan: Benchmark Smoke Gate Timeout

**Generated**: 2026-08-24T15:33:10+02:00
**Issue ID**: #137 validation blocker
**Severity**: Medium
**Falsification sub-agent**: alchemist
**Planning agent boundary**: This document was prepared by the planning agent.
Falsification must be executed by the named sub-agent, not by the planning
agent.

## Problem Statement

The full `make test` gate for the fallible `ClusteringResult` constructor
change timed out after 300 seconds in the unrelated
`benchmark_binaries_cover_discovery_and_exact_smoke_paths` integration test.
The preceding 172 tests passed. This prevents the required CodeRabbit review
and commit, while the constructor change itself has not altered benchmark code.

## Context Summary

| Aspect | Details |
| ------------------- | ---------------------------------------------------------------------- |
| First observed | 2026-08-24 during the first full validation run |
| Reproduction rate | One observation; no isolated rerun yet |
| Affected components | `chutoro-benches` benchmark smoke test and nested `cargo bench` calls |
| Recent changes | This branch only changes result construction, tests, and documentation |

### Error Artefacts

```plaintext
TIMEOUT [300.030s] (173/1087)
chutoro-benches::benchmark_smoke::benchmark_binaries_cover_discovery_and_exact_smoke_paths

Summary [452.751s] 173/1087 tests run: 172 passed, 1 timed out, 1 skipped
```

### Information Gaps

- The nested `cargo bench` child process did not produce its own diagnostic.
- Several other Chutoro worktrees were running `make test` concurrently when
the timeout occurred.

______________________________________________________________________

## Hypotheses

### H1: Concurrent Chutoro test runs exhaust the benchmark smoke budget

**Claim**: Simultaneous workspace test runs contend for CPU, disk, Cargo locks,
or benchmark resources, preventing the smoke test's five nested `cargo bench`
calls from finishing within its configured 300-second timeout.

**Plausibility**: High — the process list showed multiple concurrent
`cargo-nextest` and benchmark smoke runs, while the nextest override reserves
eight threads only within one process.

**Prediction**: The isolated smoke test will complete within 300 seconds after
all other Chutoro `cargo-nextest` and benchmark smoke processes finish.

#### H1 Falsification Plan

| Step | Action | Expected Negative Result |
| ---- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| 1 | Wait until no other Chutoro `cargo-nextest` or benchmark smoke process is active. | Active processes remain, so the experiment is deferred. |
| 2 | Run the single smoke test through nextest once. | It still times out or fails below 300 seconds, disproving contention as a sufficient explanation. |

**Tooling**: `ps`, `pgrep`, and
`cargo nextest run -p chutoro-benches -E
'test(benchmark_binaries_cover_discovery_and_exact_smoke_paths)'`.

**Confidence on falsification**: High if the system is idle of matching
processes at test start.

______________________________________________________________________

### H2: The benchmark smoke test has an intrinsic regression or stale artefact

**Claim**: One of the smoke test's nested `cargo bench` invocations hangs or
exceeds 300 seconds independently of test-run contention.

**Plausibility**: Medium — the test performs three discovery and two exact
benchmark invocations, but this branch has not changed it.

**Prediction**: The isolated test will time out again when no competing Chutoro
test or benchmark process is active.

#### H2 Falsification Plan

| Step | Action | Expected Negative Result |
| ---- | ----------------------------------------------------------------------- | -------------------------------------------------------------- |
| 1 | Execute the same single nextest selection after H1's idle precondition. | It passes within 300 seconds, disproving an intrinsic timeout. |

**Tooling**: `cargo nextest` and the nextest timeout output.

**Confidence on falsification**: High; the selected test reproduces its full
nested benchmark workflow.

______________________________________________________________________

## Recommended Execution Order

1. **H1** — It has the cheapest decisive precondition check and directly
addresses the observed concurrent test runs.
2. **H2** — The same isolated run falsifies or retains the intrinsic-regression
hypothesis once H1's precondition is met.

## Termination Criteria

- **Root cause identified**: The isolated run either passes after contention
clears, or it repeats the timeout while idle.
- **Escalation trigger**: If the isolated run fails while idle, retain its
output and revise this plan before changing benchmark code or timeout policy.

## Notes for Executing Agent

- Perform only the single selected smoke-test experiment; do not run full
repository gates, edit files, or terminate another process.
- Record the idle-process observation, command, elapsed time, and verdict as
falsified, not-falsified, or inconclusive.
124 changes: 124 additions & 0 deletions docs/debugging/debugging-plan-2026-08-24T155013+0200.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Debugging Plan: CPU-disabled Session Fixture Diagnostic Drift

**Generated**: 2026-08-24T15:50:13+02:00
**Issue ID**: #137 validation blocker
**Severity**: Medium
**Falsification sub-agent**: alchemist
**Planning agent boundary**: This document was prepared by the planning agent.
Falsification must be executed by the named sub-agent, not by the planning
agent.

## Problem Statement

The full workspace test run fails in
`session_api_is_unavailable_without_cpu_feature`. Its CPU-disabled fixture
correctly exits unsuccessfully, but the test no longer finds the exact
compiler-diagnostic phrases it expects. The failure is unrelated to the fallible
`ClusteringResult` constructor and prevents the required gate from completing.

## Context Summary

| Aspect | Details |
| ------------------- | -------------------------------------------------------- |
| First observed | 2026-08-24 during the second full validation run |
| Reproduction rate | One full-gate observation |
| Affected components | CPU-disabled session API fixture assertion |
| Recent changes | This branch does not modify session APIs or this fixture |

### Error Artefacts

```plaintext
assertion failed:
stderr.contains("cannot find type `SessionConfig` in crate `chutoro_core`")
```

### Information Gaps

- The test captures its child process output without displaying the actual
compiler diagnostic on assertion failure.

______________________________________________________________________

## Hypotheses

### H1: The fixture remains invalid but rustc wording drifted

**Claim**: The fixture's CPU-disabled references still cause compilation to
fail, but the installed compiler uses different wording for one or more missing
session API diagnostics.

**Plausibility**: High — the child command returned unsuccessfully before the
test asserted on its captured text, and the assertion specifically failed on a
string match.

**Prediction**: Running the fixture's exact `cargo check` command returns a
non-zero status and stderr contains a stable missing-session symbol diagnostic
whose wording differs from the asserted phrase.

#### H1 Falsification Plan

| Step | Action | Expected Negative Result |
| ---- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| 1 | Run the fixture's exact CPU-disabled `cargo check` once with a unique temporary target directory. | A successful exit disproves diagnostic-only drift. |
| 2 | Inspect the captured stderr for missing session API symbols. | The original asserted phrase is present, disproving wording drift for that assertion. |

**Tooling**: `cargo check`, the fixture manifest, and a temporary target
directory under `/tmp`.

**Confidence on falsification**: High; it executes the same fixture contract
without the parent assertion masking compiler output.

______________________________________________________________________

### H2: Inherited `RUSTFLAGS` stops the fixture before its API errors

**Claim**: `make test` supplies `RUSTFLAGS="-D warnings"`, and the parent test
inherits that variable into its child `cargo check`; an earlier warning
promoted to an error prevents the fixture from reaching the expected
missing-session API diagnostics.

**Plausibility**: High — H1 showed the same command produces every expected
phrase without inherited `RUSTFLAGS`, while the failure only appears inside the
full Makefile gate.

**Prediction**: The exact direct fixture command with `RUSTFLAGS="-D warnings"`
will exit unsuccessfully and omit at least one expected session API phrase,
instead reporting an earlier promoted warning.

#### H2 Falsification Plan

| Step | Action | Expected Negative Result |
| ---- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| 1 | Run the fixture's exact CPU-disabled check once with `RUSTFLAGS="-D warnings"` and a unique target directory. | All four expected API phrases remain present, disproving inherited flags as the cause. |

**Tooling**: `cargo check`, the fixture manifest, `RUSTFLAGS`, and a temporary
target directory under `/tmp`.

**Confidence on falsification**: High; it adds only the environment difference
between the successful direct compile and the parent gate.

**Outcome**: Not falsified. With `RUSTFLAGS="-D warnings"`, the CPU-disabled
build stops at `from_assignments` being unused before compiling the fixture.
The helper is only called by the CPU pipeline, so it must be gated with the same
`cpu` feature.

______________________________________________________________________

## Recommended Execution Order

1. **H1** — Falsified: the direct fixture compile emitted all expected phrases.
2. **H2** — Isolate the remaining `RUSTFLAGS` difference.

## Termination Criteria

- **Root cause identified**: The matching-environment compile reproduces or
excludes an earlier promoted warning.
- **Escalation trigger**: If H2 is falsified, revise this plan before changing
the session API test.

## Notes for Executing Agent

- Run only the supplied fixture command and inspect its captured output.
- Do not edit files, run repository gates, or remove temporary directories.
- Return the exit status, relevant diagnostic excerpts, and a falsified,
not-falsified, or inconclusive verdict.
Loading
Loading