Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 23 additions & 37 deletions HANDOFF.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,47 @@
# Handoff

**Updated:** 2026-10-14 | **By:** Copilot | **Branch:** main | **Last commit:** pending
**Updated:** 2026-10-18 | **By:** User | **Branch:** phase-4-env-rules | **Last commit:** 6960b1a

## Current task

- ID: P4-01 — environment signal schema
- ID: P5-01 - Settings and score store
- State: ready to start.

## Done in P3-11

- Added `export_keyboard_onnx()` with median imputation, standard scaling, and
Logistic Regression.
- Excluded K3 training rows and recorded that policy in `metadata.json`.
- Added `scripts/export_keyboard_onnx.py` and the
`train-keyboard --stage onnx` task.
- Added native-vs-ONNX parity and metadata tests.
- Declared `skl2onnx` directly in the ML package dependencies.
- Added deterministic same-class pseudo-session pairing in `fuse.py`.
- Added mouse-only, keyboard-only, average, noisy-OR, and validation
Logistic Regression stacker comparisons.
- Added explicit missing-modality behavior and fusion metadata output.
- Added tests for mouse-only, keyboard-only, both, neither, pairing, and
result labeling.
- Updated the model card with keyboard and fusion scope, privacy, data-source,
synthetic-bot, pseudo-session, and missing-modality limitations.
## Done in P4-07

- Recorded the decision to defer the optional BotD comparison.
- Did not add a BotD runtime dependency or download an unpinned artifact.
- Documented the local R01-R13 rule suite as the current baseline.
- Added the future requirements for any BotD comparison: pinned version,
licence review, fixture adapter, and a new decision entry.
- Marked Phase 4 acceptance criteria complete in `docs/plan/phase_4.md`.

## Exact next step

- Start Phase 4 by defining the privacy-safe environment signal schema.
- Begin Phase 5 by implementing backend settings and a privacy-safe score
store, preserving the documented model/rule boundaries.

## Files touched in P3-11
## Files touched in P4-07

- `ml/src/botguard_ml/export_onnx.py`
- `ml/tests/test_keyboard_onnx.py`
- `scripts/export_keyboard_onnx.py`
- `ml/pyproject.toml`
- `scripts/tasks.py`
- `ml/src/botguard_ml/fuse.py`
- `ml/tests/test_fuse.py`
- `docs/model_card.md`
- `docs/plan/phase_3.md`
- `docs/decision_log.md`
- `docs/signals.md`
- `docs/plan/phase_4.md`
- `PROGRESS.md`

## Validation

Documentation and decision-log task; no code validation required. The complete
rule implementation was previously validated with:

- `python scripts/tasks.py test-fast`
- `python scripts/tasks.py lint`

All passed. Windows pytest cache warnings are pre-existing and harmless.

## Known problems / gotchas

- Real final keyboard metrics require a bounded feature-shard run and
sufficient human and bot groups.
- K3 must remain evaluation-only.
- Phase 3 is merged into `main`; the next task is Phase 4 on `main`.
- Phase 4 environment rules remain on `phase-4-env-rules`.
- Do not use persistent identifiers or fingerprint hashes.
- Hard rules must bypass model scoring.

## Questions for the human (P3-08)
## Questions for the human

- None.
14 changes: 7 additions & 7 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@
| P3-09 | 3 | Keyboard ONNX export | DONE | fde90e8 | Added Logistic Regression ONNX export, metadata, CLI stage, explicit skl2onnx dependency, and native-vs-ONNX parity tests. |
| P3-10 | 3 | Desktop fusion | DONE | 5884937 | Added deterministic same-class pseudo-session pairing, missing-modality-safe fusion rules, validation stacker, result labels, metadata writer, and tests. |
| P3-11 | 3 | Keyboard and fusion model card | DONE | 0ae687d | Documented keyboard scope, privacy boundaries, single-human-source limitation, synthetic bots, pseudo-session fusion, and missing-modality policy. |
| P4-01 | 4 | Environment signal schema | TODO | | |
| P4-02 | 4 | Pure environment rules | TODO | | |
| P4-03 | 4 | Rule score | TODO | | |
| P4-04 | 4 | Configurable rule weights | TODO | | |
| P4-05 | 4 | Rule-layer tests | TODO | | |
| P4-06 | 4 | Signal privacy documentation | TODO | | |
| P4-07 | 4 | Optional BotD comparison | TODO | | |
| P4-01 | 4 | Environment signal schema | DONE | 8d8b575 | 26-field EnvSignals Pydantic model from Appendix B.2; all optional; no fingerprint hash; 16 tests. |
| P4-02 | 4 | Pure environment rules | DONE | 7f1ac74 | Implemented RuleResult and pure rule functions R01-R13, evaluate_all_rules, and 62 unit tests. |
| P4-03 | 4 | Rule score | DONE | c91ab3b | Added noisy-OR rule scoring, fired-rule reasons, hard-rule detection for R13/invalid nonce or signature, and model-bypass tests. |
| P4-04 | 4 | Configurable rule weights | DONE | 9ddd713 | Added validated YAML loading for enabled flags and weights, with disabled-rule handling in combined evaluation. |
| P4-05 | 4 | Rule-layer tests | DONE | d3195de | Added aggregate normal Chrome and headless-browser fixture assertions, including rule score and fired-rule expectations. |
| P4-06 | 4 | Signal privacy documentation | DONE | pending | Added the complete signal privacy register, prohibited-data boundary, retention guidance, and fixture coverage summary. |
| P4-07 | 4 | Optional BotD comparison | DONE | pending | Deferred by decision: no pinned offline BotD artifact or approved adapter is present; no runtime dependency added. |
| P5-01 | 5 | Settings and score store | TODO | | |
| P5-02 | 5 | API schemas and limits | TODO | | |
| P5-03 | 5 | Nonces | TODO | | |
Expand Down
48 changes: 48 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
@@ -1 +1,49 @@
"""Backend configuration loading and validation."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import yaml # type: ignore[import-untyped]

from app.rules import DEFAULT_RULE_WEIGHTS


@dataclass(frozen=True)
class RuleConfig:
"""Validated enabled flags and weights for the environment rules."""

enabled: dict[str, bool]
weights: dict[str, float]


def load_rule_config(path: Path) -> RuleConfig:
"""Load and validate a rules YAML file."""
try:
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except OSError as exc:
raise ValueError(f"unable to read rule configuration: {path}") from exc
if not isinstance(raw, dict) or not isinstance(raw.get("rules"), dict):
raise ValueError("rule configuration must contain a rules mapping")

enabled: dict[str, bool] = {}
weights: dict[str, float] = {}
for rule_id, defaults in raw["rules"].items():
if rule_id not in DEFAULT_RULE_WEIGHTS:
raise ValueError(f"unknown rule ID: {rule_id}")
if not isinstance(defaults, dict):
raise ValueError(f"configuration for {rule_id} must be a mapping")
is_enabled = defaults.get("enabled", True)
configured_weight = defaults.get("weight", DEFAULT_RULE_WEIGHTS[rule_id])
if not isinstance(is_enabled, bool):
raise ValueError(f"enabled flag for {rule_id} must be boolean")
if not isinstance(configured_weight, (int, float)) or not 0 <= configured_weight <= 1:
raise ValueError(f"weight for {rule_id} must be in [0, 1]")
enabled[rule_id] = is_enabled
weights[rule_id] = float(configured_weight)

for rule_id, default_weight in DEFAULT_RULE_WEIGHTS.items():
enabled.setdefault(rule_id, True)
weights.setdefault(rule_id, default_weight)
return RuleConfig(enabled=enabled, weights=weights)
Loading