Tracker sweep + executor robustness: net-R chain, archive inventory, std::grid, per-cell fault isolation (12 issues) #270

Merged
claude merged 25 commits from worktree-boss-tracker-sweep-258 into main 2026-07-14 17:12:58 +02:00

25 Commits

Author SHA1 Message Date
claude f0aadb54f8 audit: cycle-close tidy for the #256/#272 latecomer block
Drift review (architect) over a55e4cf..HEAD found the cycle substantially
clean — C25 closed vocabulary, C1 determinism/behaviour-preservation, the
additive-serde widening, exhaustive StageBlock matches, and the uniform exit-3
convention all hold. Two drift items resolved:

- RATIFY: the `SilencedPanic` member-boundary panic-hook silencer (added by the
  #272 implementer, not named in the spec) is a legitimate mechanism — it keeps
  "recorded, campaign continues" observably true on stderr by suppressing the
  default crash backtrace around each contained `catch_unwind`. Its mutex
  serialises only the ref-count/hook-swap (O(1)), never member computation, so
  C1 disjoint-parallel determinism is preserved. Documented in the ledger's
  #272 realization paragraph rather than left as undocumented global state.
- FIX: `cell_fault_kind_label` hand-wrote the snake_case strings that must match
  `CellFaultKind`'s serde `rename_all` (two sources of truth an aggregate over
  campaign_runs.jsonl could silently diverge from). Pinned with a test asserting
  each label equals the serialized form; the efficient `&'static str` stays.

(The #272 commit's own concern-driven doc fixes and the wf panic-containment
test landed in d3b1a1a; this commit carries only the two audit-phase items.)

Suite: cargo test --workspace green (1311 tests, 0 failed); clippy clean.
2026-07-14 17:01:36 +02:00
claude d3b1a1aead feat(campaign,registry,cli): per-cell fault isolation — a failed cell is recorded, never a global abort
closes #272

A member fault (no-data, bind, run, or a caught panic) is now a recorded
per-cell outcome instead of aborting the whole campaign and discarding every
already-computed cell. The incident that motivated this (a 22-instrument
campaign lost ~36 healthy cells ~6.7 min in because Copper had an archive gap)
now completes: the healthy cells persist, the gap cell is recorded as failed,
and the run exits 3.

Direction (owner decision 2026-07-14): run to completion and report
compromised results; no coverage preflight, no window synthesis.

Containment granularity:
- The CELL for a sweep-stage member fault (a grid hole structurally
  compromises winner selection, so the whole cell fails).
- The FOLD for a walk_forward member fault (independent time windows): the
  surviving folds pool into the family, failed folds are recorded as
  StageRealization.window_faults, and the summary names the ratio.

- aura-registry: additive CellFault / CellFaultKind (closed:
  no_data|bind|run|panic|window) / WindowFault / CellCoverage, plus
  fault/coverage fields on CellRealization and window_faults on
  StageRealization — all serde-default-skipped, so pre-#272 campaign_runs
  lines parse and round-trip byte-identical.
- aura-campaign: run_cell returns a fault-annotated CellRealization instead of
  Err (execute's accumulate-then-append-once tail is unchanged and now
  persists every healthy cell + the one run record); a `contain` split keeps
  ExecFault::Registry and doc-shape preflight faults global while Member/Window
  become per-cell/per-fold. Member panics are caught with
  catch_unwind(AssertUnwindSafe) at all three member-run sites (sweep IS/OOS)
  and recorded as MemberFault::Panic — a member panic no longer aborts the
  process. The wf stage partitions Registry faults (global) from Member/Window
  (per-fold) and filters faulted-fold placeholders (the
  "faulted-member-placeholder" broker sentinel) out of the persisted family.
- aura-cli: exec_fault_prose gains the Panic arm; CliMemberRunner::window_coverage
  derives effective bounds + interior gap months from the #264 archive
  primitives; present_campaign prints per-cell failure notes + a completion
  summary and threads the failed-cell count; a run with >=1 failed cell exits 3
  ("completed with failed cells") uniformly across `aura campaign run` and the
  dissolved sweep/walkforward/mc/generalize verbs (exit_on_campaign_result).
  Usage stays 2, refused-before-running stays 1, clean stays 0.

