Generalise sweep member-key derivation beyond the two hardcoded grid axes #105

Closed
opened 2026-06-20 20:00:51 +02:00 by Brummel · 2 comments
Owner

sweep_member_key (crates/aura-cli/src/main.rs:186-196) derives a family
member's on-disk trace key from two hard-coded axis paths,
signals.trend.fast.length and signals.trend.slow.length, rendering
f{fast}s{slow}. This is deterministic and collision-free over today's
built-in grid
(the only grid that exists — it varies exactly those two axes;
the rest are singletons), and it panics (naming the axis) if either listed axis
is absent.

The latent foot-gun: the key is collision-free only over that exact grid, not
over "a sweep" generically. If a future grid varies a different axis (e.g.
exposure.scale, or a momentum leg) while keeping trend.fast/trend.slow
fixed, two distinct grid points map to the same f…s… dir and silently
overwrite each other's traces. The panic guards a missing listed axis, not an
unlisted varying one.

This is not a current defect (there is one grid, and it is covered), so it is
filed rather than fixed in-cycle. When the sweep grid generalises, the key
derivation should become collision-free for any set of varying axes — e.g.
derive it from the full set of swept (varying) param values, or from a stable
encoding of the whole grid point, rather than two named axes.

Surfaced by the 0058 cycle-close architect review (family-member trace
persistence, #104). Severity: medium / latent.

context: depends on the sweep grid generalising beyond the built-in demo axes.

`sweep_member_key` (`crates/aura-cli/src/main.rs:186-196`) derives a family member's on-disk trace key from two hard-coded axis paths, `signals.trend.fast.length` and `signals.trend.slow.length`, rendering `f{fast}s{slow}`. This is deterministic and collision-free **over today's built-in grid** (the only grid that exists — it varies exactly those two axes; the rest are singletons), and it panics (naming the axis) if either listed axis is absent. The latent foot-gun: the key is collision-free only over *that exact* grid, not over "a sweep" generically. If a future grid varies a **different** axis (e.g. `exposure.scale`, or a momentum leg) while keeping `trend.fast`/`trend.slow` fixed, two distinct grid points map to the **same** `f…s…` dir and silently overwrite each other's traces. The panic guards a *missing listed* axis, not an *unlisted varying* one. This is not a current defect (there is one grid, and it is covered), so it is filed rather than fixed in-cycle. When the sweep grid generalises, the key derivation should become collision-free for *any* set of varying axes — e.g. derive it from the full set of swept (varying) param values, or from a stable encoding of the whole grid point, rather than two named axes. Surfaced by the 0058 cycle-close architect review (family-member trace persistence, #104). Severity: medium / latent. context: depends on the sweep grid generalising beyond the built-in demo axes.
Brummel added the idea label 2026-06-20 20:00:51 +02:00
Author
Owner

Design record + reconciliation (specify entry, /boss)

This issue is the reference issue for the cycle that fixes the foot-gun it
describes AND ships a demo strategy proving the fix is generic. The design was
settled in an in-context discussion, approved by the user ("mach es so!"), then
refined with the portability constraint below. Recording the resolved
load-bearing forks before the spec is written.

Scope. Two coupled deliverables, in this order:

  1. Generalise the family-member trace key so it is collision-free for ANY grid,
    not just the two hardcoded axes signals.trend.fast.length /
    signals.trend.slow.length.
  2. A new demo strategy with >2 params incl. a bool, swept generically — the
    empirical proof that (1) is generic.

Fork: key derivation source -> the key names the varying axes (grid axes
with >1 value), in param-space slot order, with their values.
Basis: derived. Varying axes are the only thing that distinguishes members
within a family; pinned singletons are constant and carry no information.
Which-axis-varies is grid-global knowledge (axis value-counts live in
GridSpace), so the varying mask is computed where the grid is known and the
per-member key is built from it. Decouples the key from any specific axis name
-> generic over any strategy.

Fork: on-disk key format (portability) -> the key is a filesystem-conformant
directory name on all major OSes (Linux, Windows, macOS). Charset restricted to
[A-Za-z0-9._-]. Per varying axis a token name-value; tokens joined by _;
any char outside the portable set is sanitised. Values are rendered caselessly
(integers; bool as true/false; decimal floats with NO scientific notation,
matching Rust's f64 Display) so two members of one family can never differ only
by case -> no silent overwrite on case-insensitive filesystems (NTFS/APFS
default). Key length is capped to the per-component / path limit. This replaces
the earlier name=value,... sketch, whose = / , were the user's portability
concern (bare-FS-legal but not robust under cloud-sync/tooling).
Basis: user constraint ("konforme Verzeichnis-namen fuer alle wichtigen
Betriebssysteme"), made concrete + derived case-insensitive-FS safety.

Fork: demo strategy shape -> EMA-distance momentum:
price -> Ema(length:i64) -> Sub(price - ema) -> Exposure(scale:f64) -> LongOnly(enabled:bool) -> SimBroker. LongOnly is a new node carrying the bool
param (true -> clamp short exposure to >=0; false -> pass-through). Three swept
params of three kinds incl. a bool; genuinely different from the SMA-cross demo.
The new strategy's sweep, persisted with --trace, yields member dirs whose
names carry the bool (e.g. ..._longonly.enabled-true), which is the live proof
the generic key works.
Basis: user-approved ("Bau eine Strategie ... mehr als zwei und ein bool als
Parameter").

Provenance: forks settled in-context with the user and approved via /boss mach es so! plus the portability refinement; recorded here as the run's decision log.

## Design record + reconciliation (specify entry, /boss) This issue is the reference issue for the cycle that fixes the foot-gun it describes AND ships a demo strategy proving the fix is generic. The design was settled in an in-context discussion, approved by the user ("mach es so!"), then refined with the portability constraint below. Recording the resolved load-bearing forks before the spec is written. **Scope.** Two coupled deliverables, in this order: 1. Generalise the family-member trace key so it is collision-free for ANY grid, not just the two hardcoded axes `signals.trend.fast.length` / `signals.trend.slow.length`. 2. A new demo strategy with >2 params incl. a bool, swept generically — the empirical proof that (1) is generic. **Fork: key derivation source** -> the key names the *varying* axes (grid axes with >1 value), in param-space slot order, with their values. Basis: derived. Varying axes are the only thing that distinguishes members within a family; pinned singletons are constant and carry no information. Which-axis-varies is grid-global knowledge (axis value-counts live in `GridSpace`), so the varying mask is computed where the grid is known and the per-member key is built from it. Decouples the key from any specific axis name -> generic over any strategy. **Fork: on-disk key format (portability)** -> the key is a filesystem-conformant directory name on all major OSes (Linux, Windows, macOS). Charset restricted to `[A-Za-z0-9._-]`. Per varying axis a token `name-value`; tokens joined by `_`; any char outside the portable set is sanitised. Values are rendered caselessly (integers; bool as `true`/`false`; decimal floats with NO scientific notation, matching Rust's f64 `Display`) so two members of one family can never differ only by case -> no silent overwrite on case-insensitive filesystems (NTFS/APFS default). Key length is capped to the per-component / path limit. This replaces the earlier `name=value,...` sketch, whose `=` / `,` were the user's portability concern (bare-FS-legal but not robust under cloud-sync/tooling). Basis: user constraint ("konforme Verzeichnis-namen fuer alle wichtigen Betriebssysteme"), made concrete + derived case-insensitive-FS safety. **Fork: demo strategy shape** -> EMA-distance momentum: `price -> Ema(length:i64) -> Sub(price - ema) -> Exposure(scale:f64) -> LongOnly(enabled:bool) -> SimBroker`. `LongOnly` is a new node carrying the bool param (true -> clamp short exposure to >=0; false -> pass-through). Three swept params of three kinds incl. a bool; genuinely different from the SMA-cross demo. The new strategy's sweep, persisted with `--trace`, yields member dirs whose names carry the bool (e.g. `..._longonly.enabled-true`), which is the live proof the generic key works. Basis: user-approved ("Bau eine Strategie ... mehr als zwei und ein bool als Parameter"). Provenance: forks settled in-context with the user and approved via `/boss mach es so!` plus the portability refinement; recorded here as the run's decision log.
Author
Owner

Resolved — cycle 0059 (audit drift-clean)

The foot-gun is fixed and proven generic. The sweep member-key is no longer
derived from two hardcoded axis names; it is derived from the axes that actually
VARY (SweepBinder::varying_axes) and rendered as a filesystem-conformant
directory component:

  • charset [A-Za-z0-9._-] (valid on Linux/Windows/macOS, URL- and cloud-sync-safe);
  • case-less values (i64/timestamp decimal, bool true/false, f64 decimal no
    sci-notation) -> two members of one family never differ only by case -> no
    silent overwrite on case-insensitive filesystems (NTFS/APFS);
  • length-capped (200) with a version-stable FNV-1a fallback for the unbounded-grid edge.

Proven generic by a new demo strategy whose params are unlike the SMA-cross
demo, incl. a bool: momentum (Ema -> Sub(price-ema) -> Exposure -> LongOnly ->
SimBroker), swept via aura sweep --strategy momentum. Member dirs now read e.g.
ema.length-5_exposure.scale-0.5_longonly.enabled-true (the bool conformant in
the dir name), each chartable. LongOnly is the first aura-std node with a bool
param. The SMA sweep's dirs moved from f2s4 to the portable form too.

Commits (local, unpushed): spec a8ba019, plan 44e3a1a, impl 27f850d + 0b73a75,
audit-close b92aab7 (architect drift-clean; full workspace suite + clippy green).
The C22/#101 ledger member-key note's sweep clause was amended to the new form.

## Resolved — cycle 0059 (audit drift-clean) The foot-gun is fixed and proven generic. The sweep member-key is no longer derived from two hardcoded axis names; it is derived from the axes that actually VARY (`SweepBinder::varying_axes`) and rendered as a filesystem-conformant directory component: - charset `[A-Za-z0-9._-]` (valid on Linux/Windows/macOS, URL- and cloud-sync-safe); - case-less values (i64/timestamp decimal, bool true/false, f64 decimal no sci-notation) -> two members of one family never differ only by case -> no silent overwrite on case-insensitive filesystems (NTFS/APFS); - length-capped (200) with a version-stable FNV-1a fallback for the unbounded-grid edge. Proven generic by a new demo strategy whose params are unlike the SMA-cross demo, incl. a bool: `momentum` (Ema -> Sub(price-ema) -> Exposure -> LongOnly -> SimBroker), swept via `aura sweep --strategy momentum`. Member dirs now read e.g. `ema.length-5_exposure.scale-0.5_longonly.enabled-true` (the bool conformant in the dir name), each chartable. `LongOnly` is the first aura-std node with a bool *param*. The SMA sweep's dirs moved from `f2s4` to the portable form too. Commits (local, unpushed): spec a8ba019, plan 44e3a1a, impl 27f850d + 0b73a75, audit-close b92aab7 (architect drift-clean; full workspace suite + clippy green). The C22/#101 ledger member-key note's sweep clause was amended to the new form.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#105