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
21 changes: 20 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,16 @@ impl Trainer {
source: Source,
) -> Result<LearnReport, ManasError> { ... }

// Teach one fact with explicit freshness metadata.
pub fn learn_with_source_and_freshness(
&mut self,
network: &mut Network,
input: &str,
target: &str,
source: Source,
freshness: FreshnessCategory,
) -> Result<LearnReport, ManasError> { ... }

// Ask the network a question. Returns best answer from weights.
pub fn query(&mut self, network: &Network, question: &str)
-> Result<QueryResult, ManasError> { ... }
Expand All @@ -767,6 +777,12 @@ pub struct QueryResult {
pub answer: String,
pub confidence: f32, // 0.0 → 1.0
pub answered_from: AnswerSource,
pub freshness_warning: Option<FreshnessWarning>,
}

pub struct FreshnessWarning {
pub category: FreshnessCategory,
pub age_days: u64,
}

pub enum AnswerSource {
Expand Down Expand Up @@ -1096,7 +1112,7 @@ Every neuron has a `freshness_category: u8`:
The freshness category is detected automatically from the text content during `teach`:

```rust
pub fn detect_freshness(text: &str) -> u8 {
pub fn detect_freshness(text: &str) -> FreshnessCategory {
// keywords like "theorem", "law", "always" → 0 (Timeless)
// keywords like "today", "breaking", "live" → 3 (Realtime)
// keywords like "released", "version" → 2 (Fast)
Expand All @@ -1114,6 +1130,9 @@ Answer
Confidence
0.81

Answered from
neural weights

Note
This knowledge may be outdated (Fast freshness, learned 47 days ago).
```
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Manas uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Stage 9 — `manas-cli` v1: teach and ask.
- Stage 10 — File and folder ingestion.
- Stage 11 — Importance scoring and promotion.
- Stage 12 — Freshness system.

### Added

Expand Down Expand Up @@ -66,10 +67,17 @@ Manas uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Replaced activation-count-only promotion with importance-driven
`Open -> Guarded -> Frozen` promotion and preserved importance metadata
through `.manas` save/load.
- Added `manas-learn::freshness` with Timeless, Slow, Fast, and Realtime
categories, keyword detection, age thresholds, and stale-neuron warnings.
- `manas teach` now stamps freshness metadata on learned neurons, and
`manas ask` appends an outdated-knowledge note when the retrieved neuron is
stale.
- Added freshness tests for detection, staleness, trainer query warnings, CLI
rendering, CLI teach metadata, and `.manas` persistence.

### Next

- Stage 12Freshness system.
- Stage 13The real demo.

---

Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ Manas v2 is in active development. The roadmap follows a strict rule:
| Stage 9 | `manas teach` and `manas ask` | Complete |
| Stage 10 | File and folder ingestion | Complete |
| Stage 11 | Importance scoring and promotion | Complete |
| Stage 12 | Freshness system | Next |
| Stage 13+ | The real demo, inspect, benchmarks, layer growth | Planned |
| Stage 12 | Freshness system | Complete |
| Stage 13 | The real demo | Next |
| Stage 14+ | Inspect, benchmarks, layer growth | Planned |

Stages 1 and 2 are preserved as a standalone proof in
`manas-core/src/experiment.rs`. Stage 3 promotes the proven engine into
Expand All @@ -151,7 +152,9 @@ Stage 10 completes local ingestion so `manas teach` accepts raw text, a supporte
file, or a folder of supported files while preserving local file source metadata
inside the learned neurons. Stage 11 replaces activation-count-only promotion
with weighted importance scoring based on frequency, recency, weight magnitude,
and smooth age grace.
and smooth age grace. Stage 12 classifies learned knowledge as Timeless, Slow,
Fast, or Realtime and warns during `manas ask` when the answer comes from stale
neuron metadata.

Run the proof:

Expand Down Expand Up @@ -326,6 +329,27 @@ The entire brain — weights, vocab, metadata — is in this one file.

---

## Freshness

`manas teach` detects freshness from the taught text and stores the category on
the best matching learned neuron:

| Category | Examples | Stale after |
|---|---|---|
| Timeless | Definitions, proofs, laws | Never |
| Slow | Historical facts, biographies | 365 days |
| Fast | Software versions, news | 30 days |
| Realtime | Stock prices, live scores | 1 day |

When `manas ask` answers from a stale neuron, it appends a note such as:

```text
Note
This knowledge may be outdated (Fast freshness, learned 47 days ago).
```

---

## Crate Structure

```
Expand Down
25 changes: 19 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ from Stage 2 onward.**
| Stage 9 | `manas-cli` v1 — teach and ask | Complete |
| Stage 10 | File and folder ingestion | Complete |
| Stage 11 | Importance scoring and promotion | Complete |
| Stage 12 | Freshness system | Next |
| Stage 13 | The real demo | Planned |
| Stage 12 | Freshness system | Complete |
| Stage 13 | The real demo | Next |
| Stage 14 | Inspect, neurons, and debug commands | Planned |
| Stage 15 | Compression and forget command | Planned |
| Stage 16 | Benchmarks and test suite | Planned |
Expand Down Expand Up @@ -1569,6 +1569,10 @@ Completion note:

**Goal:** Every fact knows how time-sensitive it is. Stale facts are flagged.

**Status:** Complete. Freshness is detected during `teach`, stored on learned
neurons, persisted in `.manas`, and surfaced by `manas ask` when an answer comes
from stale neuron metadata.

### What to Build

```rust
Expand Down Expand Up @@ -1638,10 +1642,19 @@ fn fast_fact_stale_after_30_days() {

### Done When

- [ ] Keyword detection tests pass for all 4 categories
- [ ] Staleness detection tests pass for all 4 categories
- [ ] `manas ask` appends a "Note: may be outdated" line when answering from a stale neuron
- [ ] `cargo test -p manas-learn freshness` passes clean
- [x] Keyword detection tests pass for all 4 categories
- [x] Staleness detection tests pass for all 4 categories
- [x] `manas ask` appends a "Note: may be outdated" line when answering from a stale neuron
- [x] `cargo test -p manas-learn freshness` passes clean

### Stage 12 Implementation Notes

- Added `manas-learn::freshness` with `FreshnessCategory`,
`FreshnessWarning`, `detect_freshness`, and `is_stale`
- Added `Trainer::learn_with_source_and_freshness` while keeping
`learn_with_source` backward compatible
- `QueryResult` now carries optional freshness warning metadata
- Added freshness coverage in `manas-learn`, `manas-cli`, and `manas-store`

---

Expand Down
112 changes: 99 additions & 13 deletions manas-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use std::process;

use manas_core::Network;
use manas_ingest::{IngestSource, ingest};
use manas_learn::{AnswerSource, EncoderVocabEntry, LearnReport, Trainer};
use manas_learn::{
AnswerSource, EncoderVocabEntry, FreshnessWarning, LearnReport, Trainer, detect_freshness,
};
use manas_store::{BrainState, ManasBrain, VocabEntry};

const DEFAULT_BRAIN_PATH: &str = "brain.manas";
Expand Down Expand Up @@ -53,8 +55,15 @@ fn teach(brain_path: &Path, args: &[String]) -> Result<(), String> {
for chunk in &chunks {
for unit in teachable_units(&chunk.text) {
let (input, target) = extract_association(&unit)?;
let freshness = detect_freshness(&unit);
let report = trainer
.learn_with_source(&mut network, &input, &target, chunk.source.clone())
.learn_with_source_and_freshness(
&mut network,
&input,
&target,
chunk.source.clone(),
freshness,
)
.map_err(|error| error.to_string())?;
summary.record(&input, &target, &report);
}
Expand All @@ -75,7 +84,12 @@ fn ask(brain_path: &Path, args: &[String]) -> Result<(), String> {
let brain = ManasBrain::new(brain_path);

if !brain.exists() {
print_answer("Not enough knowledge yet.", 0.0, AnswerSource::NotEnough);
print_answer(
"Not enough knowledge yet.",
0.0,
AnswerSource::NotEnough,
None,
);
return Ok(());
}

Expand All @@ -93,7 +107,12 @@ fn ask(brain_path: &Path, args: &[String]) -> Result<(), String> {
let result = trainer
.query(&state.network, &question)
.map_err(|error| error.to_string())?;
print_answer(&result.answer, result.confidence, result.answered_from);
print_answer(
&result.answer,
result.confidence,
result.answered_from,
result.freshness_warning.as_ref(),
);
Ok(())
}

Expand Down Expand Up @@ -218,15 +237,50 @@ fn print_teach_report(summary: &TeachSummary) {
);
}

fn print_answer(answer: &str, confidence: f32, source: AnswerSource) {
println!("Answer");
println!(" {answer}");
println!();
println!("Confidence");
println!(" {:.2}", confidence);
println!();
println!("Answered from");
println!(" {}", answer_source_label(source));
fn print_answer(
answer: &str,
confidence: f32,
source: AnswerSource,
freshness_warning: Option<&FreshnessWarning>,
) {
print!(
"{}",
render_answer(answer, confidence, source, freshness_warning)
);
}

fn render_answer(
answer: &str,
confidence: f32,
source: AnswerSource,
freshness_warning: Option<&FreshnessWarning>,
) -> String {
use std::fmt::Write as _;

let mut output = String::new();
writeln!(&mut output, "Answer").expect("writing to String should not fail");
writeln!(&mut output, " {answer}").expect("writing to String should not fail");
writeln!(&mut output).expect("writing to String should not fail");
writeln!(&mut output, "Confidence").expect("writing to String should not fail");
writeln!(&mut output, " {:.2}", confidence).expect("writing to String should not fail");
writeln!(&mut output).expect("writing to String should not fail");
writeln!(&mut output, "Answered from").expect("writing to String should not fail");
writeln!(&mut output, " {}", answer_source_label(source))
.expect("writing to String should not fail");

if let Some(warning) = freshness_warning {
writeln!(&mut output).expect("writing to String should not fail");
writeln!(&mut output, "Note").expect("writing to String should not fail");
writeln!(
&mut output,
" This knowledge may be outdated ({} freshness, learned {} days ago).",
warning.category.label(),
warning.age_days
)
.expect("writing to String should not fail");
}

output
}

fn print_help() {
Expand Down Expand Up @@ -540,6 +594,7 @@ fn known_sidecar_paths(brain_path: &Path) -> Vec<PathBuf> {
#[cfg(test)]
mod tests {
use super::*;
use manas_learn::FreshnessCategory;

#[test]
fn extracts_simple_is_association() {
Expand All @@ -558,4 +613,35 @@ mod tests {
assert_eq!(input, "Eiffel Tower");
assert_eq!(target, "located in Paris France");
}

#[test]
fn render_answer_omits_note_without_freshness_warning() {
let output = render_answer("small animal", 0.91, AnswerSource::NeuralWeights, None);

assert!(output.contains("Answer\n small animal"));
assert!(output.contains("Answered from\n neural weights"));
assert!(!output.contains("Note"));
}

#[test]
fn render_answer_appends_stale_freshness_note() {
let warning = FreshnessWarning {
category: FreshnessCategory::Fast,
age_days: 47,
};

let output = render_answer(
"Rust 2.0 was released last month",
0.88,
AnswerSource::NeuralWeights,
Some(&warning),
);

assert!(output.contains("Note\n"));
assert!(
output.contains(
" This knowledge may be outdated (Fast freshness, learned 47 days ago)."
)
);
}
}
33 changes: 33 additions & 0 deletions manas-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};

use manas_core::Source;
use manas_learn::FreshnessCategory;
use manas_store::ManasBrain;

#[test]
Expand Down Expand Up @@ -122,6 +123,38 @@ fn cli_teach_folder_walks_supported_files_recursively() {
fs::remove_dir_all(dir).unwrap();
}

#[test]
fn cli_teach_stamps_realtime_freshness_metadata() {
let dir = temp_dir("teach-freshness");

let teach = run(
&dir,
&["teach", "Breaking news: the stock market fell today."],
);
assert_success(&teach);

let state = ManasBrain::new(dir.join("brain.manas"))
.load_state()
.unwrap();
let has_realtime_freshness = state
.network
.layers
.first()
.map(|layer| {
layer
.neurons
.iter()
.any(|neuron| neuron.freshness_category == FreshnessCategory::Realtime as u8)
})
.unwrap_or(false);
assert!(
has_realtime_freshness,
"expected realtime freshness metadata"
);

fs::remove_dir_all(dir).unwrap();
}

#[test]
fn cli_ask_without_brain_returns_not_enough() {
let dir = temp_dir("empty-ask");
Expand Down
Loading
Loading