Tests: the global-abort pins flip to containment (execute + the two wf fault
tests → fold-containment + all-folds-fail-the-cell); new panic-containment
tests on both the sweep path (PanicRunner) and the wf path (this commit adds
the wf mirror the loop left uncovered); a new gapped-archive e2e (one covered
cell + one gap cell → exit 3); the ~14 CLI exit-1 pins move to the exit-3
register; a pre-#272-line byte-identical round-trip guard.

Suite: cargo test --workspace green (1309 tests, 0 failed); clippy clean.
Decision log: #272 comments (fork rationale, the fold Registry/Member split,
the placeholder sentinel, uniform exit-3).

Follow-up (minor, not blocking): the plan under-scoped Task 1 to aura-registry
though the additive fields also touch aura-campaign's exec.rs literals — the
loop absorbed it mechanically; a future plan for a cross-crate additive-field
change should scope every crate's construction sites in the first task.
2026-07-14 16:51:31 +02:00
claude ea4e79d73f feat(research,campaign,cli): std::grid — the enumerate-only leading stage
closes #256

Fork B (owner decision 2026-07-14): the dissolved walkforward/mc
translations' leading sweep executed the full grid over the whole campaign
window and persisted a Sweep family, yet only the enumerated parameter
points ever crossed the stage seam (the wf stage re-sweeps them per IS
window itself). The leading stage is now the fieldless vocabulary block
std::grid: it enumerates, executes nothing, persists nothing.

- aura-research: StageBlock::Grid ({"block":"std::grid"}), schema-strict
  parse arm (empty slot list: every key but "block" is refused by the
  generic unknown-slot check), PROCESS_BLOCKS entry, intrinsic-tier no-op
  arm; the vocabulary test's non-empty-slots guard carries a pinned
  std::grid-only exception (a nominal slot would misdescribe the
  vocabulary to describe_block consumers).
- aura-campaign: the inter-stage seam is a typed two-armed StageFlow
  (points-only vs executed members); gate / mc-per-survivor fence the
  points-only arm with defensive PipelineShape faults; preflight admits
  std::grid only as the first stage and only immediately before
  std::walk_forward (every other neighbor consumes executed reports).
- aura-cli: translate_walkforward / translate_mc lead with
  StageBlock::Grid; the two family-shape E2E pins flip to zero Sweep
  families; translate_generalize keeps its executed sweep(argmax) — its
  generalize stage consumes the argmax winner report as the cell nominee.
- docs: dated #256 amendment in the ledger's verb-dissolution narrative;
  the authoring-guide vocabulary transcript gains the std::grid line.

Behaviour preservation: the exact-grade real-data pins
(walkforward_real_e2e_pins_the_exact_current_grade,
mc_r_bootstrap_real_e2e_pins_the_exact_current_grade) pass unmodified —
survivor points reach the wf stage in the same odometer order as before.

Measured (the #256 acceptance measurement; debug build, real GER40 2025,
2x2 grid, `aura walkforward --real`, 3 runs each):
  before (a55e4cf): 6.27 / 6.18 / 6.18 s
  after:            4.52 / 4.49 / 4.45 s   (~ -27% wall clock)

Suite: cargo test --workspace green (0 failed); clippy clean.
Decision log: #256 comments (fork rationale incl. the StageFlow seam and
the slot-guard exception).
2026-07-14 13:07:40 +02:00
claude a55e4cfd10 audit: trio tidy — hoist the ns-per-minute constant
Cycle-close audit over the feature trio 75bfb93..d1b3a3d (#261 #260
#262). Architect verdict: clean — C1 (both stop variants and the
per-instrument cost round-trip through the manifest), C2 (rollover-only
emission), C18 (scalar cost wire form byte-identical; VolTf additive),
the four-scalar discipline (SessionFrankfurt bakes the timezone, no
string kind), and the match-arm lockstep across all four RiskRegime/
StopRule sites all hold; commit bodies match the diff. One low drift
item — the repeated 60·10^9 ns-per-minute literal in vol_tf_stop.rs —
fixed here as NS_PER_MINUTE rather than carried.

No regression scripts are configured (the architect is the sole gate);
full workspace suite green, clippy clean. Spec and plan working files
discarded per the audit lifecycle.

