Every document filename now mirrors its lifecycle state with a status suffix (e.g., .draft.md, .wip.md, .accepted.md). No more bare .md for tracked document types. Also renamed all from_str methods to parse to avoid FromStr trait confusion, introduced StagingDeploymentParams struct, and fixed all 19 clippy warnings across the codebase. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
314 lines
9 KiB
Markdown
314 lines
9 KiB
Markdown
# RFC 0017: Dynamic Context Activation
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Status** | Implemented |
|
|
| **Created** | 2026-01-25 |
|
|
| **Source** | Alignment Dialogue (12 experts, 95% convergence) |
|
|
| **Depends On** | RFC 0016 (Context Injection Architecture) |
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Implements Phase 3 of RFC 0016: refresh triggers, relevance graph computation, and staleness detection for dynamic context activation.
|
|
|
|
## Motivation
|
|
|
|
RFC 0016 established the three-tier context injection architecture with manifest-driven configuration. However, the current implementation is static:
|
|
- Triggers are defined but not activated
|
|
- `blue://context/relevance` returns empty
|
|
- `staleness_days` is declared but not enforced
|
|
|
|
Users experience context drift when working on RFCs that change state, documents that get updated, or across long sessions. The system needs to dynamically refresh context based on activity.
|
|
|
|
## Principles
|
|
|
|
1. **Staleness is content-based, not time-based** - A document unchanged for 30 days isn't stale; a document changed since injection is
|
|
2. **Predictability is agency** - Users should be able to predict what context is active without controlling every refresh
|
|
3. **ML is optimization, not feature** - Start with explicit relationships; add inference when data justifies it
|
|
4. **Event-sourced truth** - The audit log (`context_injections`) is the source of truth for staleness and refresh decisions
|
|
|
|
## Design
|
|
|
|
### Refresh Triggers
|
|
|
|
#### MVP: `on_rfc_change`
|
|
|
|
The only trigger implemented in Phase 1. Fires when:
|
|
- RFC status transitions (draft → accepted → in-progress → implemented)
|
|
- RFC content changes (hash differs from last injection)
|
|
- RFC tasks are completed
|
|
|
|
```rust
|
|
pub enum RefreshTrigger {
|
|
OnRfcChange, // MVP - implemented
|
|
EveryNTurns(u32), // Deferred - needs usage data
|
|
OnToolCall(String), // Deferred - needs pattern analysis
|
|
}
|
|
```
|
|
|
|
#### Trigger Evaluation
|
|
|
|
Piggyback on existing audit writes. When `resources/read` is called:
|
|
|
|
```rust
|
|
fn should_refresh(uri: &str, session_id: &str, store: &DocumentStore) -> bool {
|
|
let last_injection = store.get_last_injection(session_id, uri);
|
|
let current_hash = compute_content_hash(uri);
|
|
|
|
match last_injection {
|
|
None => true, // Never injected
|
|
Some(inj) => inj.content_hash != current_hash
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Rate Limiting
|
|
|
|
Prevent refresh storms with cooldown:
|
|
|
|
```rust
|
|
const REFRESH_COOLDOWN_SECS: u64 = 30;
|
|
|
|
fn is_refresh_allowed(session_id: &str, store: &DocumentStore) -> bool {
|
|
let last_refresh = store.get_last_refresh_time(session_id);
|
|
match last_refresh {
|
|
None => true,
|
|
Some(t) => Utc::now() - t > Duration::seconds(REFRESH_COOLDOWN_SECS)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Relevance Graph
|
|
|
|
#### Phased Implementation
|
|
|
|
| Phase | Scope | Trigger to Advance |
|
|
|-------|-------|-------------------|
|
|
| 0 | Explicit links only | MVP |
|
|
| 1 | Weighted by recency/access | After 30 days usage |
|
|
| 2 | Keyword expansion (TF-IDF) | <50% recall on explicit |
|
|
| 3 | Full ML (embeddings, co-access) | >1000 events AND <70% precision |
|
|
|
|
#### Phase 0: Explicit Links
|
|
|
|
Parse declared relationships from documents:
|
|
|
|
```markdown
|
|
<!-- In RFC body -->
|
|
References: ADR 0005, ADR 0007
|
|
```
|
|
|
|
Store in `relevance_edges` table:
|
|
|
|
```sql
|
|
CREATE TABLE relevance_edges (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
source_uri TEXT NOT NULL,
|
|
target_uri TEXT NOT NULL,
|
|
edge_type TEXT NOT NULL, -- 'explicit', 'keyword', 'learned'
|
|
weight REAL DEFAULT 1.0,
|
|
created_at TEXT NOT NULL,
|
|
UNIQUE(source_uri, target_uri, edge_type)
|
|
);
|
|
|
|
CREATE INDEX idx_relevance_source ON relevance_edges(source_uri);
|
|
```
|
|
|
|
#### Resolving `blue://context/relevance`
|
|
|
|
Returns documents related to current workflow context:
|
|
|
|
```rust
|
|
pub fn resolve_relevance(state: &ProjectState) -> Vec<RelevantDocument> {
|
|
let current_rfc = get_current_rfc(state);
|
|
|
|
// Get explicit links from current RFC
|
|
let edges = state.store.get_relevance_edges(¤t_rfc.uri);
|
|
|
|
// Sort by weight, limit to token budget
|
|
edges.sort_by(|a, b| b.weight.cmp(&a.weight));
|
|
|
|
let mut result = Vec::new();
|
|
let mut tokens = 0;
|
|
|
|
for edge in edges {
|
|
let doc = resolve_uri(&edge.target_uri);
|
|
if tokens + doc.tokens <= REFERENCE_BUDGET {
|
|
result.push(doc);
|
|
tokens += doc.tokens;
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
```
|
|
|
|
### Staleness Detection
|
|
|
|
#### Content-Hash Based
|
|
|
|
Staleness = content changed since last injection:
|
|
|
|
```rust
|
|
pub struct StalenessCheck {
|
|
pub uri: String,
|
|
pub is_stale: bool,
|
|
pub reason: StalenessReason,
|
|
pub last_injected: Option<DateTime<Utc>>,
|
|
pub current_hash: String,
|
|
pub injected_hash: Option<String>,
|
|
}
|
|
|
|
pub enum StalenessReason {
|
|
NeverInjected,
|
|
ContentChanged,
|
|
Fresh,
|
|
}
|
|
```
|
|
|
|
#### Tiered Thresholds
|
|
|
|
Different document types have different volatility:
|
|
|
|
| Doc Type | Refresh Policy | Rationale |
|
|
|----------|---------------|-----------|
|
|
| ADR | Session start only | Foundational beliefs rarely change |
|
|
| RFC (draft) | Every state change | Actively evolving |
|
|
| RFC (implemented) | On explicit request | Historical record |
|
|
| Spike | On completion | Time-boxed investigation |
|
|
| Dialogue | Never auto-refresh | Immutable record |
|
|
|
|
```rust
|
|
fn get_staleness_policy(doc_type: DocType, status: &str) -> RefreshPolicy {
|
|
match (doc_type, status) {
|
|
(DocType::Adr, _) => RefreshPolicy::SessionStart,
|
|
(DocType::Rfc, "draft" | "in-progress") => RefreshPolicy::OnChange,
|
|
(DocType::Rfc, _) => RefreshPolicy::OnRequest,
|
|
(DocType::Spike, "active") => RefreshPolicy::OnChange,
|
|
(DocType::Dialogue, _) => RefreshPolicy::Never,
|
|
_ => RefreshPolicy::OnRequest,
|
|
}
|
|
}
|
|
```
|
|
|
|
### Session Identity
|
|
|
|
Composite format for correlation and uniqueness:
|
|
|
|
```
|
|
{repo}-{realm}-{random12}
|
|
Example: blue-default-a7f3c9e2d1b4
|
|
```
|
|
|
|
- **Stable prefix** (`repo-realm`): Enables log correlation via SQL LIKE
|
|
- **Random suffix**: Cryptographically unique per MCP lifecycle
|
|
|
|
```rust
|
|
fn generate_session_id(repo: &str, realm: &str) -> String {
|
|
use rand::Rng;
|
|
const CHARSET: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
let suffix: String = (0..12)
|
|
.map(|_| CHARSET[rand::thread_rng().gen_range(0..CHARSET.len())] as char)
|
|
.collect();
|
|
format!("{}-{}-{}", repo, realm, suffix)
|
|
}
|
|
```
|
|
|
|
### Notification Model
|
|
|
|
Tier-based transparency:
|
|
|
|
| Tier | Behavior | User Notification |
|
|
|------|----------|-------------------|
|
|
| 1-2 (Identity/Workflow) | Automatic | Silent - user action is the notification |
|
|
| 3 (Reference) | Advisory | Inline: "Reading related tests..." |
|
|
| 4 (Expensive ops) | Consent | Prompt: "I can scan the full codebase..." |
|
|
|
|
**Honor Test**: If user asks "what context do you have?", the answer should match their intuition.
|
|
|
|
## Implementation
|
|
|
|
### Phase 1: MVP (This RFC)
|
|
|
|
- [x] Implement `on_rfc_change` trigger evaluation
|
|
- [x] Add content-hash staleness detection
|
|
- [x] Create `relevance_edges` table for explicit links
|
|
- [x] Update session ID generation
|
|
- [x] Add rate limiting (30s cooldown)
|
|
- [x] Implement `blue_context_status` MCP tool
|
|
|
|
### Phase 2: Weighted Relevance
|
|
|
|
- [ ] Add recency decay to edge weights
|
|
- [ ] Track access frequency per document
|
|
- [ ] Implement `blue context refresh` CLI command
|
|
|
|
### Phase 3: ML Integration (Gated)
|
|
|
|
Gate criteria:
|
|
- >1000 co-access events in audit log
|
|
- Explicit links precision >80%, recall <50%
|
|
- User feedback indicates "missing connections"
|
|
|
|
If gated:
|
|
- [ ] Co-access matrix factorization
|
|
- [ ] Embedding-based similarity
|
|
- [ ] Bandit learning for trigger timing
|
|
|
|
## Schema Changes
|
|
|
|
```sql
|
|
-- New table for relevance graph
|
|
CREATE TABLE relevance_edges (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
source_uri TEXT NOT NULL,
|
|
target_uri TEXT NOT NULL,
|
|
edge_type TEXT NOT NULL,
|
|
weight REAL DEFAULT 1.0,
|
|
created_at TEXT NOT NULL,
|
|
UNIQUE(source_uri, target_uri, edge_type)
|
|
);
|
|
|
|
CREATE INDEX idx_relevance_source ON relevance_edges(source_uri);
|
|
CREATE INDEX idx_relevance_target ON relevance_edges(target_uri);
|
|
|
|
-- Add to documents table
|
|
ALTER TABLE documents ADD COLUMN content_hash TEXT;
|
|
ALTER TABLE documents ADD COLUMN last_injected_at TEXT;
|
|
|
|
-- Efficient staleness query index
|
|
CREATE INDEX idx_documents_staleness ON documents(
|
|
doc_type,
|
|
updated_at,
|
|
last_injected_at
|
|
) WHERE deleted_at IS NULL;
|
|
```
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
- Context stays fresh during active RFC work
|
|
- Explicit architectural traceability through relevance graph
|
|
- Graceful degradation: system works without ML
|
|
- Auditable refresh decisions via event log
|
|
|
|
### Negative
|
|
- Additional complexity in refresh evaluation
|
|
- Rate limiting may delay urgent context updates
|
|
- Explicit links require document authors to declare relationships
|
|
|
|
### Neutral
|
|
- ML features gated on data, may never ship if simple approach suffices
|
|
|
|
## Related
|
|
|
|
- [RFC 0016: Context Injection Architecture](./0016-context-injection-architecture.md)
|
|
- [Dialogue: Dynamic Context Activation](../dialogues/rfc-0017-dynamic-context-activation.dialogue.md)
|
|
- ADR 0004: Evidence
|
|
- ADR 0005: Single Source
|
|
|
|
---
|
|
|
|
*Drafted from alignment dialogue with 12 domain experts achieving 95% convergence.*
|