Skip to content

Add semantic drift monitor with CLI and security hardening#2

Open
Knapp-Kevin wants to merge 2 commits intossdavidai:masterfrom
Knapp-Kevin:master
Open

Add semantic drift monitor with CLI and security hardening#2
Knapp-Kevin wants to merge 2 commits intossdavidai:masterfrom
Knapp-Kevin:master

Conversation

@Knapp-Kevin
Copy link
Copy Markdown

This PR introduces an optional Semantic Drift Monitor for Surveyor, along with a focused round of security and robustness hardening.

The drift monitor adds temporal awareness to Surveyor’s clustering output by comparing cluster structure across runs. It does not alter clustering behavior or writeback logic. It is fully optional and disabled by default.

The security fixes address availability, path confinement, atomic writes, retention, and config validation.


What’s Included

1. Semantic Drift Monitor (Optional)

New module:

  • drift.py
    • Persists cluster snapshots to JSON
    • Compares latest two snapshots
    • Computes:
      • Jaccard similarity
      • Membership churn
      • New clusters
      • Dissolved clusters
      • Split/merge heuristics
    • Writes drift reports to JSON payloads in .log files

Surveyor integration:

  • Drift runs after clustering and labeling
  • No changes to clustering logic
  • No changes to writeback behavior
  • Fully non-blocking
  • Wrapped in try/except to prevent pipeline failure

CLI additions:

  • alfred drift status
  • alfred drift history [--limit N]
  • alfred drift show <timestamp|filename>

Configuration:

features:
  semantic_drift_monitor: false

semantic_drift:
  snapshot_retention: 10
  similarity_threshold: 0.5
  warn_on_high_drift: true

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional “Semantic Drift Monitor” to Surveyor to persist cluster snapshots, compare consecutive runs, and expose drift summaries via new CLI commands, along with config + documentation updates.

Changes:

  • Introduce DriftMonitor to snapshot cluster membership and write drift reports.
  • Integrate drift processing into the Surveyor daemon and add alfred drift {status|history|show} CLI.
  • Add config schema + examples/docs for feature flag and drift settings.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/alfred/surveyor/drift.py New snapshot + drift comparison logic, report writing, pruning, and path confinement for report reads