refs #262
2026-07-14 03:02:54 +02:00
claude d1b3a3dd31 feat(research,composites,cli,docs): vol_tf — the timescale-matched stop regime
The second risk-regime variant vol_tf{period_minutes, length, k}
computes the vol estimator over completed time buckets: an H1 signal
gets an H1-matched stop in the research matrix instead of a hand-scaled
minute stop (the issue's k-inflation workaround retires). Additive
externally-tagged serde (stored Vol documents keep their content ids);
the executor arm wires the VolTfStop primitive via the builder+bind
chain; validate checks period_minutes/length/k positivity per regime.

The member-manifest round-trip holds for both variants (C1): vol_tf
members stamp stop_period_minutes/stop_length/stop_k and the reproduce
path re-derives the VolTf stop whenever stop_period_minutes is present
— never a silent default-Vol fallback. The Regimes slot label and the
unit notes name the new variant (period_minutes IS the stop's
timescale); glossary, authoring guide, and design ledger record the
second variant. Default regime and sugar paths byte-untouched.

Coverage: node units (rollover-only emission, bucket-delta math, the
constant-|delta| correspondence with the per-cycle regime), executor
fold, serde/validate units, the manifest round-trip unit, and a
hostless two-regime e2e (distinct ordinals; vol_tf members stamp their
timescale, vol members do not).

closes #262
2026-07-14 02:55:02 +02:00
claude 528e8f33d6 feat(std): VolTfStop — the timescale-matched volatility stop primitive
k · sqrt(EMA(delta^2, length)) over completed period_minutes buckets —
the vol regime on a coarser clock, as a fused primitive in the Resample
mold: the in-progress bucket lives in node state, the finished bucket's
close is differenced against the previous one on rollover, and the stop
distance is emitted ONCE per completed bucket (C2: a partial bucket
never exists downstream; Firing::Any consumers hold the last value).
EMA follows Ema's exact convention (SMA seed over the first length
deltas, alpha = 2/(length+1)), pinned by a constant-|delta|
correspondence test against the per-cycle vol regime. Not rostered in
the std vocabulary (stop primitives are risk-executor internals).

First slice of #262; the regime variants, executor arm, and manifest
round-trip follow.

