diff --git a/README.md b/README.md
index 35c6e64..f68f0e0 100644
--- a/README.md
+++ b/README.md
@@ -159,6 +159,20 @@ Interpret findings by maturity:
- `event_rate_burst`, `auth_failure_burst`, `dns_query_burst`, and
`deny_action_burst` compare the current window to historical buckets.
+Field deviations that share the same detector and field are clustered before
+scoring, not after. If the same `(detector, field)` signature appears across
+five or more entities in one poll (`false_positive`/`expected`-suppressed
+deviations don't count toward that total), every deviation in the cluster is
+tagged with a `cluster_size` and its score is capped at 25, and the Evidence
+column shows `[part of a N-entity pattern]` ahead of the reason. This is a
+deliberate inversion of the usual "more occurrences = more severe" rule: a
+pattern hitting many unrelated entities identically almost always means a
+detector or profile problem (a field that shouldn't have been picked as a
+relationship target, for example) rather than N separate critical security
+events, so it is scored at or below an isolated occurrence instead of above
+it. A small, isolated deviation keeps its full score. See
+[`ARCHITECTURE.md`](ARCHITECTURE.md) for the incident that motivated this.
+
The review buttons are part of the detection loop:
- `Expected`: known behavior that should stay visible but not keep creating
@@ -217,6 +231,16 @@ dashboard status payload carries the underlying per-field shape and confidence
values, so a `field_confidence`/`field_shapes` map is available for any field
even when it was not selected into the applied profile.
+A name match never overrides value shape, even for names on the trusted
+priority lists such as `context`. If the observed values for a name-matched
+field confidently look like free text (a raw log line or URL, not a
+low-cardinality category) it is excluded from entity, categorical, and
+relationship-target selection regardless of its name, both for a fresh
+recommendation and as a second, independent check when a field reaches
+relationship-candidate selection through cross-stream field sharing. A field
+with too few samples to classify is never excluded on that basis alone; the
+veto only fires on a confident, contradicting shape.
+
Windows-like streams are recognized from stream names such as `Windows`,
`Winlog`, `Security Event Log`, `Sysmon`, and `Powershell`, or from Windows event
fields. The MCP search asks for common Winlogbeat/ECS names such as
@@ -337,6 +361,15 @@ After the baseline has learned those relationships, a new `username -> srcip` or
`username -> hostname` pair is reported as `new_relationship` with sample events.
The dashboard profile editor exposes this as `Behavior relationships (JSON)`.
+An existing stored profile's `categorical_fields` and `relationship_fields` are
+re-checked against current observed shapes on every poll, not just when the
+profile was first created — a field can look fine at profile-creation time and
+drift into free text later, or the classifier's read of it can simply have been
+wrong initially. A mismatch never rewrites the stored profile automatically;
+it only shows up under `Recommended Stream Profiles` as a warning naming the
+field, the stream, and the confidence behind the mismatch, with a button that
+opens that stream's profile editor so a person decides whether to remove it.
+
SignalScope keeps a common alias map for fields such as source IP, destination
IP, ports, action, severity, service/protocol, DNS query, URL, message, and event
type. This lets Related Activity and correlations work with firewall/proxy/DNS
diff --git a/ROADMAP.md b/ROADMAP.md
index 55c3662..b8c8010 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -29,18 +29,18 @@ parts of sections 1 and 6 below (baseline confidence tooling and compact
incident cards both depend on findings being clustered correctly first) — do
this section before spending more effort on either.
-- [ ] Validate name-matched fields against observed value shape too, not just
+- [x] Validate name-matched fields against observed value shape too, not just
fields that had no name match. A field that matches a trusted name (e.g.
`context`) but is actually free-text (a full raw log line, confirmed in 12
of 25 audited stream profiles) must not be trusted just because its name
matched a known-useful alias.
-- [ ] Make relationship-candidate selection shape-aware: never propose a
+- [x] Make relationship-candidate selection shape-aware: never propose a
free-text-shaped field as a relationship target, regardless of how it
entered `categorical_fields`. This alone was responsible for 17 of 25
entries in one real triage queue snapshot (91 individual `new_relationship`
deviations from a single misconfigured `*->context` pairing, repeated
across 5 streams).
-- [ ] Re-validate existing stored profiles' `relationship_fields` and
+- [x] Re-validate existing stored profiles' `relationship_fields` and
`categorical_fields` against current observed shapes on an ongoing basis,
not just at profile-creation time — flag (never silently rewrite) fields
that no longer look fit for their assigned role, and surface a one-click
@@ -50,18 +50,22 @@ this section before spending more effort on either.
(for example a source IP seen in both a firewall stream and an IIS
stream), instead of a cold, isolated baseline per stream per field. A value
already known-normal in one contributing stream must not register as "new"
- the first time a different, related stream happens to see it.
-- [ ] Cluster field deviations by `(detector, field, value-pattern)`
- signature *before* scoring, not after. A pattern recurring identically
- across many entities must surface as one clustered item with an occurrence
- count and a "how many entities" breakdown, not N independently-scored
- "critical" rows that all have to be discovered, selected, and bulk-cleared
- by hand after the fact.
-- [ ] Recalibrate severity so cluster size is itself a signal: a pattern
- hitting many unrelated entities identically is usually a
- configuration/detector problem, not N separate critical incidents, and
- should score at or below an isolated occurrence, never above it.
-- [ ] Update README usage documentation alongside each item above as it
+ the first time a different, related stream happens to see it. Deferred:
+ needs a `baseline.py` schema change and its own dedicated planning pass.
+- [x] Cluster field deviations by `(detector, field)` signature *before*
+ scoring, not after (value-pattern was dropped from the original signature —
+ detector+field alone already matched the real incident's cross-stream
+ pattern, and adding value-pattern would have under-clustered near-identical
+ but not-byte-identical raw messages). A pattern recurring identically
+ across many entities surfaces as one clustered item with an occurrence
+ count (`cluster_size`) shown inline in the Evidence column, not N
+ independently-scored "critical" rows that all have to be discovered,
+ selected, and bulk-cleared by hand after the fact.
+- [x] Recalibrate severity so cluster size is itself a signal: a pattern
+ hitting five or more entities identically is capped at score 25 regardless
+ of its uncapped score, so it never outranks an isolated, uncapped
+ occurrence.
+- [x] Update README usage documentation alongside each item above as it
ships — a behavior change is not done until it is documented.
Acceptance: replaying the dataset that produced the original 25-entry,
diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py
index dce26a3..2c1ef17 100644
--- a/src/fgai/dashboard.py
+++ b/src/fgai/dashboard.py
@@ -193,7 +193,7 @@ HTML = """
Events and Anomalies
Baseline and Stream Health
Operator Guidance
Waiting for monitor data.
Correlation Map
Waiting for correlated entities.
Investigation Incidents
Anomalies
Whole-source suppressions must have an expiry of at least 1 day.
Recommendations
AI Assessment
LLM assessment disabled.
Triage Queue
Field Baseline Deviations
Broad-scope rules must expire. Choose how many days this suppression should last.
Active Feedback Rules
Related Activity Across SourcesBlock CandidatesThreat IntelligencePolicy Findings
Diagnostics
-
Recommended Stream Profiles
Waiting for observed stream data.
Installed Ollama Models
Loading local Ollama models.
Runtime Configuration
+
Recommended Stream Profiles
Waiting for observed stream data.
Installed Ollama Models
Loading local Ollama models.
Runtime Configuration
How To Use SignalScope
1. Normal workflow
Settings: connect Graylog MCP and enable streams.
Settings: apply recommended profiles for missing streams.
Diagnostics: confirm raw samples, aggregate counts, and profile readiness.
Overview: use Operator Guidance, incidents, trends, and correlation map.
Findings: review only high-signal deviations first, then mark decisions.
2. Stream health
ready: profile exists, events are arriving, and baseline fields are ready.
learning: profile exists and events arrive, but baseline age or buckets are still too low.
missing_profile: stream is enabled but no profile exists. Apply or edit one.
no_events: stream is enabled but the current poll has no raw sample events.
partial_fetch: Graylog returned only part of the requested raw sample.
3. Ready fields
0/8 means 8 profile fields are tracked but none are mature enough yet. A field needs at least 12 baseline buckets and the configured Baseline training days before it is ready.
During learning, treat findings as signals to tune profiles, not as final alerts.
4. Profiles
Entity fields define who or what behavior is tracked, such as user, host, source IP, or application actor.
Baseline fields define the changing behavior to learn, such as action, event ID, service, URL, status, or counters.
Relationships learn pairs such as username to srcip or host to process.
Detectors add burst checks for auth failures, DNS queries, and deny actions.
5. Findings
Start with Triage Queue and incidents, not raw long tables.
Open evidence details before confirming a finding.
Use Expected for known behavior, False positive for bad signal, Confirmed for real investigation items.
Use expiry when a behavior is expected only temporarily.
6. High EPS / MCP
Use aggregate or auto fetch mode for high EPS streams.
Keep raw samples small enough for context; aggregate counts represent the full window.
Sample capped is normal in aggregate mode. Truncated raw mode means you may miss context.
If MCP is stale, the UI shows cached status so you can still inspect previous findings.
7. Ollama
Dashboard assessment summarizes current evidence.
Profile advisor maps unknown/custom fields and suggests relationships.
Ollama advice is constrained to fields discovered from Graylog; unknown fields are rejected.
8. Baseline DB size
Baseline DB size is the local SQLite behavior baseline.
Retention deletes old rows, but SQLite only returns disk space after manual VACUUM.
If the DB grows fast, lower bucket retention, value retention, or max values per entity field in Settings.
Stop the service before running signalscope baseline-maintenance --vacuum.
9. What to fix first
No streams enabled.
Enabled streams with missing profiles.
Enabled streams with zero raw events.
Profiles stuck at 0 ready fields after the training window.
Too many repeated findings without review feedback.
'${esc(w.field)}' in ${esc(w.stream_name || w.stream_id)}'s ${esc(w.role)} fields now looks like free text (${esc(String(w.confidence))}% confidence) — consider removing it from the profile.