src/alfred/surveyor/drift_cli.py New CLI handlers for drift status/history/show
src/alfred/surveyor/daemon.py Integrates drift processing into the Surveyor pipeline after clustering/labeling
src/alfred/surveyor/config.py Adds FeatureFlags and SemanticDriftConfig to typed config loading
src/alfred/cli.py Adds top-level alfred drift command wiring and dispatch
src/alfred/quickstart.py Includes drift feature/config defaults in generated config
src/alfred/_bundled/config.yaml.example Documents new config keys in bundled example
config.yaml.example Documents new config keys in root example
docs/Surveyor.md Documents drift settings and artifacts
docs/CLI-Commands.md Documents new alfred drift commands
INITIAL_SECURITY_REVIEW.md Adds security review notes for drift monitor

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 167 to +170
all_changed = result.changed_semantic | result.changed_structural
if not all_changed:
log.info("daemon.no_changed_clusters")
return
else:
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The control flow now logs daemon.no_changed_clusters but still continues into drift processing and then logs daemon.labeling_complete. This results in two potentially contradictory log events for the no-change case. Consider returning early after no_changed_clusters, or renaming/guarding the later log so it only fires when labeling actually ran (or rename it to reflect the full step including drift).

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +194
cluster_key = f"semantic_{cid}"
from .state import ClusterState
from datetime import datetime, timezone
self.state.clusters[cluster_key] = ClusterState(
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imports inside the per-cluster loop (from .state import ClusterState, from datetime import ...) reduce readability and are executed repeatedly (even if cached). Since ClusterState and datetime/timezone are already safe to import at module scope, prefer moving these imports to the top of the file (or at least above the loop).

Copilot uses AI. Check for mistakes.
Comment thread docs/Surveyor.md
Comment on lines 195 to +206
All configuration lives in `config.yaml` under the `surveyor` section.

### Full Example

```yaml
features:
semantic_drift_monitor: false

semantic_drift:
snapshot_retention: 10
similarity_threshold: 0.5
warn_on_high_drift: true
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs say “All configuration lives in config.yaml under the surveyor section.” but the example immediately places features: and semantic_drift: at the top level (and the loader reads them from the top level). Update the wording (or the example structure) so users aren’t misled about where these keys must live.

Copilot uses AI. Check for mistakes.
Comment thread docs/CLI-Commands.md

The surveyor runs its 4-stage pipeline once and exits. As part of `alfred up`, it runs as a daemon with configurable intervals.

`alfred drift ...` commands are available only when `features.semantic_drift_monitor` is enabled.
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence is inaccurate: the alfred drift ... commands are still available when the feature flag is disabled; they just print a message and exit. Suggest rephrasing to clarify that drift artifacts/reports are only generated when enabled, and the CLI will report “disabled” otherwise.

Suggested change
`alfred drift ...` commands are available only when `features.semantic_drift_monitor` is enabled.
`alfred drift ...` commands are always available, but drift artifacts/reports are only generated when `features.semantic_drift_monitor` is enabled; otherwise, the CLI reports that semantic drift monitoring is disabled and exits.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +35
print(f"Compared clusters: {latest['previous_cluster_count']} -> {latest['current_cluster_count']}")
print(f"Cluster delta: {latest['cluster_count_delta']}")
print(f"New clusters: {len(latest.get('new_clusters', []))}")
print(f"Dissolved clusters: {len(latest.get('dissolved_clusters', []))}")
print(f"Overall churn: {round(latest.get('overall_churn', 0.0) * 100, 2)}%")
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd_status indexes required fields on latest using latest[...] (e.g., previous_cluster_count). If a drift report JSON is tampered with or partially written but still valid JSON, this will raise KeyError and crash the CLI. Prefer latest.get(...) with sensible defaults (and/or validate required keys after loading) so alfred drift status remains robust.

Copilot uses AI. Check for mistakes.
Comment thread src/alfred/cli.py
Comment on lines +488 to +498
def cmd_drift(args: argparse.Namespace) -> None:
raw = _load_unified_config(args.config)

if "surveyor" not in raw:
print("Surveyor is not configured in this config file.")
return

from alfred.surveyor.config import load_from_unified
from alfred.surveyor import drift_cli as dcli

config = load_from_unified(raw)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd_drift doesn’t call _setup_logging_from_config(raw), so structlog warnings from drift artifact parsing (and any other surveyor logging used by the drift CLI) may be missing or inconsistently formatted compared to other commands (e.g. alfred surveyor, alfred temporal). Consider initializing logging the same way here.

Copilot uses AI. Check for mistakes.
ssdavidai added a commit that referenced this pull request Mar 27, 2026
newtonium-errant added a commit to newtonium-errant/alfred that referenced this pull request Apr 18, 2026
Second of 5 commits for Voice Stage 2a-wk2. Extends the talker session
schema with two new top-level fields and threads them through all close
paths (explicit, timeout, startup sweep, shutdown).

- session._build_session_frontmatter: adds session_type (default "note")
  and continues_from (default None) kwargs; emits both at the top level
  of the frontmatter so Dataview and vault search can filter directly.
- session.close_session: new kwargs, written to the vault record AND to
  the closed_sessions state summary (plan open question ssdavidai#5 — state-only
  continuation lookup for wk2; body-parser fallback is wk3).
- session.{resolve_on_startup,check_timeouts}: read _session_type /
  _continues_from off active dict with safe "note" / None defaults so
  wk1 state rehydrates cleanly (plan open question ssdavidai#2 — backwards-compat
  via .get).
- bot._open_session_with_stash: accepts model/session_type/continues_from
  kwargs, stashes _session_type / _continues_from on the active dict.
  /end reads them back and passes them into close_session.
- daemon shutdown path: reads both off active dict before close.

Field name is "model" (not "model_used") — matches wk1 records so the
two cohorts share one telemetry schema (plan open question ssdavidai#2).

Tests: tests/telegram/test_session_frontmatter.py (2 tests).
pytest: 24/24 passing (22 prior + 2 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
newtonium-errant added a commit to newtonium-errant/alfred that referenced this pull request Apr 26, 2026
Code-reviewer P1 ssdavidai#2 — release-blocker for Phase 1 Hypatia.

`_validate_type` ran before `check_scope` on `vault_create`, gating
against the canonical 20-type `KNOWN_TYPES` set only. Every Hypatia
`document` / `concept` / `source` / `citation` / `template` and every
KAL-LE `pattern` / `principle` create raised ``VaultError: Unknown
type`` and never reached the scope-policy check — a misleading error
that pointed at the wrong gate. The new `KNOWN_TYPES_KALLE` /
`KNOWN_TYPES_HYPATIA` schema constants existed only to populate the
scope's create-allowlists; nothing wired them into the type gate.

Smoke check (master, pre-fix):
    $ ALFRED_VAULT_SCOPE=kalle alfred vault create pattern foo
    {"error": "Unknown type: 'pattern'. Valid: account, asset, ..."}
    $ ALFRED_VAULT_SCOPE=hypatia alfred vault create document foo
    {"error": "Unknown type: 'document'. Valid: account, asset, ..."}

Smoke check (post-fix):
    pattern under kalle → SUCCESS, document under hypatia → SUCCESS,
    pattern under hypatia → "scope 'hypatia' can only create hypatia
    types" (caught by the second gate, not the first), document with
    no scope → "Unknown type" (canonical-only preserved for
    untriggered scopes).

Implementation:

* New `schema.KNOWN_TYPES_BY_SCOPE: dict[str, set[str]]` maps each
  extension scope to the union of canonical types plus its own set.
  Two-layer contract documented inline: this gate accepts the type;
  `check_scope`'s create-allowlist enforces the per-scope policy.
* `_validate_type(record_type, scope=None)` consults the union when
  scope is set; default `None` preserves byte-for-byte behavior so
  every caller that doesn't propagate scope (talker `_execute_tool`,
  capture-extract, instructor) stays on canonical types only.
* `vault_create` and `vault_list` thread their existing `scope` kwarg
  into the type gate. `vault/cli.py` already sources scope from
  `ALFRED_VAULT_SCOPE`; the create / list handlers now forward it
  through. CLI agent paths (curator, janitor, distiller, instructor's
  subprocess fallback, every external `alfred vault` invocation) get
  the fix automatically.

Tests: 13 new cases in `test_hypatia_scope.py` exercise `vault_create`
end-to-end — every Hypatia type, both KAL-LE types, cross-scope leak
(document under kalle / pattern under hypatia both rejected at the
type gate), Salem regression guard (note under talker still works),
default scope=None still rejects extension types.

Note: the talker `_execute_tool` in `telegram/conversation.py` still
hardcodes `scope="talker"` — a separate Hypatia release-blocker for
the LIVE bot path (vs the CLI agent path this commit unblocks). That
one's a wider contract change (per-instance scope plumbing) and
filed for a follow-up commit.

Pattern-trigger note for `CLAUDE.md` Vault Operations Layer:
"validation gate ordering" applies to every future instance with
its own type set (V.E.R.A. RRTS, STAY-C). Add the scope's
`KNOWN_TYPES_<NAME>` set AND a `KNOWN_TYPES_BY_SCOPE` entry, or
the type gate silently rejects before scope enforcement runs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants