Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #827 +/- ##
===========================================
+ Coverage 68.52% 72.37% +3.85%
===========================================
Files 161 175 +14
Lines 13111 15757 +2646
Branches 13111 15757 +2646
===========================================
+ Hits 8984 11404 +2420
- Misses 3853 4048 +195
- Partials 274 305 +31 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
98b82af to
0d4bf03
Compare
0d4bf03 to
84defab
Compare
A seed fixes noise assignment and reports only for identical inputs with synchronous warming on a fixed build. Background warming chooses staged cells as the worker observes their priorities, so its scheduling also affects learned baselines and the first scoring cycle. State those limits on the seed and warming fields. The bit encoding places its two levels symmetrically around zero. A zero expected mean requires balanced bits; arbitrary traffic retains its bias. Align the public vector documentation and test premise with that condition without changing the encoding or any public item.
A background worker can take the only waiting cell before the test locks staging, so a long warming schedule does not ensure a nonempty waiting map. Accepting only an in-flight record would also leave no cached volume to inspect. Stop the worker through its existing shutdown handshake before enqueueing traffic and retain its handle so reconciliation still selects deferred staging. The waiting-map assertion and every volume assertion then inspect actual queued cells regardless of scheduling. Production behavior and all public item shapes remain unchanged.
…e open the ceiling The tracker holds both baselines when the clip filter retains no samples. A clip ratio of one still raises pressure, widening the ceiling on subsequent batches while CUSUM receives the raw batch mean. The algorithm previously described learning the unclipped batch, which would let the baselines chase a sustained shift and defeat the accumulation required by gradual_single_cell_cusum. Describe the implemented lockout behavior, condition both EWMA updates on retained samples, and align the recovery explanation and tracker comments. Only documentation changes; executable statements and the existing detection witness stay intact.
Allocated warming trackers consume investment capacity before they can produce scores. Preserve the deferred-warm-up decision as history while marking its online-only accounting as superseded and recording the investment-versus-production distinction. The crate keeps implementation modules crate-private and re-exports downstream types at the root. Record that flat surface in the API plan so its intended imports match the API the crate exposes.
A cell completed after the promotion pass remains in the warm-up pipeline until the next ingest promotes it. The total warming count includes that ready state, so excluding it from the competitive subset can make one health snapshot contradict itself. Count competitive ready entries alongside waiting and in-flight work. A deterministic staging witness completes a target after promotion and checks the public health fields before another ingest can promote it.
Scores and their normalisation geometry are computed before rank adaptation. Reading rank-dependent fields afterward can pair a rank-two geometry with coherence that was deliberately disabled while the batch was scored at rank one. Snapshot rank, energy ratio, and scoring geometry before adaptation, leaving the adapted rank for the next batch. A deterministic rank-change witness checks the report against the scoring state and the tracker against its next state.
Batch generation only creates random centred vectors; it does not feed trackers or reset drift state. Describing the whole injection lifecycle on that helper gives it responsibilities its body never performs. Keep the generator description local to the generator. The injection helper already documents the schedule, baseline seeding, and evidence reset that it performs.
Explicit shutdown deliberately surfaces a failed background worker to its caller, but destruction has no caller that can recover from a second panic. Joining through the same method made an earlier worker failure turn ordinary teardown into unwinding and made teardown during another unwind abort the process. Separate the shared shutdown handshake from the join policy. Explicit shutdown retains its existing failure surface, while destruction consumes a failed join and records it through tracing. A test-only failure seam drives the worker through its poisoned-staging path and proves that dropping its sentinel completes without panic.
Noise warm-up now defers cell work and creates coordination contexts lazily, selection filters dimensions before ancestor closure, and synchronous fallback drains staging before ingest returns. Leaving earlier ADR consequences phrased as current behaviour makes operational expectations contradict the code. Record the current lifecycle while preserving the original decisions, name the collected convergence test location, and remove a rendering artifact from the SVD witness source. Regenerate the affected test inventories so their labels and claims match the current sources.
The rank-change witness requires the coherence score to be the exact structural zero produced at rank one. A direct floating-point equality obscures that intent from the lint configuration even though approximate comparison would weaken the claim. Compare the score and expected zero through their bit patterns. This preserves the exact-value requirement, including the sign of zero, without suppressing the floating-point comparison lint.
The collected test module exercises crate-visible internals, so its overview now uses the repository's test classification. The ingest contract accepts the generic coordinate type, so its documentation describes coordinate observations without narrowing the method to one convenience alias.
The drift allowance is defined in slow-baseline sigma units, while epsilon protects unrelated denominators. Computing the allowance from slow variance alone keeps configured numerical stability from changing the accumulator trajectory and matches the documented detection rule.
There was a problem hiding this comment.
🟡 Changes recommended
Coordination lifecycle semantics conflict across implementation and specification, and several public documentation references are stale or ambiguous.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (6)
packages/sentinel/src/sentinel/mod.rs:1289
§5.5documents coherence scoring, not coordination-context pruning. This should point to the lifecycle pruning section and use the package-qualified source-reference form required byAGENTS.md:72-83.
packages/sentinel/src/sentinel/mod.rs:1532- This repeats the stale coherence-section reference for lifecycle pruning. Source references must be package-qualified (
AGENTS.md:72-83), and the relevant contract is §ALGO S-7.7.2.
packages/sentinel/src/sentinel/tracker.rs:105 - This source cross-reference is missing the required document and Sentinel package qualifiers (
AGENTS.md:72-83).
packages/sentinel/src/sentinel/tracker.rs:653 - This source cross-reference is missing the required document and Sentinel package qualifiers (
AGENTS.md:72-83).
packages/sentinel/src/sentinel/tracker.rs:785 - This source cross-reference is missing the required document and Sentinel package qualifiers (
AGENTS.md:72-83).
packages/sentinel/src/sentinel/tracker.rs:834 - This source cross-reference is missing the required document and Sentinel package qualifiers (
AGENTS.md:72-83).
- Files reviewed: 54/101 changed files
- Comments generated: 10
- Review effort level: Balanced
The public schedule accepts every usize depth, so narrowing the exponent can turn an extreme depth negative and grow the schedule above its root. Saturating the exponent at the signed limit preserves the documented taper, floor, and maximum across the full input domain.
Coordination firing depends on which cells score in a batch, but learned context state belongs to the online competitive membership. Building retention from that membership preserves state through quiet and one-sided batches while still destroying contexts when either subtree loses its last member.
There was a problem hiding this comment.
🟡 Changes recommended
Structural mutation counts rely on an invalid Mudlark eviction identity, causing debug panics and incorrect release reports.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (4)
packages/sentinel/src/sentinel/tracker.rs:493
- This bare source-code section reference violates the package-qualified format required by
AGENTS.md:94-100.
packages/sentinel/src/sentinel/tracker.rs:651 - This bare source-code section reference violates the package-qualified format required by
AGENTS.md:94-100.
packages/sentinel/src/sentinel/tracker.rs:785 - This bare source-code section reference violates the package-qualified format required by
AGENTS.md:94-100.
packages/sentinel/src/sentinel/tracker.rs:833 - This bare source-code section reference violates the package-qualified format required by
AGENTS.md:94-100.
- Files reviewed: 54/101 changed files
- Comments generated: 3
- Review effort level: Balanced
Source and test references need package-qualified targets that continue to resolve when the algorithm document is read outside its own context. The coordination guide likewise needs to describe lazy context creation rather than the retired chained warm-up flow. Moving the implementation plan out of the decision-record namespace leaves ADR-S-020 with one authoritative record, while the constructor documentation explains why the graph width relation is absent from runtime configuration errors.
Reset has no error channel and promises that a background resource failure will not abort the host. Raising a failed worker join through expect turns an earlier thread failure into an undocumented reset panic. Recording the join failure through the existing tracing channel lets reset rebuild the sentinel, while the documented staging-mutex poison condition remains unchanged.
A batch is scored before rank adaptation, so deriving its geometry from the evolved rank pairs its novelty value with the wrong residual degrees of freedom. Consumers can no longer reconstruct the residual energy or judge saturation from the fields beside the score. Snapshotting geometry at scoring time keeps rank, residual degrees of freedom, and saturation aligned with the batch they describe. The current rank remains available as the model prepared for the next batch.
The convergence helper cited a document that does not exist, leaving the statistical basis of its block comparison unverifiable. Its nearby numeric explanation also treated a two-block difference as though only one block contributed variance. Deriving the EWMA transient, autocorrelation inflation, and two-block difference scale beside the helper makes the tolerance checkable from the quantities the test uses. The existing per-axis budgets and test behaviour remain unchanged.
Node and terminal population deltas cannot identify every structural event: removing the last child leaves the terminal population unchanged, and recreating a missing child overlaps other growth signatures. Monotonic totals now record each child created by bisection as a split, each terminal child removal as an eviction, and each missing child recreated by legacy promotion as a restoration. The accessor and value record are additive public API, released in the semver-minor 1.1.0 line.
…ounters Node and terminal population deltas do not identify an eviction that turns a semi-internal parent into a terminal. Reading the spatial layer's monotonic event counters preserves every structural event, while snapshotting each new graph prevents reset from creating a synthetic interval. The split field counts created children, and unsigned net removals floor restoration-heavy intervals at zero and saturate values outside the report field's range.
There was a problem hiding this comment.
🔵 Needs a closer look
The large public numerical API and concurrent warming lifecycle warrant final human review despite extensive documentation and tests.
Review details
- Files reviewed: 63/110 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Maturity decays inconsistently with the learned model, infinite epsilon can disable scoring, and several public references are inaccurate.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 63/110 changed files
- Comments generated: 5
- Review effort level: Balanced
An infinite denominator guard passes a positivity check while making every protected score or energy denominator infinite, collapsing the resulting ratios to zero. Validation now separates non-finite values from non-positive finite values and leaves the largest finite guard admissible. EpsNotFinite is an additive variant of the existing non-exhaustive ConfigError enum, giving callers a truthful diagnostic without reshaping any existing item.
The learned subspace, latent statistics, and score baselines consume the forgetting factor once per tracker batch, while maturity consumed it once per row. Larger batches therefore claimed that warm-up had been forgotten before the model state had forgotten it. Advance noise influence once per tracker batch while retaining sample-based observation counters. The algorithm previously specified the recurrence per observation even though its convergence table used batches; it now states the model-aligned cadence and its batch-size independence.
Rank adaptation advances once per tracker batch, so describing its interval in observations makes the schedule appear to depend on batch size. State the unit consistently in the public configuration and supporting documents. Replace internal record and section keys in source documentation with the navigable ADR and package-qualified algorithm references required by the contributor contract.
There was a problem hiding this comment.
🔵 Needs a closer look
The new statistical engine, numerical routines, concurrency lifecycle, and broad public API require final human validation despite no confirmed blocking defect in the reviewed changes.
Review details
- Files reviewed: 63/110 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
ACK 4c052f8 |
A sentinel is not a judge. It stands watch, keeps its bearings, and reports what has changed.
That is the idea behind Spectral Sentinel. It does not decide whether a pattern is dangerous, important, or actionable. It measures structure in a stream, learns what has been ordinary so far, and returns statistical readouts when new observations depart from the learned geometry.
The "spectral" part is literal: each selected region is modelled with low-rank subspace trackers, and a second tier models the spectrum of those scores across related cells. The "sentinel" part is restraint: the crate observes, scores, and reports, but policy stays with the host.
Summary
This PR adds
torrust-sentinelto the workspace: a library crate for hierarchical online subspace anomaly detection over positionally structured observation streams. It is rebuilt as a single commit on the currentdevelop(after #884), so it sits on the MSRV-1.89 floor of ADR-T-011 and follows the per-crate versioning of ADR-T-012.It is built on top of Mudlark. Mudlark provides the adaptive spatial substrate — regions that receive more observation volume earn finer resolution, quiet regions remain coarse. Spectral Sentinel uses that structure to decide where statistical trackers are worth maintaining. It selects significant V-Tree entries, closes them under G-tree ancestry so every selected cell has a complete ancestor chain back to the root, and scores incoming batches against learned subspace models at every selected scale.
The simplest way to think about it: Mudlark decides where the stream has shape; Sentinel measures whether the recent shape still looks like what that region has learned to expect.
The crate is deliberately policy-free. Reports carry raw measurements — four scoring axes (novelty, displacement, surprise, coherence), maturity, baselines, CUSUM drift accumulators, geometry, contour summaries, and health snapshots. They do not encode threat levels, recommended actions, or decisions. The host reads the measurements and decides what they mean.
The core invariant is feed-forward: every input value updates Mudlark with exactly one unit of observation volume. Anomaly scores never flow back into the spatial index. That keeps spatial adaptation driven by traffic structure, not by the detector's own conclusions. Temporal policy is host-controlled: Sentinel never applies decay automatically.
A second analysis tier — coordination trackers — runs at internal G-tree nodes whose subtrees both contribute competitive cells. It scores cross-cell patterns of the four axes, so a coordinated shift that no single cell would flag still surfaces in the report.
Contents
The addition is substantial, but almost entirely self-contained within
packages/sentinel.torrust-sentinelcrate (1.0.0) with a narrow public surface exposed through flat crate-root re-exportsSpectralSentinel<C, V, N>as the generic engine, withSentinel128andSentinel64aliases for the common domain widthsSentinelConfig,NoiseSchedule, andSvdStrategyfor host-controlled measurement parameters, with structuredConfigError/ConfigErrors/ConfigWarningvalidation rather than panicsGNodeIdserdesupport (off by default; pulls intorrust-mudlark/serde)src/tests/), integration tests (tests/) and doc-tests, with the README compiled as a doc-test via#[cfg(doctest)] include_str!pedagogy.rs,pedagogy_advanced.rs) written to be read end-to-end as a walkthrough of the public surfaceChanges outside sentinel
Cargo.toml—packages/sentineladded as a workspace memberpackages/mudlark—1.0.1→1.1.0: monotonic structural mutation counters (splits, evictions, restorations) recorded at the sites that already maintain the node and terminal counts, and asemi_internal_count()accessor; both additive, with a changelog entryCargo.lock— 69 entries added for the dependency closure (faer,rand_distr, plus dev-onlycriterionandtracing-subscriber); the only moved entry istorrust-mudlarkfollowing its bumpAGENTS.md— adds Sentinel'sS-cross-reference prefix to the package table and ADR examplesManifest under ADR-T-012
The dependency on the sibling
torrust-mudlarkpinsversion = "1.1.0"beside itspath, becausecargo publishwrites that requirement into the published manifest and the report reads the structural mutation counters that arrive with that minor.faer,tracingandcriterionname the 0.x line the sources are written against (0.24, 0.1, 0.8) instead of a bare0, for the reason #884 gave for the root's requirements.cargo publish --dry-run -p torrust-sentinelstops at resolution becausetorrust-mudlarkis not on crates.io yet; that is the publication order ADR-T-012 documents, andtorrust-mudlarkitself dry-runs cleanly.Reviewing this
The best starting point is the public surface:
packages/sentinel/src/lib.rs→packages/sentinel/docs/api.md→packages/sentinel/README.mdFrom there, the main implementation path is
src/sentinel/mod.rsfor the orchestrator,src/analysis_set.rsfor competitive selection and ancestor closure,src/sentinel/tracker.rsfor per-cell scoring,src/sentinel/{cusum,staging,warming_thread}.rsfor drift and warm-up, andsrc/maths/for the SVD plumbing.For a focused review, I would look at:
d, narrow rank gaps) and the debug-mode oracle pathThe pedagogy tests are intended to be readable end-to-end; running
cargo test -p torrust-sentinel --test pedagogy -- --nocaptureproduces a narrated walk through the public surface.Verification
On the rebuilt commit:
cargo fmt --checkclean;cargo clippy --workspace --all-targets --all-features -- -D warningsclean under the workspace lint table; the crate's 565 tests and 15 doc-tests pass; the whole workspace passes (2,370 tests, none failed);cargo auditkeeps the vulnerability count ofdevelop(the onersaadvisory with no fixed release) and adds a single allowed unmaintained-crate warning, RUSTSEC-2024-0436 (paste, a proc-macro pulled byfaerthroughgemm).Notes
1.0.0: the public surface documented indocs/api.mdis covered by semver guarantees from this release onwards. A sibling crate consumes it through aversion-beside-pathpin, the same discipline this manifest applies totorrust-mudlark, so the version a consumer pins is the one the manifest declares.unsafecode;#![forbid(unsafe_code)]at the crate root.serdeis opt-in.S-cross-reference prefix added in this PR.Review fixes
Since the previous head, nine commits on top of the three original ones fix every finding an automated review of the package produced and a code-level verification confirmed: the configuration validation refuses non-numbers, an unrepresentable depth-buffer headroom and a coordinate width below the tracker minimum (two additive error variants); the full-width cell owns the domain maximum and the root leaves the analysis candidates before the capacity cut; staged cells warm by their real volume and a pass with no competitive scores retires every coordination context; health and batch reports count the online sets, populate the semi-internal count from the graph (one additive
mudlarkaccessor, and the headroom arithmetic inmudlarksaturates instead of wrapping), count the whole contour and order coordination reports by depth then identifier; the geometric schedule reports its true maximum, a failed corrective factorisation reports failure so the dispatcher falls back, and the unread round scores are gone. Prose follows the code (the z-score denominator, the live-tracker figure, the open bit-source trait, the implemented dimension guard), and the one exact float equality in the invariants suite compares bit patterns. Nothing on the public surface is removed or reshaped.Second round of review fixes
Five further commits fix every finding of a second automated review at the previous head. The warming thread's shutdown transition is made under the staging lock its wait is paired with, so a shutdown can no longer be lost between the worker reading its predicate and sleeping on it, which left the join — reached from
Drop— waiting forever; theu128centred-bit conversion caps the requested width at the type's own instead of indexing past its backing array; the centred bit vector gains a validated constructor and a length accessor so an implementation of the open bridge trait outside the crate can return the value its impl must produce; the online summary reports the investment count over the whole selection, warming cells included, as its contract states. The test support's four-bit generator refuses a nibble at sixteen or above (which shifted every set bit out of the coordinate and aliased the range sixteen below), a six-bit generator carries the sprays that claim sixty-four distinct ranges, and the ordering, budget and concentration witnesses assert the documented order, the structure's own budget and a report below the root. The upper bounds of analysis entries and coordination contexts document the top-of-domain exception, the analysis set's full field is named as the investment set it is, the thread-safety plan states theSend + Syncthe crate asserts statically, and section-mark references with no referent leave the record and the test banners. Public surface: three additive constant functions on the centred bit vector; nothing is removed or reshaped. Verified on stable 1.98 (the toolchain this repository lints and tests with), 1.89.0 and nightly.Third round of review fixes
Four further commits fix every finding of a third automated review at the previous head. The headroom a depth pair demands was computed with one checked step and three unchecked ones around it, so a creation depth of zero beside an eviction depth at the top of the range overflowed inside the very method that promises to hand back its faults as values; the computation now lives in a helper whose every step is checked, and any overflow reports the existing structured error for a buffer too large to honour, with the widest pair a budget can clear pinned as accepted. The centred-bit vector holds at most 128 values, but the coordinate trait it is fed from is open to wider types and the only width guard compared the tracker's dimension with the coordinate's declared bits, so a 200-wide tracker over a 256-bit coordinate was admitted and fed from a 128-slot vector; the sentinel now refuses a width above the vector's ceiling with a configuration error naming the width and the maximum, and the ceiling is documented on the bit source, on the bit vector and in the crate docs. The prefix generators in the test support guarded their ranges with assertions that release builds compile out; all three sites assert unconditionally. The dimension guard's doc said widths at or below the minimum are refused where the predicate refuses only widths below it, and now names the side of the boundary that is kept; a bare section ordinal in the exponential-average module is replaced by the sense it carried. Public surface: one additive configuration-error variant; nothing is removed or reshaped. Verified on stable 1.98 (the toolchain this repository lints and tests with), 1.89.0 and nightly.
Fourth round of review fixes
Three further commits correct every finding of a fourth automated review at the previous head; all eight are documentation, and no Rust moves. The two warming modes draw from two different generators: synchronous warming drains from the sentinel's own generator, while background warming draws from a second one seeded on the worker and promotes whatever the worker has finished at each ingest, against a map the main thread is concurrently writing. Four sites promised bit-for-bit reproducibility across runs without saying which mode delivers it; each now scopes the claim to synchronous warming on a fixed build, in one identical clause, and names the background-mode interleaving as the second source of randomness that reaches the scores. Three lifecycle records described mechanisms that no longer run where they said: the cell-width rejection lives in the suffix-width filter applied at runtime rather than in configuration validation, and its effective range is stated; creation schedules noise injection rather than performing it, now that the injection itself is deferred; and the bounded per-ingest work of the deferred warm-up record is stated for the background mode it holds in, with the default mode's in-line drain named beside it. The dimension guard's record said cells at or below the minimum are excluded where the filter keeps a cell at it, and now carries the same words as the constant's own documentation. Verified on stable 1.98 and nightly, with the crate's rustdoc and doc tests, since the README is the crate's front-page documentation.
Fifth round of review fixes
Three further commits correct every finding of a fifth automated review at the previous head. Construction asked the operating system for the background warming thread and aborted the host when the request was refused, over a resource limit that has nothing to do with the configuration's correctness; the request now returns the environment's own account as a configuration error naming the setting, and it arrives alone because the thread is asked for only once validation has passed. Reset, which has no error channel, keeps the sentinel running and warms cells synchronously instead, recording the refusal as a warning: the warm-up dispatch keys on whether a thread is present rather than on the flag, so the fallback is complete and every report is produced as before. Seeding one baseline from another copied the numbers but only ever raised warmth, so a receiver seeded from a cold source stayed warm over placeholder statistics and the cold path that replaces them never ran again; warmth is now part of what is handed over, in both directions, with a witness that fails at the previous head. Three tests and their prose claimed more, or other, than the engine guarantees: the determinism test compared three lengths and a few means where it now compares whole reports figure by figure with equal bit patterns; the coordination-report ordering test asserted ascending handle where the producer sorts by depth and then handle, and both prose statements of the handle-only order are corrected with it; and the reproducibility claim at the top of the determinism suite is scoped to synchronous warming on a fixed build, which is the configuration those tests share. Public surface: one additive configuration-error variant, and the configuration-error enumeration is marked non-exhaustive ahead of first publication so a later refusal is additive too; the warming-thread handle whose signature changed is crate-internal. Verified on stable 1.98 (the whole workspace lints clean; the crate's tests pass), 1.89.0 and nightly, with the crate's rustdoc and doc tests.
Later rounds of review fixes
The remaining rounds of automated review, each verified at code level before a change was made, are answered by the commits after the fifth round. The CUSUM allowance now follows the algorithm text, κσ·√v_slow, with the denominator-protection constant kept out of it; the geometric noise schedule saturates an unrepresentable exponent instead of wrapping it, so a public caller with an arbitrarily deep argument still lands on the floor; coordination contexts are retained by online competitive membership rather than by which cells happened to score in the batch, so a quiet batch no longer destroys a context that the next joint batch would have to re-warm. Every section reference in source and tests uses the qualified
§ALGO S-Nform and points at the section that carries the cited content; the clip-pressure implementation plan moved todocs/plans/so the ADR identifier it borrowed resolves to one record; the implementation guide describes the lazy coordination warm-up the code performs; a failed warming worker is recorded rather than allowed to takereset()down; the report's geometry record describes the model that produced the scores beside it; the compile-time width bound is named where a reader would look for it in the error list; and a test comment that cited a document the package never contained now derives its tolerance in place. Finally, the structural mutation counts in the contour snapshot are read from the spatial layer's own counters instead of being inferred from node and terminal deltas, an inference that a last-child eviction falsified; that is what the Mudlark minor bump carries.