refs #262
2026-07-14 02:08:28 +02:00
claude e00e264252 feat(research,cli): per-instrument cost factors in one campaign document
Each cost component's knob (cost_per_trade / slip_vol_mult /
carry_per_cycle) now accepts one number for every cell — today's form,
byte-identical on the wire so stored documents keep their content ids
(CostValue's custom serde, the Axis precedent) — or an instrument-keyed
map resolved per cell at member construction. A mixed-scale matrix
(GER40 ~2e4, EURUSD ~1.1) keeps consistent cost units in ONE document:
the N-copies workaround retires and generalize-under-constant-costs
becomes expressible again.

Validation is strict both ways at the intrinsic tier: a map missing a
campaign instrument, or naming one the campaign does not list, refuses
at validate with prose naming the component and instruments (no silent
default — a partial map would reproduce exactly the silent unit
inconsistency the issue reports). The instrument threads through
cost_knob/cost_nodes_for/run_blueprint_member from the cell (run side
and persist re-run side receive the same value — the C1 drift-alarm
lockstep); the manifest stamps the resolved per-cell value under the
unchanged cost[k].<knob> key, and the sugar/reproduce paths pass an
inert instrument (their specs are scalar by construction). Fork
decisions and the discarded first attempt: issue #260 comments.

New coverage: serde round-trips (map form; scalar stays bare), validate
cross-check units, both prose directions pinned, and two hostless e2es
over the synthetic SYMA+SYMB archive — per-cell resolution visible in
the member manifests, and the validate refusal.

closes #260
2026-07-14 01:20:59 +02:00
claude ca4a89864c feat(std,engine): SessionFrankfurt — the Session node enters the closed roster
A Frankfurt-baked zero-arg Session preset (open 09:00 Europe/Berlin as
literals) joins the std vocabulary as SessionFrankfurt, declaring
period_minutes: I64 (default 15) as its one scalar knob — the
single-knob roster pattern the cost nodes established. A data-only
blueprint can now reach a session/time-of-day stream (bars_since_open),
unblocking time-of-day conditioners for the confirm/refute research
model. Roster count pins bump 28 -> 29 in both conscious-act sites.

Fork decision on the issue: the preset variant over a param-vocabulary
extension — a timezone param would need a string kind, breaching the
four-scalar discipline; the preset keeps the vocabulary closed and is
the additive extension the roster-exclusion comment anticipates. The
4-arg Session::builder stays untouched; in_session/session_open_ts
stay deferred (#154).

closes #261
2026-07-14 00:02:50 +02:00
claude 89d703dd99 test: red executable-spec for #261 — SessionFrankfurt preset reachable from blueprints
A data-only op-script naming the roster id SessionFrankfurt must
resolve to a Frankfurt-baked Session preset (09:00 Europe/Berlin baked,
one period_minutes: I64 knob, bars_since_open: I64 out) and build to a
finished Composite. Fails on std_vocabulary returning None — the preset
is absent from the roster; fork decision on the issue.

refs #261
2026-07-13 23:50:48 +02:00
claude 75bfb9326f audit: batch tidy — exhaustive bind-error prose + the carry unit note
Cycle-close audit over the /boss batch 84e1075..bdafbde (#258 #259
#264 #266 #247 #265 #269). Architect verdict: substance clean — C1
byte-identity (conduit serde-skipped, pinned floats verbatim, uncosted
bit-identical), C10 one-home-for-cost (all four conduit consumers read
the already-netted series), C18 (conduit off the wire, --version and
manifest single-sourced) all hold. Two medium debt items, both fixed
here rather than carried:

- render_bind_error's catch-all Debug-framed the remaining BindError
  variants, making #269's no-identifier-on-stderr claim argv-gated
  rather than structural. The match is now exhaustive: DuplicateBinding,
  EmptyAxis, EmptyRange render as axis-usage prose; a Compile fault — a
  blueprint defect, not a usage error — renders as a prose frame with
  the compile detail explicitly labelled internal (per-variant
  CompileError prose belongs to the graph-build surface, not this
  boundary). Unit tests pin the arms.
- The #265 knob-unit notes covered constant and vol_slippage but not
  carry.carry_per_cycle, which carries the identical price-unit-vs-R
  trap; the note set and its pins now cover all three cost components.

No regression scripts are configured (the architect is the sole gate);
full workspace suite green, clippy clean. Spec and plan working files
discarded per the audit lifecycle.

refs #265 refs #269
2026-07-13 23:39:38 +02:00
claude bdafbdef59 fix(cli): unwrap the walkforward UnknownKnob Debug frame into bare prose
render_bind_error's catch-all Debug-framed UnknownKnob although its
payload is already a fully-prosed message; the variant now renders the
payload alone. The two tests that pinned the frame verbatim repoint to
the prose fragments. Exit 2 unchanged. Completes the #247 defect
family: no Rust identifier reaches a user's stderr on either verb path.

closes #269
2026-07-13 23:28:25 +02:00
claude 3d422341eb test: red executable-spec for #269 — walkforward unknown-axis rejection as bare prose
The rejection must reach stderr as the prose payload alone — no
UnknownKnob(..) Debug frame around it. Fails on today's frame; the
prose fragments already pass, so the sole pinned defect is the frame.

refs #269
2026-07-13 23:17:59 +02:00
claude e8b6878d3b feat(research,cli,docs): state cost and risk knob units in introspect and the glossary
campaign introspect --unwired now annotates the four silently
misreadable knobs with unit/semantics sub-lines: cost_per_trade is a
price-unit numerator charged as cost/|entry−stop| in R (not a cost in
R); slip_vol_mult multiplies the per-cycle vol estimate; vol.length
smooths the estimator and does not set the stop's timescale; vol.k
scales the stop distance (the risk-unit lever). Rendered additively
via a new OpenSlot.notes field, so the sibling tests pinning the slot
hint lines verbatim stay green byte-identically.

The glossary's cost-model and risk-regime entries mirror the same
semantics inline (two-sentence convention kept), the risk entry
pointing at #262 for the timescale-matched variant.

closes #265
2026-07-13 23:12:36 +02:00
claude e12b4a730b test: red executable-spec for #265 — introspect --unwired states knob units
The four silently-misreadable cost/risk knobs (cost_per_trade,
slip_vol_mult, vol.length, vol.k) must carry one-line unit/semantics
annotations in campaign introspect --unwired — blocking the price-vs-R
and length-as-timescale misreadings that produce no error today.

refs #265
2026-07-13 22:59:07 +02:00
claude 414448bd3f fix(cli): render sweep-boundary bind errors as prose, not Debug structs
The blueprint-sweep terminal rendered BindError via {:?}, leaking
KindMismatch { .. } and MissingKnob(..) Rust Debug structs where the
sibling diagnostic on the same verb ships prose. Both now render in
that register — the axis name, the expected-vs-supplied kinds resp.
the unbound knob, and the --list-axes pointer; exit stays 2.

Scoped out (same defect family, different verb path): the walkforward
path's UnknownKnob wrapper still prints its Debug-ish frame around an
already-prose payload and is pinned verbatim by two existing tests;
filed as a follow-up rather than widened into this slice.

closes #247
2026-07-13 22:52:03 +02:00
claude ce12e7f976 test: red executable-spec for #247 — sweep-boundary bind errors render as prose
Both blueprint-sweep bind faults (wrong-kind axis value; an open param
no axis covers) must print prose in the unknown-axis sibling's register
— axis name, kind/knob specifics, a --list-axes pointer — never the raw
Debug structs KindMismatch{..}/MissingKnob(..). Exit stays 2. Fails on
the Debug leakage present today, not on scaffolding.

refs #247
2026-07-13 22:35:03 +02:00
claude be9f1cef8c feat(cli): print the engine commit in --version
aura --version now prints 'aura 0.1.0 (<commit>)', the commit sourced
from the same AURA_COMMIT build provenance the run manifest stamps
(option_env! at build time, 'unknown' fallback, dirty suffix included)
— a pre-run freshness probe: a stale binary is now distinguishable from
an engine bug at the CLI surface, and research docs pinning
'Engine: aura @ <sha>' can verify the binary they invoke.

The version string is memoized as a OnceLock-leaked &'static str:
clap 4.6's builder Str accepts only &'static str (no From<String>), so
the owned-String alternative a quality pass suggested does not build —
verified, held with evidence in the loop report.

closes #266
2026-07-13 22:26:47 +02:00
claude 5a1696891a test: red executable-spec for #266 — aura --version carries the engine commit
aura --version must print 'aura 0.1.0 (<commit>)', the commit sourced
from the same AURA_COMMIT build provenance the run manifest stamps —
a pre-run freshness probe distinguishing a stale binary from an engine
bug. Fails on the absent parenthesized commit, not on scaffolding.

refs #266
2026-07-13 22:11:03 +02:00
claude 5b70bd60a5 feat(cli): aura data list — enumerate the archive's symbols
aura data list prints the archive's known symbols sorted, one per line
(DataServer::symbols() over the project-resolved archive root — the
discovery step before aura data coverage or scoping a campaign's
instrument matrix). An empty or absent archive prints a 'no symbols'
prose line and exits 0, per the verb corpus's empty-result register.
Pure report fn with unit tests; the headline e2e runs hostless over
the synthetic SYMA+SYMB archive.

Together with the coverage verb this closes the inventory gap: an
agent operating through the CLI alone can now discover instruments
and their per-month gaps before authoring a campaign.

closes #264
2026-07-13 22:06:31 +02:00
claude a7d465d2b5 test: red executable-spec for #264 cut 2 — aura data list enumerates symbols
aura data list must print the archive's symbols sorted, one per line,
exit 0 — the discovery step before aura data coverage. Hostless RED over
the synthetic SYMA+SYMB archive; fails on the absent 'list' subcommand
(clap exit 2), not on scaffolding.

refs #264
2026-07-13 21:52:32 +02:00
claude 529a8af0d6 feat(cli,ingest): aura data coverage — month-gap inventory for a symbol
aura data coverage <SYMBOL> prints the archive's monthly file-index
coverage: a span line (first..last present month) and either an explicit
'no gaps' line or one 'missing: YYYY-MM..YYYY-MM' line per interior
contiguous hole. The Copper failure mode — a campaign aborting mid-run
because a first/last-bounds check passed while the window start had no
data — is now visible before any run (the real archive reports
'Copper missing: 2017-04..2019-09'). An unknown symbol (no archive
files) refuses on stderr with exit 1; informational absence stays
prose + exit 0 per the verb corpus convention.

Mechanics: list_m1_months goes pub in aura-ingest (archive_extent
deliberately walks past gaps and cannot report them); a new two-level
'aura data' clap namespace follows the Nodes precedent; the archive
root resolves through the normal project path (paths.data override,
data-server default otherwise). Pure report fn with unit tests for the
refusal, the gapless span, and the collapsed-range shape; the headline
e2e runs hostless over a new gapped synthetic-archive fixture.

refs #264 (cut 2, aura data list, follows separately)
2026-07-13 21:49:24 +02:00
claude 28984ba73c test: red executable-spec for #264 cut 1 — aura data coverage reports month gaps
aura data coverage <SYMBOL> must print the archive's first/last present
month and each interior missing-month range collapsed to one
'missing: YYYY-MM..YYYY-MM' line — the Copper failure mode (first/last
check passes while the window start has no data) made visible before a
campaign runs. Hostless RED over a new gapped synthetic-archive fixture
(fresh_project_with_gapped_data); fails on the absent 'data' subcommand
(clap exit 2), not on scaffolding.

refs #264
2026-07-13 21:28:58 +02:00
claude bb0b0aeac2 feat(analysis,campaign,registry,cli): bootstrap net R through the process pipeline
The OOS bootstrap conduit RMetrics.trade_rs becomes net_trade_rs and
carries the COST-NETTED per-trade R (r − cost_in_r, trade order). Every
conduit consumer is thereby net-when-costed: the monte-carlo pooled-OOS
and per-survivor bootstraps, the walk-forward oos_r pooling, and the
deflation null-max — which previously compared a net observed statistic
against a gross-resampled null whenever a costed campaign selected on
net_expectancy_r. An uncosted run is bit-identical (empty cost stream
⇒ cost 0.0 per trade), so every existing golden pin stays green.

Design: one conduit, no knob — an explicit net: knob would let a costed
campaign silently produce a gross headline again (the exact misreading
of the issue's evidence). Per-member RMetrics scalar fields stay gross;
net_expectancy_r keeps its meaning. Wire shape unchanged (serde(skip)).
The net series is materialized as a separate expression in summarize_r;
the pinned-float expressions (net_sum, the SQN pair, the lockstep
r_metrics_from_rs copies) keep their tokens verbatim. Fork decisions
and rationales: issue #259 comments.

New coverage, both hostless over the synthetic SYMA archive: a costed
campaign twin shifts the pooled-OOS bootstrap (sweep→gate→wf→mc) and
every per-survivor bootstrap (sweep→mc) below its gross sibling, with
the trade population unchanged; a unit test pins the conduit as the
cost-netted series with the empty-stream degeneracy. Existing conduit
tests renamed with the field.

Verified: full workspace suite green (71 result blocks), clippy clean,
zero bare trade_rs tokens remain, ledger conduit prose updated in place.

closes #259
2026-07-13 21:21:41 +02:00
claude 1ccce9ad32 fix(test): anchor all test sandboxes on reclaimable pid-free names
Every hand-rolled test-sandbox helper keyed its directory on
std::process::id() under env::temp_dir(), so the pre-create wipe never
matched a prior run's path: one directory stranded per helper site per
test-binary invocation, >50k dirs / 15.5 GiB on the 16 GiB tmpfs by
2026-07-13.

The remedy extends commit 84e1075's knob-lab pattern to every site:
fixed, tag-keyed, PID-FREE names under the build-tree tmp anchor, each
site keeping its pre-create remove_dir_all — which now genuinely
reclaims the previous run. Integration/bench targets use
env!("CARGO_TARGET_TMPDIR"); src-internal unit-test helpers (cargo sets
that var only for integration/bench targets) derive the same anchor
from env!("CARGO_MANIFEST_DIR") + ../../target/tmp. Residue is bounded
to one directory per site, on disk instead of the tmpfs, per checkout.

One deliberate exception: project_new.rs's tmp() stays under
env::temp_dir() because two of its tests (new_outside_a_work_tree_*)
need a base genuinely detached from any git work tree, and target/
lives inside the checkout's work tree. Its name carries a per-checkout
hash discriminator instead, so concurrently running checkouts (which
share /tmp) cannot race on the fixed name while each run still
reclaims its predecessor.

tempfile-RAII was considered and rejected (issue #258 decision log):
it cannot cover the process-lifetime OnceLock scaffolds (statics never
drop), adds a dependency, and still strands kill-leftovers on the
tmpfs.

Verified: full workspace suite green (incl. the new RED regression
test temp_cwd_sandbox_does_not_leak_under_env_temp_dir), clippy clean,
sandboxes observed under target/tmp with stable names across runs.
Stale pre-fix PID-keyed dirs in /tmp are not swept by this change and
age out only by manual cleanup.

closes #258
2026-07-13 20:15:31 +02:00
claude 05565ee8a2 test: red for #258 — temp_cwd sandbox leaks one dir per run into /tmp
The helper keys its sandbox on std::process::id() under env::temp_dir(),
so the pre-create wipe never matches a prior run's path: one directory
strands per test-binary invocation on the 16 GiB tmpfs (>50k dirs by
2026-07-13). The test pins the property at one representative site: the
returned path embeds no PID and lives off env::temp_dir().

refs #258
2026-07-13 19:40:59 +02:00