feat: Schema system for Basic Memory (#549)

Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Hernandez
2026-02-11 15:04:42 -06:00
committed by GitHub
parent 00537272c6
commit c97733d785
108 changed files with 7123 additions and 1 deletions
+494
View File
@@ -0,0 +1,494 @@
# Note Format Reference
Every document in Basic Memory is a plain Markdown file. Files are the source of truth — changes to files automatically update the knowledge graph in the database. You maintain complete ownership, files work with git, and knowledge persists independently of any AI conversation.
## Document Structure
A note has three parts: YAML frontmatter, content (observations), and relations.
```markdown
---
title: Coffee Brewing Methods
type: note
tags: [coffee, brewing]
permalink: coffee-brewing-methods
---
# Coffee Brewing Methods
## Observations
- [method] Pour over provides more flavor clarity than French press
- [technique] Water temperature at 205°F extracts optimal compounds #brewing
- [preference] Ethiopian beans work well with lighter roasts (personal experience)
## Relations
- relates_to [[Coffee Bean Origins]]
- requires [[Proper Grinding Technique]]
- contrasts_with [[Tea Brewing Methods]]
```
The `## Observations` and `## Relations` headings are conventional but not required — the parser detects observations and relations by their syntax patterns anywhere in the document.
## Frontmatter
YAML metadata between `---` fences at the top of the file.
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `title` | No | filename stem | Used for linking and references. Auto-set from filename if missing. |
| `type` | No | `note` | Entity type. Used for schema resolution and filtering. |
| `tags` | No | `[]` | List or comma-separated string. Used for organization and search. |
| `permalink` | No | generated from title | Stable identifier. Persists even if the file moves. |
| `schema` | No | none | Schema attachment — dict (inline), string (reference), or omitted (implicit). |
Custom fields are allowed. Any key not in the standard set is stored as `entity_metadata` and indexed for search and filtering.
```yaml
---
title: Paul Graham
type: Person
tags: [startups, essays, lisp]
permalink: paul-graham
status: active
source: wikipedia
---
```
Here `status` and `source` are custom fields stored in `entity_metadata`.
### Frontmatter Value Handling
YAML automatically converts some values to native types. Basic Memory normalizes them:
- Date strings (`2025-10-24`) → kept as ISO format strings
- Numbers (`1.0`) → converted to strings
- Booleans (`true`) → converted to strings (`"True"`)
- Lists and dicts → preserved, items normalized recursively
This prevents errors when downstream code expects string values.
## Observations
An observation is a categorized fact about the entity. Written as a Markdown list item.
**Syntax:**
```
- [category] content text #tag1 #tag2 (context)
```
| Part | Required | Description |
|------|----------|-------------|
| `[category]` | Yes | Classification in square brackets. Any text except `[]()` chars. |
| content | Yes | The fact or statement. |
| `#tags` | No | Inline tags. Space-separated, each starting with `#`. |
| `(context)` | No | Parenthesized text at end of line. Supporting details or source. |
### Examples
```markdown
- [tech] Uses SQLite for storage #database
- [design] Follows local-first architecture #architecture
- [decision] Selected bcrypt for passwords #security (based on OWASP audit)
- [name] Paul Graham
- [expertise] Startups
- [expertise] Lisp
- [expertise] Essay writing
```
Array-like fields use repeated categories — multiple `[expertise]` observations above.
### What Is Not an Observation
The parser excludes these list item patterns:
| Pattern | Example | Reason |
|---------|---------|--------|
| Checkboxes | `- [ ] Todo item`, `- [x] Done`, `- [-] Cancelled` | Task list syntax |
| Markdown links | `- [text](url)` | URL link syntax |
| Bare wiki links | `- [[Target]]` | Treated as a relation instead |
A list item with `#tags` but no `[category]` is still parsed — the tags are extracted and the category defaults to `Note`.
## Relations
Relations connect documents to form the knowledge graph. There are two kinds.
### Explicit Relations
Written as list items with a relation type and a `[[wiki link]]` target.
**Syntax:**
```
- relation_type [[Target Entity]] (context)
```
| Part | Required | Description |
|------|----------|-------------|
| `relation_type` | No | Text before `[[`. Defaults to `relates_to` if omitted. |
| `[[Target]]` | Yes | Wiki link to the target entity. Matched by title or permalink. |
| `(context)` | No | Parenthesized text after `]]`. Supporting details. |
### Examples
```markdown
- implements [[Search Design]]
- depends_on [[Database Schema]]
- works_at [[Y Combinator]] (co-founder)
- [[Some Entity]]
```
The last example — a bare `[[wiki link]]` in a list item — gets relation type `relates_to`.
Common relation types:
- `implements`, `depends_on`, `relates_to`, `inspired_by`
- `extends`, `part_of`, `contains`, `pairs_with`
- `works_at`, `authored`, `collaborated_with`
Any text works as a relation type. These are conventions, not a fixed set.
### Inline References
Wiki links appearing in regular prose (not as list items) create implicit `links_to` relations.
```markdown
This builds on [[Core Design]] and uses [[Utility Functions]].
```
This creates two relations: `links_to [[Core Design]]` and `links_to [[Utility Functions]]`.
### Forward References
Relations can link to entities that don't exist yet. Basic Memory resolves them when the target is created.
## Permalinks and memory:// URLs
Every document has a unique **permalink** — a stable identifier derived from its title. You can set one explicitly in frontmatter, or let the system generate it.
```yaml
permalink: auth-approaches-2024
```
Permalinks form the basis of `memory://` URLs:
```
memory://auth-approaches-2024 # By permalink
memory://Authentication Approaches # By title (auto-resolves)
memory://project/auth-approaches # By path
```
Pattern matching is supported:
```
memory://auth* # Starts with "auth"
memory://*/approaches # Ends with "approaches"
memory://project/*/requirements # Nested wildcard
```
## Schemas
Schemas declare the expected structure of a note — which observation categories and relation types a well-formed note should have. They use Picoschema, a compact notation from Google's Dotprompt that fits naturally in YAML frontmatter.
### Picoschema Syntax
```yaml
schema:
name: string, full name # required field with description
email?: string, contact email # ? = optional
role?: string, job title
works_at?: Organization, employer # capitalized type = entity reference
tags?(array): string, categories # array of type
status?(enum): [active, inactive] # enum with allowed values
metadata?(object): # nested object
updated_at?: string
source?: string
```
| Notation | Meaning | Example |
|----------|---------|---------|
| `field: type` | Required field | `name: string` |
| `field?: type` | Optional field | `role?: string` |
| `field(array): type` | Array of values | `expertise(array): string` |
| `field?(enum): [vals]` | Enum with allowed values | `status?(enum): [active, inactive]` |
| `field?(object):` | Nested object with sub-fields | `metadata?(object):` |
| `, description` | Description after comma | `name: string, full name` |
| `EntityName` | Capitalized type = entity reference | `works_at?: Organization` |
**Scalar types:** `string`, `integer`, `number`, `boolean`, `any`
Any type not in that set whose first letter is uppercase is treated as an entity reference (a relation target).
### Schema-to-Note Mapping
Schemas validate against existing observation/relation syntax. Note authors don't learn new syntax.
| Schema Declaration | Maps To | Example in Note |
|--------------------|---------|-----------------|
| `field: string` | Observation `[field] value` | `- [name] Paul Graham` |
| `field?(array): string` | Multiple `[field]` observations | `- [expertise] Lisp` (repeated) |
| `field?: EntityType` | Relation `field [[Target]]` | `- works_at [[Y Combinator]]` |
| `field?(array): EntityType` | Multiple `field` relations | `- authored [[Book]]` (repeated) |
| `tags` | Frontmatter `tags` array | `tags: [startups, essays]` |
| `field?(enum): [vals]` | Observation `[field] value` where value is in the set | `- [status] active` |
Observations and relations not covered by the schema are valid — schemas describe a subset, not a straitjacket.
### Schema Attachment
Three ways to attach a schema to a note, resolved in priority order:
**1. Inline schema**`schema` is a dict in frontmatter:
```yaml
---
title: Team Standup 2024-01-15
type: meeting
schema:
attendees(array): string, who was there
decisions(array): string, what was decided
action_items(array): string, follow-ups
blockers?(array): string, anything stuck
---
```
Good for one-off structured notes or prototyping a schema before extracting it.
**2. Explicit reference**`schema` is a string naming a schema note:
```yaml
---
title: Basic Memory
schema: SoftwareProject
---
```
or by permalink:
```yaml
---
title: LLM Memory Patterns
schema: schema/research-project
---
```
Use when the note's `type` differs from the schema it should validate against, or when multiple schema variants exist.
**3. Implicit by type** — no `schema` field, resolved by matching `type`:
```yaml
---
title: Paul Graham
type: Person
---
```
The system looks up a schema note where `entity: Person`. If found, it applies. If not, no validation occurs.
**4. No schema** — perfectly fine. Most notes don't need one.
### Schema Notes
A schema is itself a Basic Memory note with `type: schema`. It lives anywhere (though `schema/` is the conventional directory).
```yaml
# schema/Person.md
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
email?: string, contact email
settings:
validation: warn
---
# Person
A human individual in the knowledge graph.
```
| Field | Required | Description |
|-------|----------|-------------|
| `type` | Yes | Must be `schema` |
| `entity` | Yes | The entity type this schema describes (e.g., `Person`) |
| `version` | No | Schema version number (default: `1`) |
| `schema` | Yes | Picoschema dict defining the fields |
| `settings.validation` | No | Validation mode (default: `warn`) |
Schema notes are regular notes — they show up in search, can have observations and relations, and participate in the knowledge graph.
### Validation Modes
| Mode | Behavior |
|------|----------|
| `warn` | Warnings in output, doesn't block (default) |
| `strict` | Errors that block sync, for CI/CD enforcement |
| `off` | No validation |
### Validation Output
```
$ bm schema validate people/ada-lovelace.md
⚠ Person schema validation:
- Missing required field: name (expected [name] observation)
- Missing optional field: role
- Missing optional field: works_at (no relation found)
Unmatched observations: [fact] ×2, [born] ×1
Unmatched relations: collaborated_with
```
"Unmatched" items are informational — observations and relations the schema doesn't cover.
### Schema Inference
Generate schemas from existing notes by analyzing observation and relation frequency:
```
$ bm schema infer Person
Analyzing 30 notes with type: Person...
Observations found:
[name] 30/30 100% → name: string
[role] 27/30 90% → role?: string
[expertise] 18/30 60% → expertise?(array): string
[email] 8/30 27% → email?: string
Relations found:
works_at 22/30 73% → works_at?: Organization
Suggested schema:
name: string, full name
role?: string, job title
expertise?(array): string, areas of knowledge
email?: string, contact email
works_at?: Organization, employer
Save to schema/Person.md? [y/n]
```
Frequency thresholds:
- **100% present** → required field
- **25%+ present** → optional field
- **Below 25%** → excluded from suggestion
### Schema Drift Detection
Track how usage patterns shift over time:
```
$ bm schema diff Person
Schema drift detected:
+ expertise: now in 81% of notes (was 12%)
- department: dropped to 3% of notes
~ works_at: cardinality changed (one → many)
Update schema? [y/n/review]
```
## Complete Examples
### Simple Note (No Schema)
```markdown
---
title: Project Ideas
type: note
tags: [ideas, brainstorm]
---
# Project Ideas
## Observations
- [idea] Build a CLI tool for markdown linting #tooling
- [idea] Create a recipe knowledge base #cooking
- [priority] Focus on developer tools first (Q1 goal)
## Relations
- inspired_by [[Developer Workflow Research]]
- part_of [[Q1 Planning]]
```
### Schema-Validated Note
Schema at `schema/Person.md`:
```yaml
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
email?: string, contact email
settings:
validation: warn
---
# Person
A human individual in the knowledge graph.
```
Note at `people/paul-graham.md`:
```markdown
---
title: Paul Graham
type: Person
tags: [startups, essays, lisp]
---
# Paul Graham
## Observations
- [name] Paul Graham
- [role] Essayist and investor
- [expertise] Startups
- [expertise] Lisp
- [expertise] Essay writing
- [fact] Created Viaweb, the first web app
## Relations
- works_at [[Y Combinator]]
- authored [[Hackers and Painters]]
```
The `[fact]` observation and `authored` relation are not in the schema — they're valid, just unmatched. The schema only checks that `[name]` exists (required) and looks for optional fields like `[role]`, `[expertise]`, and `works_at`.
### Inline Schema Note
```markdown
---
title: Team Standup 2024-01-15
type: meeting
schema:
attendees(array): string, who was there
decisions(array): string, what was decided
action_items(array): string, follow-ups
blockers?(array): string, anything stuck
---
# Team Standup 2024-01-15
## Observations
- [attendees] Paul
- [attendees] Sarah
- [decisions] Ship v2 by Friday
- [action_items] Paul to review PR #42
- [blockers] Waiting on API credentials
```
+365
View File
@@ -0,0 +1,365 @@
# SPEC-SCHEMA-IMPL: Schema System Implementation Plan
**Status:** Draft
**Created:** 2025-02-06
**Branch:** `feature/schema-system`
**Depends on:** [SPEC-SCHEMA](SPEC-SCHEMA.md)
## Overview
Implementation plan for the Basic Memory Schema System. The system is entirely programmatic —
no LLM agent runtime or API key required. The LLM already in the user's session (Claude Code,
Claude Desktop, etc.) provides the intelligence layer by reading schema notes via existing
MCP tools.
## Architecture
```
┌─────────────────────────────────────────────────┐
│ Entry Points │
│ CLI (bm schema ...) │ MCP (schema_validate) │
└──────────┬────────────┴──────────┬──────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────┐
│ Schema Service Layer │
│ resolve_schema · validate · infer · diff │
└──────────┬────────────────────────┬──────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌────────────────────────┐
│ Picoschema Parser │ │ Note/Entity Access │
│ YAML → SchemaModel │ │ (existing repository) │
└──────────────────────┘ └────────────────────────┘
```
No new database tables. Schemas are notes with `type: schema` — they're already indexed.
Validation reads observations and relations from existing data.
## Components
### 1. Picoschema Parser
**Location:** `src/basic_memory/schema/parser.py`
Parses Picoschema YAML into an internal representation.
```python
@dataclass
class SchemaField:
name: str
type: str # string, integer, number, boolean, any, or EntityName
required: bool # True unless field name ends with ?
is_array: bool # True if (array) notation
is_enum: bool # True if (enum) notation
enum_values: list[str] # Populated for enums
description: str | None # Text after comma
is_entity_ref: bool # True if type is capitalized (entity reference)
children: list[SchemaField] # For (object) types
@dataclass
class SchemaDefinition:
entity: str # The entity type this schema describes
version: int # Schema version
fields: list[SchemaField] # Parsed fields
validation_mode: str # "warn" | "strict" | "off"
def parse_picoschema(yaml_dict: dict) -> list[SchemaField]:
"""Parse a Picoschema YAML dict into a list of SchemaField objects."""
def parse_schema_note(frontmatter: dict) -> SchemaDefinition:
"""Parse a full schema note's frontmatter into a SchemaDefinition."""
```
**Input/Output:**
```yaml
# Input (YAML dict from frontmatter)
schema:
name: string, full name
role?: string, job title
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
```
```python
# Output
[
SchemaField(name="name", type="string", required=True, description="full name", ...),
SchemaField(name="role", type="string", required=False, description="job title", ...),
SchemaField(name="works_at", type="Organization", required=False, is_entity_ref=True, ...),
SchemaField(name="expertise", type="string", required=False, is_array=True, ...),
]
```
### 2. Schema Resolver
**Location:** `src/basic_memory/schema/resolver.py`
Finds the applicable schema for a note using the resolution order.
```python
async def resolve_schema(
note_frontmatter: dict,
search_fn: Callable, # injected search capability
) -> SchemaDefinition | None:
"""Resolve schema for a note.
Resolution order:
1. Inline schema (frontmatter['schema'] is a dict)
2. Explicit reference (frontmatter['schema'] is a string)
3. Implicit by type (frontmatter['type'] → schema note with matching entity)
4. No schema (returns None)
"""
```
### 3. Schema Validator
**Location:** `src/basic_memory/schema/validator.py`
Validates a note's observations and relations against a resolved schema.
```python
@dataclass
class FieldResult:
field: SchemaField
status: str # "present" | "missing" | "type_mismatch"
values: list[str] # Matched observation values or relation targets
message: str | None # Human-readable detail
@dataclass
class ValidationResult:
note_identifier: str
schema_entity: str
passed: bool # True if no errors (warnings are OK)
field_results: list[FieldResult]
unmatched_observations: dict[str, int] # category → count
unmatched_relations: list[str] # relation types not in schema
warnings: list[str]
errors: list[str]
async def validate_note(
note: Note,
schema: SchemaDefinition,
) -> ValidationResult:
"""Validate a note against a schema definition.
Mapping rules:
- field: string → observation [field] exists
- field?(array): type → multiple [field] observations
- field?: EntityType → relation 'field [[...]]' exists
- field?(enum): [v] → observation [field] value ∈ enum values
"""
```
### 4. Schema Inference Engine
**Location:** `src/basic_memory/schema/inference.py`
Analyzes notes of a given type and suggests a schema based on usage frequency.
```python
@dataclass
class FieldFrequency:
name: str
source: str # "observation" | "relation"
count: int # notes containing this field
total: int # total notes analyzed
percentage: float
sample_values: list[str] # representative values
is_array: bool # True if typically appears multiple times per note
target_type: str | None # For relations, the most common target entity type
@dataclass
class InferenceResult:
entity_type: str
notes_analyzed: int
field_frequencies: list[FieldFrequency]
suggested_schema: dict # Ready-to-use Picoschema YAML dict
suggested_required: list[str]
suggested_optional: list[str]
excluded: list[str] # Below threshold
async def infer_schema(
entity_type: str,
notes: list[Note],
required_threshold: float = 0.95, # 95%+ = required
optional_threshold: float = 0.25, # 25%+ = optional
) -> InferenceResult:
"""Analyze notes and suggest a Picoschema definition."""
```
### 5. Schema Diff
**Location:** `src/basic_memory/schema/diff.py`
Compares current note usage against an existing schema definition.
```python
@dataclass
class SchemaDrift:
new_fields: list[FieldFrequency] # Fields not in schema but common in notes
dropped_fields: list[FieldFrequency] # Fields in schema but rare in notes
cardinality_changes: list[str] # one → many or many → one
type_mismatches: list[str] # observation values don't match declared type
async def diff_schema(
schema: SchemaDefinition,
notes: list[Note],
) -> SchemaDrift:
"""Compare a schema against actual note usage to detect drift."""
```
## Entry Points
### CLI Commands
**Location:** `src/basic_memory/cli/schema.py`
```python
import typer
schema_app = typer.Typer(name="schema", help="Schema management commands")
@schema_app.command()
async def validate(
target: str = typer.Argument(None, help="Note path or entity type"),
strict: bool = typer.Option(False, help="Override to strict mode"),
):
"""Validate notes against their schemas."""
@schema_app.command()
async def infer(
entity_type: str = typer.Argument(..., help="Entity type to analyze"),
threshold: float = typer.Option(0.25, help="Minimum frequency for optional fields"),
save: bool = typer.Option(False, help="Save to schema/ directory"),
):
"""Infer schema from existing notes of a type."""
@schema_app.command()
async def diff(
entity_type: str = typer.Argument(..., help="Entity type to diff"),
):
"""Show drift between schema and actual usage."""
```
Registered as subcommand: `bm schema validate`, `bm schema infer`, `bm schema diff`.
### MCP Tools
**Location:** `src/basic_memory/mcp/tools/schema.py`
```python
@mcp_tool
async def schema_validate(
entity_type: str | None = None,
identifier: str | None = None,
project: str | None = None,
) -> str:
"""Validate notes against their resolved schema."""
@mcp_tool
async def schema_infer(
entity_type: str,
threshold: float = 0.25,
project: str | None = None,
) -> str:
"""Analyze existing notes and suggest a schema definition."""
```
### API Endpoints
**Location:** `src/basic_memory/api/schema_router.py`
```python
router = APIRouter(prefix="/schema", tags=["schema"])
@router.post("/validate")
async def validate_schema(...) -> ValidationReport: ...
@router.post("/infer")
async def infer_schema(...) -> InferenceResult: ...
@router.get("/diff/{entity_type}")
async def diff_schema(...) -> SchemaDrift: ...
```
MCP tools call these endpoints via the typed client pattern (consistent with existing
architecture).
## Implementation Phases
### Phase 1: Parser + Resolver
Build the foundation — can parse Picoschema and find schemas for notes.
**Deliverables:**
- `schema/parser.py` — Picoschema YAML → `SchemaDefinition`
- `schema/resolver.py` — Resolution order (inline → explicit ref → implicit by type → none)
- Unit tests for all Picoschema syntax variations
- Unit tests for resolution order
**No external dependencies.** Pure Python parsing of YAML dicts. Can develop and test
in isolation.
### Phase 2: Validator
Connect schemas to notes and produce validation results.
**Deliverables:**
- `schema/validator.py` — Validate note observations/relations against schema fields
- API endpoint: `POST /schema/validate`
- MCP tool: `schema_validate`
- CLI command: `bm schema validate`
- Integration tests with real notes and schemas
**Depends on:** Phase 1 (parser + resolver)
### Phase 3: Inference
Analyze existing notes to suggest schemas.
**Deliverables:**
- `schema/inference.py` — Frequency analysis across notes of a type
- API endpoint: `POST /schema/infer`
- MCP tool: `schema_infer`
- CLI command: `bm schema infer`
- Option to save inferred schema as a note via `write_note`
**Depends on:** Phase 1 (parser for output format)
### Phase 4: Diff
Compare schemas against current usage.
**Deliverables:**
- `schema/diff.py` — Drift detection between schema and actual notes
- API endpoint: `GET /schema/diff/{entity_type}`
- CLI command: `bm schema diff`
**Depends on:** Phase 1 (parser), Phase 3 (inference, for frequency analysis)
## Testing Strategy
- **Unit tests** (`tests/schema/`): Parser edge cases, resolution logic, validation mapping,
inference thresholds
- **Integration tests** (`test-int/schema/`): End-to-end with real markdown files, schema notes
on disk, CLI invocation
- Coverage target: 100% (consistent with project standard)
## What This Does NOT Include
- No new database tables or migrations
- No new markdown syntax (schemas validate existing observations/relations)
- No LLM agent runtime or API key management
- No hook integration (deferred)
- No schema composition/inheritance (deferred)
- No OWL/RDF export (deferred)
- No built-in templates (deferred)
+462
View File
@@ -0,0 +1,462 @@
# SPEC-SCHEMA: Basic Memory Schema System
**Status:** Draft
**Created:** 2025-02-06
**Branch:** `feature/schema-system`
## Summary
A schema system for Basic Memory that uses [Picoschema](https://genkit.dev/docs/dotprompt/)
syntax in YAML frontmatter. Schemas validate notes against their existing observation/relation
structure — no new data model, no migration, just a declarative lens over what's already there.
## Core Principles
1. **Schemas are just notes** — A schema is a note with `type: schema`, lives anywhere
2. **Use prior art** — Picoschema syntax in YAML frontmatter, no custom notation
3. **Validation maps to existing format** — Observations and relations, not a parallel data model
4. **Validation is soft** — Warnings by default, not blocking errors
5. **Inference over prescription** — Schemas describe reality, emerge from usage
6. **No built-in agent** — Programmatic core; the LLM already in the session provides intelligence
## Picoschema Syntax
Picoschema is a compact schema notation from Google's Dotprompt that fits naturally in YAML
frontmatter.
### Supported Types
| Type | Description |
|------|-------------|
| `string` | Text value |
| `integer` | Whole number |
| `number` | Decimal number |
| `boolean` | True/false |
| `any` | Any scalar type |
| `EntityName` | Reference to another entity (capitalized = entity reference) |
### Syntax Rules
```yaml
schema:
name: string, full name # required field with description
email?: string, contact email # ? = optional
role?: string, job title
works_at?: Organization, employer # capitalized type = entity reference
tags?(array): string, categories # array of type
status?(enum): [active, inactive] # enum with allowed values
metadata?(object): # nested object
updated_at?: string
source?: string
```
- `field: type` — required field
- `field?: type` — optional field
- `field(array): type` — array of values
- `field?(enum): [values]` — enumeration
- `field?(object):` — nested object with sub-fields
- `, description` — description after comma
- `EntityName` as type (capitalized) — reference to another entity
## Schema-to-Note Mapping
Schemas validate against the existing Basic Memory note format. No new syntax for note
authors to learn.
### Mapping Rules
| Schema Declaration | Grounded In | Example Match |
|--------------------|-------------|---------------|
| `field: string` | Observation `[field] value` | `- [name] Paul Graham` |
| `field?(array): string` | Multiple `[field]` observations | `- [expertise] Lisp` (×N) |
| `field?: EntityType` | Relation `field [[Target]]` | `- works_at [[Y Combinator]]` |
| `field?(array): EntityType` | Multiple `field` relations | `- authored [[Book]]` (×N) |
| `tags` | Frontmatter `tags` array | `tags: [startups, essays]` |
| `field?(enum): [values]` | Observation `[field] value` where value ∈ set | `- [status] active` |
### Key Insight
Schemas don't introduce a new way to store data. They describe the patterns already present
in observations and relations. A note doesn't have to change how it's written — the schema
just says "a good Person note has a `[name]` observation and a `works_at` relation."
## Schema Definition
### As a Dedicated Schema Note
```yaml
# schema/Person.md
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
email?: string, contact email
role?: string, job title
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
settings:
validation: warn # warn | strict | off
---
# Person
A human individual in the knowledge graph.
Any documentation about this entity type goes here as prose.
```
Schema notes are regular Basic Memory notes. They show up in search, can have their own
observations and relations, and can be organized in any folder (though `schema/` is
the suggested convention).
### Inline Schema in a Note
Notes can carry their own schema directly:
```yaml
# meetings/2024-01-15-standup.md
---
title: Team Standup 2024-01-15
type: meeting
schema:
attendees(array): string, who was there
decisions(array): string, what was decided
action_items(array): string, follow-ups
blockers?(array): string, anything stuck
---
# Team Standup 2024-01-15
## Observations
- [attendees] Paul
- [attendees] Sarah
- [decisions] Ship v2 by Friday
- [action_items] Paul to review PR #42
- [blockers] Waiting on API credentials
```
Good for one-off structured notes or prototyping a schema before extracting it.
### Explicit Schema Reference
A note can reference a schema by entity name or permalink:
```yaml
# projects/basic-memory.md
---
title: Basic Memory
schema: SoftwareProject # by entity name
---
# research/llm-memory-patterns.md
---
title: LLM Memory Patterns
schema: schema/research-project # by permalink
---
```
Use cases:
- Note's `type` differs from the schema it should validate against
- Multiple schema variants exist for the same domain
- Applying structure to existing notes without changing their type
## Schema Resolution
When validating a note, schemas resolve in priority order:
```
1. Inline schema → schema: { ... } (dict in frontmatter)
2. Explicit ref → schema: Person (string in frontmatter)
3. Implicit by type → type: Person (lookup schema note with entity: Person)
4. No schema → no validation (perfectly fine)
```
```python
async def resolve_schema(note: Note) -> Schema | None:
schema_value = note.frontmatter.get('schema')
# 1. Inline schema (dict)
if isinstance(schema_value, dict):
return parse_picoschema(schema_value)
# 2. Explicit reference (string)
if isinstance(schema_value, str):
schema_note = await find_schema_note(schema_value)
if schema_note:
return parse_picoschema(schema_note.frontmatter['schema'])
# 3. Implicit by type
note_type = note.frontmatter.get('type')
if note_type:
results = await search_notes(f"type:schema entity:{note_type}")
if results:
return parse_picoschema(results[0].frontmatter['schema'])
# 4. No schema
return None
```
## Validation
### Modes
Configured in the schema's `settings.validation`:
| Mode | Behavior |
|------|----------|
| `off` | No validation |
| `warn` | Warnings in output, doesn't block (default) |
| `strict` | Errors that block sync, for CI/CD enforcement |
### Validation Output
For a note missing required fields:
```
$ bm schema validate people/ada-lovelace.md
⚠ Person schema validation:
- Missing required field: name (expected [name] observation)
- Missing optional field: role
- Missing optional field: works_at (no relation found)
Unmatched observations: [fact] ×2, [born] ×1
Unmatched relations: collaborated_with
```
"Unmatched" items are informational — observations and relations the schema doesn't cover.
They're valid. Schemas are a subset, not a straitjacket.
### Batch Validation
```
$ bm schema validate Person
Validating 30 notes against Person schema...
✓ people/paul-graham.md — all fields present
✓ people/rich-hickey.md — all fields present
⚠ people/ada-lovelace.md — missing: name
⚠ people/alan-kay.md — missing: name, role
✓ people/linus-torvalds.md — all fields present
...
Summary: 22/30 valid, 8 warnings, 0 errors
```
## Emerging Schemas
### The Problem with Traditional Schemas
Most schema systems require: define schema → create conforming content → fight the schema
when reality doesn't match. This is backwards. Knowledge grows organically.
### The Basic Memory Approach
```
Write notes freely → Patterns emerge → Crystallize into schema → Validate future notes
```
### Schema Inference
Generate schemas from existing notes by analyzing observation and relation frequency:
```
$ bm schema infer Person
Analyzing 30 notes with type: Person...
Observations found:
[name] 30/30 100% → name: string
[role] 27/30 90% → role?: string
[fact] 25/30 83% (generic — no single field)
[expertise] 18/30 60% → expertise?(array): string
[email] 8/30 27% → email?: string
[born] 6/30 20% (below threshold)
Relations found:
works_at 22/30 73% → works_at?: Organization
authored 11/30 37% → authored?(array): string
Suggested schema:
name: string, full name
role?: string, job title
expertise?(array): string, areas of knowledge
email?: string, contact email
works_at?: Organization, employer
Save to schema/Person.md? [y/n]
```
Frequency thresholds:
- 100% present → required field
- 25%+ present → optional field
- Below 25% → excluded from suggestion (but noted)
### Schema Drift Detection
Track how usage patterns shift over time:
```
$ bm schema diff Person
Schema drift detected:
+ expertise: now in 81% of notes (was 12%)
- department: dropped to 3% of notes
~ works_at: cardinality changed (one → many)
Update schema? [y/n/review]
```
## LLM Integration (AI Guidance)
No agent runtime or API key required. The LLM already in the session uses schemas as
context for note creation.
### Flow
1. User asks LLM to "write a note about Rich Hickey"
2. LLM determines `type: Person` is appropriate
3. LLM calls `search_notes("type:schema entity:Person")` → finds schema
4. LLM reads schema fields: required `name`, optional `role`, `works_at`, `expertise`
5. LLM calls `write_note` with observations and relations that satisfy the schema
The schema acts as a creation template. The LLM knows what a "complete" note looks like
without any custom agent infrastructure.
### MCP Tools
```python
@mcp_tool
async def schema_validate(
entity_type: str | None = None,
identifier: str | None = None,
project: str | None = None,
) -> ValidationReport:
"""Validate notes against their resolved schema.
Validates a specific note (by identifier) or all notes of a given type.
Returns warnings/errors based on the schema's validation mode.
"""
@mcp_tool
async def schema_infer(
entity_type: str,
threshold: float = 0.25,
project: str | None = None,
) -> SuggestedSchema:
"""Analyze existing notes and suggest a schema definition.
Examines observation categories and relation types across all notes
of the given type. Returns frequency analysis and suggested Picoschema.
"""
```
## CLI Commands
```bash
# Validate a specific note
bm schema validate people/ada-lovelace.md
# Validate all notes of a type
bm schema validate Person
# Validate everything with a schema
bm schema validate
# Infer schema from existing notes
bm schema infer Person
# Show schema drift from current definition
bm schema diff Person
# List all schema notes
bm search "type:schema"
```
## Examples
### Complete Person Workflow
**Schema:**
```yaml
# schema/Person.md
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
email?: string, contact email
settings:
validation: warn
---
# Person
A human individual in the knowledge graph.
```
**Valid note:**
```yaml
# people/paul-graham.md
---
title: Paul Graham
type: Person
tags: [startups, essays, lisp]
---
# Paul Graham
## Observations
- [name] Paul Graham
- [role] Essayist and investor
- [expertise] Startups
- [expertise] Lisp
- [expertise] Essay writing
- [fact] Created Viaweb, the first web app
## Relations
- works_at [[Y Combinator]]
- authored [[Hackers and Painters]]
```
**Note with warnings:**
```yaml
# people/ada-lovelace.md
---
title: Ada Lovelace
type: Person
---
# Ada Lovelace
## Observations
- [fact] Wrote the first computer program
- [born] 1815
## Relations
- collaborated_with [[Charles Babbage]]
```
Validation: warns about missing required `[name]` observation. Everything else is optional
or unmatched (which is fine).
## Future Considerations (Deferred)
These are interesting but out of scope for the initial implementation:
- **Multiple schema inheritance** — `schema: [Person, Author]`
- **Hook integration** — Pre-write validation via the hooks system
- **OWL/RDF export** — `bm schema export --format owl`
- **SPARQL queries** — Schema-aware graph queries
- **Built-in templates** — `bm schema use gtd`, `bm schema use zettelkasten`
- **Schema versioning/migration** — Tracking breaking changes across versions
+2
View File
@@ -18,6 +18,7 @@ from basic_memory.api.v2.routers import (
directory_router as v2_directory,
prompt_router as v2_prompt,
importer_router as v2_importer,
schema_router as v2_schema,
)
from basic_memory.api.v2.routers.project_router import (
add_project,
@@ -84,6 +85,7 @@ app.include_router(v2_resource, prefix="/v2/projects/{project_id}")
app.include_router(v2_directory, prefix="/v2/projects/{project_id}")
app.include_router(v2_prompt, prefix="/v2/projects/{project_id}")
app.include_router(v2_importer, prefix="/v2/projects/{project_id}")
app.include_router(v2_schema, prefix="/v2/projects/{project_id}")
app.include_router(v2_project, prefix="/v2")
# Legacy web app proxy paths (compat with /proxy/projects/projects)
@@ -8,6 +8,7 @@ from basic_memory.api.v2.routers.resource_router import router as resource_route
from basic_memory.api.v2.routers.directory_router import router as directory_router
from basic_memory.api.v2.routers.prompt_router import router as prompt_router
from basic_memory.api.v2.routers.importer_router import router as importer_router
from basic_memory.api.v2.routers.schema_router import router as schema_router
__all__ = [
"knowledge_router",
@@ -18,4 +19,5 @@ __all__ = [
"directory_router",
"prompt_router",
"importer_router",
"schema_router",
]
@@ -0,0 +1,305 @@
"""V2 router for schema operations.
Provides endpoints for schema validation, inference, and drift detection.
The schema system validates notes against Picoschema definitions without
introducing any new data model -- it works entirely with existing
observations and relations.
Flow: Entity loaded with eager observations/relations -> convert to tuples -> core functions.
"""
from fastapi import APIRouter, Path, Query
from basic_memory.deps import (
SearchServiceV2ExternalDep,
EntityRepositoryV2ExternalDep,
)
from basic_memory.models.knowledge import Entity
from basic_memory.schemas.schema import (
ValidationReport,
InferenceReport,
DriftReport,
NoteValidationResponse,
FieldResultResponse,
FieldFrequencyResponse,
DriftFieldResponse,
)
from basic_memory.schemas.search import SearchQuery
from basic_memory.schema.resolver import resolve_schema
from basic_memory.schema.validator import validate_note
from basic_memory.schema.inference import infer_schema, NoteData, ObservationData, RelationData
from basic_memory.schema.diff import diff_schema
# Note: No prefix here -- it's added during registration as /v2/{project_id}/schema
router = APIRouter(tags=["schema"])
# --- ORM to core data conversion ---
def _entity_observations(entity: Entity) -> list[ObservationData]:
"""Extract ObservationData from an entity's observations."""
return [ObservationData(obs.category, obs.content) for obs in entity.observations]
def _entity_relations(entity: Entity) -> list[RelationData]:
"""Extract RelationData from an entity's outgoing relations.
Carries the target entity's type on each relation so the inference engine
can suggest correct types (e.g. works_at -> Organization, not the source type).
"""
return [
RelationData(
relation_type=rel.relation_type,
target_name=rel.to_name,
target_entity_type=rel.to_entity.entity_type if rel.to_entity else None,
)
for rel in entity.outgoing_relations
]
def _entity_to_note_data(entity: Entity) -> NoteData:
"""Convert an ORM Entity to a NoteData for inference/diff analysis."""
return NoteData(
identifier=entity.permalink or entity.file_path,
observations=_entity_observations(entity),
relations=_entity_relations(entity),
)
def _entity_frontmatter(entity: Entity) -> dict:
"""Build a frontmatter dict from an entity for schema resolution."""
frontmatter = dict(entity.entity_metadata) if entity.entity_metadata else {}
if entity.entity_type:
frontmatter.setdefault("type", entity.entity_type)
return frontmatter
# --- Validation ---
@router.post("/schema/validate", response_model=ValidationReport)
async def validate_schema(
entity_repository: EntityRepositoryV2ExternalDep,
search_service: SearchServiceV2ExternalDep,
project_id: str = Path(..., description="Project external UUID"),
entity_type: str | None = Query(None, description="Entity type to validate"),
identifier: str | None = Query(None, description="Specific note identifier"),
):
"""Validate notes against their resolved schemas.
Validates a specific note (by identifier) or all notes of a given type.
Returns warnings/errors based on the schema's validation mode.
"""
results: list[NoteValidationResponse] = []
async def search_fn(query: str) -> list:
# Search for schema notes, then load full entity_metadata from the entity table.
# The search index only stores minimal metadata (e.g., {"entity_type": "schema"}),
# but parse_schema_note needs the full frontmatter with entity/schema/version keys.
results = await search_service.search(SearchQuery(text=query, types=["schema"]), limit=5)
frontmatters = []
for row in results:
if row.permalink:
entity = await entity_repository.get_by_permalink(row.permalink)
if entity:
frontmatters.append(_entity_frontmatter(entity))
return frontmatters
# --- Single note validation ---
if identifier:
entity = await entity_repository.get_by_permalink(identifier)
if not entity:
return ValidationReport(entity_type=entity_type, total_notes=0, results=[])
schema_def = await resolve_schema(_entity_frontmatter(entity), search_fn)
if schema_def:
result = validate_note(
entity.permalink or identifier,
schema_def,
_entity_observations(entity),
_entity_relations(entity),
)
results.append(_to_note_validation_response(result))
return ValidationReport(
entity_type=entity_type or entity.entity_type,
total_notes=1,
valid_count=1 if (results and results[0].passed) else 0,
warning_count=sum(len(r.warnings) for r in results),
error_count=sum(len(r.errors) for r in results),
results=results,
)
# --- Batch validation by entity type ---
entities = await _find_by_entity_type(entity_repository, entity_type) if entity_type else []
for entity in entities:
schema_def = await resolve_schema(_entity_frontmatter(entity), search_fn)
if schema_def:
result = validate_note(
entity.permalink or entity.file_path,
schema_def,
_entity_observations(entity),
_entity_relations(entity),
)
results.append(_to_note_validation_response(result))
valid = sum(1 for r in results if r.passed)
return ValidationReport(
entity_type=entity_type,
total_notes=len(results),
valid_count=valid,
warning_count=sum(len(r.warnings) for r in results),
error_count=sum(len(r.errors) for r in results),
results=results,
)
# --- Inference ---
@router.post("/schema/infer", response_model=InferenceReport)
async def infer_schema_endpoint(
entity_repository: EntityRepositoryV2ExternalDep,
project_id: str = Path(..., description="Project external UUID"),
entity_type: str = Query(..., description="Entity type to analyze"),
threshold: float = Query(0.25, description="Minimum frequency for optional fields"),
):
"""Infer a schema from existing notes of a given type.
Examines observation categories and relation types across all notes
of the given type. Returns frequency analysis and suggested Picoschema.
"""
entities = await _find_by_entity_type(entity_repository, entity_type)
notes_data = [_entity_to_note_data(entity) for entity in entities]
result = infer_schema(entity_type, notes_data, optional_threshold=threshold)
return InferenceReport(
entity_type=result.entity_type,
notes_analyzed=result.notes_analyzed,
field_frequencies=[
FieldFrequencyResponse(
name=f.name,
source=f.source,
count=f.count,
total=f.total,
percentage=f.percentage,
sample_values=f.sample_values,
is_array=f.is_array,
target_type=f.target_type,
)
for f in result.field_frequencies
],
suggested_schema=result.suggested_schema,
suggested_required=result.suggested_required,
suggested_optional=result.suggested_optional,
excluded=result.excluded,
)
# --- Drift Detection ---
@router.get("/schema/diff/{entity_type}", response_model=DriftReport)
async def diff_schema_endpoint(
entity_repository: EntityRepositoryV2ExternalDep,
search_service: SearchServiceV2ExternalDep,
entity_type: str = Path(..., description="Entity type to check for drift"),
project_id: str = Path(..., description="Project external UUID"),
):
"""Show drift between a schema definition and actual note usage.
Compares the existing schema for an entity type against how notes
of that type are actually structured. Identifies new fields, dropped
fields, and cardinality changes.
"""
async def search_fn(query: str) -> list:
# Search for schema notes, then load full entity_metadata from the entity table.
# The search index only stores minimal metadata (e.g., {"entity_type": "schema"}),
# but parse_schema_note needs the full frontmatter with entity/schema/version keys.
results = await search_service.search(SearchQuery(text=query, types=["schema"]), limit=5)
frontmatters = []
for row in results:
if row.permalink:
entity = await entity_repository.get_by_permalink(row.permalink)
if entity:
frontmatters.append(_entity_frontmatter(entity))
return frontmatters
# Resolve schema by entity type
schema_frontmatter = {"type": entity_type}
schema_def = await resolve_schema(schema_frontmatter, search_fn)
if not schema_def:
return DriftReport(entity_type=entity_type)
# Collect all notes of this type
entities = await _find_by_entity_type(entity_repository, entity_type)
notes_data = [_entity_to_note_data(entity) for entity in entities]
result = diff_schema(schema_def, notes_data)
return DriftReport(
entity_type=entity_type,
new_fields=[
DriftFieldResponse(
name=f.name,
source=f.source,
count=f.count,
total=f.total,
percentage=f.percentage,
)
for f in result.new_fields
],
dropped_fields=[
DriftFieldResponse(
name=f.name,
source=f.source,
count=f.count,
total=f.total,
percentage=f.percentage,
)
for f in result.dropped_fields
],
cardinality_changes=result.cardinality_changes,
)
# --- Helpers ---
async def _find_by_entity_type(
entity_repository: EntityRepositoryV2ExternalDep,
entity_type: str,
) -> list[Entity]:
"""Find all entities of a given type using the repository's select pattern."""
query = entity_repository.select().where(Entity.entity_type == entity_type)
result = await entity_repository.execute_query(query)
return list(result.scalars().all())
def _to_note_validation_response(result) -> NoteValidationResponse:
"""Convert a core ValidationResult to a Pydantic response model."""
return NoteValidationResponse(
note_identifier=result.note_identifier,
schema_entity=result.schema_entity,
passed=result.passed,
field_results=[
FieldResultResponse(
field_name=fr.field.name,
field_type=fr.field.type,
required=fr.field.required,
status=fr.status,
values=fr.values,
message=fr.message,
)
for fr in result.field_results
],
unmatched_observations=result.unmatched_observations,
unmatched_relations=result.unmatched_relations,
warnings=result.warnings,
errors=result.errors,
)
+2 -1
View File
@@ -1,7 +1,7 @@
"""CLI commands for basic-memory."""
from . import status, db, doctor, import_memory_json, mcp, import_claude_conversations
from . import import_claude_projects, import_chatgpt, tool, project, format
from . import import_claude_projects, import_chatgpt, tool, project, format, schema
__all__ = [
"status",
@@ -15,4 +15,5 @@ __all__ = [
"tool",
"project",
"format",
"schema",
]
+336
View File
@@ -0,0 +1,336 @@
"""Schema management CLI commands for Basic Memory.
Provides CLI access to schema validation, inference, and drift detection.
Registered as a subcommand group: `bm schema validate`, `bm schema infer`, `bm schema diff`.
"""
import json
from typing import Annotated, Optional
import typer
from loguru import logger
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from basic_memory.cli.app import app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.cli.commands.routing import force_routing, validate_routing_flags
from basic_memory.config import ConfigManager
from basic_memory.mcp.async_client import get_client
from basic_memory.mcp.project_context import get_active_project
console = Console()
schema_app = typer.Typer(help="Schema management commands")
app.add_typer(schema_app, name="schema")
def _resolve_project_name(project: Optional[str]) -> Optional[str]:
"""Resolve project name from CLI argument or config default."""
config_manager = ConfigManager()
if project is not None:
project_name, _ = config_manager.get_project(project)
if not project_name:
typer.echo(f"No project found named: {project}", err=True)
raise typer.Exit(1)
return project_name
return config_manager.default_project
# --- Validate ---
async def _run_validate(
target: Optional[str] = None,
project: Optional[str] = None,
strict: bool = False,
):
"""Run schema validation via the API."""
from basic_memory.mcp.clients.schema import SchemaClient
async with get_client() as client:
active_project = await get_active_project(client, project, None)
schema_client = SchemaClient(client, active_project.external_id)
# Determine if target is a note identifier or entity type
# Heuristic: if target contains / or ., treat as identifier
entity_type = None
identifier = None
if target:
if "/" in target or "." in target:
identifier = target
else:
entity_type = target
report = await schema_client.validate(
entity_type=entity_type,
identifier=identifier,
)
# --- Display results ---
if report.total_notes == 0:
console.print("[yellow]No notes matched for validation.[/yellow]")
return
table = Table(title=f"Schema Validation: {entity_type or identifier or 'all'}")
table.add_column("Note", style="cyan")
table.add_column("Status", justify="center")
table.add_column("Warnings", justify="right")
table.add_column("Errors", justify="right")
for result in report.results:
if result.passed and not result.warnings:
status = "[green]pass[/green]"
elif result.passed:
status = "[yellow]warn[/yellow]"
else:
status = "[red]fail[/red]"
table.add_row(
result.note_identifier,
status,
str(len(result.warnings)),
str(len(result.errors)),
)
console.print(table)
console.print(
f"\nSummary: {report.valid_count}/{report.total_notes} valid, "
f"{report.warning_count} warnings, {report.error_count} errors"
)
# Exit with error code in strict mode if there are failures
if strict and report.error_count > 0:
raise typer.Exit(1)
@schema_app.command()
def validate(
target: Annotated[
Optional[str],
typer.Argument(help="Note path or entity type to validate"),
] = None,
project: Annotated[
Optional[str],
typer.Option(help="The project name."),
] = None,
strict: bool = typer.Option(False, "--strict", help="Exit with error on validation failures"),
local: bool = typer.Option(
False, "--local", help="Force local API routing (ignore cloud mode)"
),
cloud: bool = typer.Option(False, "--cloud", help="Force cloud API routing"),
):
"""Validate notes against their schemas.
TARGET can be a note path (e.g., people/ada-lovelace.md) or an entity type
(e.g., Person). If omitted, validates all notes that have schemas.
Use --strict to exit with error code 1 if any validation errors are found.
Use --local to force local routing when cloud mode is enabled.
Use --cloud to force cloud routing when cloud mode is disabled.
"""
try:
validate_routing_flags(local, cloud)
project_name = _resolve_project_name(project)
with force_routing(local=local, cloud=cloud):
run_with_cleanup(_run_validate(target, project_name, strict))
except ValueError as e:
console.print(f"[red]Error: {e}[/red]")
raise typer.Exit(1)
except Exception as e:
if not isinstance(e, typer.Exit):
logger.error(f"Error during schema validate: {e}")
typer.echo(f"Error during schema validate: {e}", err=True)
raise typer.Exit(1)
raise
# --- Infer ---
async def _run_infer(
entity_type: str,
project: Optional[str] = None,
threshold: float = 0.25,
save: bool = False,
):
"""Run schema inference via the API."""
from basic_memory.mcp.clients.schema import SchemaClient
async with get_client() as client:
active_project = await get_active_project(client, project, None)
schema_client = SchemaClient(client, active_project.external_id)
report = await schema_client.infer(entity_type, threshold=threshold)
if report.notes_analyzed == 0:
console.print(f"[yellow]No notes found with type: {entity_type}[/yellow]")
return
# --- Display frequency analysis ---
console.print(
f"\n[bold]Analyzing {report.notes_analyzed} notes with type: {entity_type}...[/bold]\n"
)
table = Table(title="Field Frequencies")
table.add_column("Field", style="cyan")
table.add_column("Source")
table.add_column("Count", justify="right")
table.add_column("Percentage", justify="right")
table.add_column("Suggested")
for freq in report.field_frequencies:
pct = f"{freq.percentage:.0%}"
if freq.name in report.suggested_required:
suggested = "[green]required[/green]"
elif freq.name in report.suggested_optional:
suggested = "[yellow]optional[/yellow]"
else:
suggested = "[dim]excluded[/dim]"
table.add_row(
freq.name,
freq.source,
str(freq.count),
pct,
suggested,
)
console.print(table)
# --- Display suggested schema ---
console.print("\n[bold]Suggested schema:[/bold]")
console.print(Panel(json.dumps(report.suggested_schema, indent=2), title="Picoschema"))
if save:
console.print(
f"\n[yellow]--save not yet implemented. "
f"Copy the schema above into schema/{entity_type}.md[/yellow]"
)
@schema_app.command()
def infer(
entity_type: Annotated[
str,
typer.Argument(help="Entity type to analyze (e.g., Person, meeting)"),
],
project: Annotated[
Optional[str],
typer.Option(help="The project name."),
] = None,
threshold: float = typer.Option(
0.25, "--threshold", help="Minimum frequency for optional fields (0-1)"
),
save: bool = typer.Option(False, "--save", help="Save inferred schema to schema/ directory"),
local: bool = typer.Option(
False, "--local", help="Force local API routing (ignore cloud mode)"
),
cloud: bool = typer.Option(False, "--cloud", help="Force cloud API routing"),
):
"""Infer schema from existing notes of a type.
Analyzes all notes with the given entity type and suggests a Picoschema
definition based on observation and relation frequency.
Fields present in 95%+ of notes become required. Fields above the
threshold (default 25%) become optional. Fields below threshold are excluded.
Use --local to force local routing when cloud mode is enabled.
Use --cloud to force cloud routing when cloud mode is disabled.
"""
try:
validate_routing_flags(local, cloud)
project_name = _resolve_project_name(project)
with force_routing(local=local, cloud=cloud):
run_with_cleanup(_run_infer(entity_type, project_name, threshold, save))
except ValueError as e:
console.print(f"[red]Error: {e}[/red]")
raise typer.Exit(1)
except Exception as e:
if not isinstance(e, typer.Exit):
logger.error(f"Error during schema infer: {e}")
typer.echo(f"Error during schema infer: {e}", err=True)
raise typer.Exit(1)
raise
# --- Diff ---
async def _run_diff(
entity_type: str,
project: Optional[str] = None,
):
"""Run schema drift detection via the API."""
from basic_memory.mcp.clients.schema import SchemaClient
async with get_client() as client:
active_project = await get_active_project(client, project, None)
schema_client = SchemaClient(client, active_project.external_id)
report = await schema_client.diff(entity_type)
has_drift = report.new_fields or report.dropped_fields or report.cardinality_changes
if not has_drift:
console.print(f"[green]No drift detected for {entity_type} schema.[/green]")
return
console.print(f"\n[bold]Schema drift detected for {entity_type}:[/bold]\n")
if report.new_fields:
console.print("[green]+ New fields (common in notes, not in schema):[/green]")
for f in report.new_fields:
console.print(f" + {f.name}: {f.percentage:.0%} of notes ({f.source})")
if report.dropped_fields:
console.print("[red]- Dropped fields (in schema, rare in notes):[/red]")
for f in report.dropped_fields:
console.print(f" - {f.name}: {f.percentage:.0%} of notes ({f.source})")
if report.cardinality_changes:
console.print("[yellow]~ Cardinality changes:[/yellow]")
for change in report.cardinality_changes:
console.print(f" ~ {change}")
@schema_app.command()
def diff(
entity_type: Annotated[
str,
typer.Argument(help="Entity type to check for drift"),
],
project: Annotated[
Optional[str],
typer.Option(help="The project name."),
] = None,
local: bool = typer.Option(
False, "--local", help="Force local API routing (ignore cloud mode)"
),
cloud: bool = typer.Option(False, "--cloud", help="Force cloud API routing"),
):
"""Show drift between schema and actual usage.
Compares the existing schema definition for an entity type against
how notes of that type are actually structured. Identifies new fields,
dropped fields, and cardinality changes.
Use --local to force local routing when cloud mode is enabled.
Use --cloud to force cloud routing when cloud mode is disabled.
"""
try:
validate_routing_flags(local, cloud)
project_name = _resolve_project_name(project)
with force_routing(local=local, cloud=cloud):
run_with_cleanup(_run_diff(entity_type, project_name))
except ValueError as e:
console.print(f"[red]Error: {e}[/red]")
raise typer.Exit(1)
except Exception as e:
if not isinstance(e, typer.Exit):
logger.error(f"Error during schema diff: {e}")
typer.echo(f"Error during schema diff: {e}", err=True)
raise typer.Exit(1)
raise
+1
View File
@@ -13,6 +13,7 @@ from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover
import_memory_json,
mcp,
project,
schema,
status,
tool,
)
+2
View File
@@ -17,6 +17,7 @@ from basic_memory.mcp.clients.memory import MemoryClient
from basic_memory.mcp.clients.directory import DirectoryClient
from basic_memory.mcp.clients.resource import ResourceClient
from basic_memory.mcp.clients.project import ProjectClient
from basic_memory.mcp.clients.schema import SchemaClient
__all__ = [
"KnowledgeClient",
@@ -25,4 +26,5 @@ __all__ = [
"DirectoryClient",
"ResourceClient",
"ProjectClient",
"SchemaClient",
]
+113
View File
@@ -0,0 +1,113 @@
"""Typed client for schema API operations.
Encapsulates all /v2/projects/{project_id}/schema/* endpoints.
"""
from httpx import AsyncClient
from basic_memory.mcp.tools.utils import call_post, call_get
from basic_memory.schemas.schema import (
ValidationReport,
InferenceReport,
DriftReport,
)
class SchemaClient:
"""Typed client for schema operations.
Centralizes:
- API path construction for /v2/projects/{project_id}/schema/*
- Response validation via Pydantic models
- Consistent error handling through call_* utilities
Usage:
async with get_client() as http_client:
client = SchemaClient(http_client, project_id)
report = await client.validate(entity_type="Person")
"""
def __init__(self, http_client: AsyncClient, project_id: str):
"""Initialize the schema client.
Args:
http_client: HTTPX AsyncClient for making requests
project_id: Project external_id (UUID) for API calls
"""
self.http_client = http_client
self.project_id = project_id
self._base_path = f"/v2/projects/{project_id}/schema"
async def validate(
self,
*,
entity_type: str | None = None,
identifier: str | None = None,
) -> ValidationReport:
"""Validate notes against their resolved schemas.
Args:
entity_type: Optional entity type to batch-validate
identifier: Optional specific note to validate
Returns:
ValidationReport with per-note results
Raises:
ToolError: If the request fails
"""
params: dict[str, str] = {}
if entity_type:
params["entity_type"] = entity_type
if identifier:
params["identifier"] = identifier
response = await call_post(
self.http_client,
f"{self._base_path}/validate",
params=params,
)
return ValidationReport.model_validate(response.json())
async def infer(
self,
entity_type: str,
*,
threshold: float = 0.25,
) -> InferenceReport:
"""Infer a schema from existing notes of a given type.
Args:
entity_type: The entity type to analyze
threshold: Minimum frequency for optional fields (0-1)
Returns:
InferenceReport with frequency data and suggested schema
Raises:
ToolError: If the request fails
"""
response = await call_post(
self.http_client,
f"{self._base_path}/infer",
params={"entity_type": entity_type, "threshold": threshold},
)
return InferenceReport.model_validate(response.json())
async def diff(self, entity_type: str) -> DriftReport:
"""Show drift between schema definition and actual usage.
Args:
entity_type: The entity type to check for drift
Returns:
DriftReport with detected differences
Raises:
ToolError: If the request fails
"""
response = await call_get(
self.http_client,
f"{self._base_path}/diff/{entity_type}",
)
return DriftReport.model_validate(response.json())
+6
View File
@@ -27,6 +27,9 @@ from basic_memory.mcp.tools.project_management import (
# ChatGPT-compatible tools
from basic_memory.mcp.tools.chatgpt_tools import search, fetch
# Schema tools
from basic_memory.mcp.tools.schema import schema_validate, schema_infer, schema_diff
__all__ = [
"build_context",
"canvas",
@@ -41,6 +44,9 @@ __all__ = [
"read_content",
"read_note",
"recent_activity",
"schema_diff",
"schema_infer",
"schema_validate",
"search",
"search_by_metadata",
"search_notes",
+244
View File
@@ -0,0 +1,244 @@
"""Schema tools for Basic Memory MCP server.
Provides tools for schema validation, inference, and drift detection through the MCP protocol.
These tools call the schema API endpoints via the typed SchemaClient.
"""
from typing import Optional
from loguru import logger
from fastmcp import Context
from basic_memory.mcp.async_client import get_client
from basic_memory.mcp.project_context import get_active_project
from basic_memory.mcp.server import mcp
from basic_memory.schemas.schema import ValidationReport, InferenceReport, DriftReport
@mcp.tool(
description="Validate notes against their Picoschema definitions.",
)
async def schema_validate(
entity_type: Optional[str] = None,
identifier: Optional[str] = None,
project: Optional[str] = None,
context: Context | None = None,
) -> ValidationReport | str:
"""Validate notes against their resolved schema.
Validates a specific note (by identifier) or all notes of a given type.
Returns warnings/errors based on the schema's validation mode.
Schemas are resolved in priority order:
1. Inline schema (dict in frontmatter)
2. Explicit reference (string in frontmatter)
3. Implicit by type (type field matches schema note entity field)
4. No schema (no validation)
Project Resolution:
Server resolves projects in this order: Single Project Mode -> project parameter -> default.
If project unknown, use list_memory_projects() first.
Args:
entity_type: Entity type to batch-validate (e.g., "Person").
If provided, validates all notes of this type.
identifier: Specific note to validate (permalink, title, or path).
If provided, validates only this note.
project: Project name. Optional -- server will resolve.
context: Optional FastMCP context for performance caching.
Returns:
ValidationReport with per-note results, or error guidance string
Examples:
# Validate all Person notes
schema_validate(entity_type="Person")
# Validate a specific note
schema_validate(identifier="people/paul-graham")
# Validate in a specific project
schema_validate(entity_type="Person", project="my-research")
"""
async with get_client() as client:
active_project = await get_active_project(client, project, context)
logger.info(
f"MCP tool call tool=schema_validate project={active_project.name} "
f"entity_type={entity_type} identifier={identifier}"
)
try:
from basic_memory.mcp.clients.schema import SchemaClient
schema_client = SchemaClient(client, active_project.external_id)
result = await schema_client.validate(
entity_type=entity_type,
identifier=identifier,
)
logger.info(
f"MCP tool response: tool=schema_validate project={active_project.name} "
f"total={result.total_notes} valid={result.valid_count} "
f"warnings={result.warning_count} errors={result.error_count}"
)
return result
except Exception as e:
logger.error(f"Schema validation failed: {e}, project: {active_project.name}")
return (
f"# Schema Validation Failed\n\n"
f"Error validating schemas: {e}\n\n"
f"## Troubleshooting\n"
f"1. Ensure schema notes exist (type: schema) for the target entity type\n"
f"2. Check that notes have the correct type in frontmatter\n"
f"3. Verify the project has been synced: `basic-memory status`\n"
)
@mcp.tool(
description="Analyze existing notes and suggest a Picoschema definition.",
)
async def schema_infer(
entity_type: str,
threshold: float = 0.25,
project: Optional[str] = None,
context: Context | None = None,
) -> InferenceReport | str:
"""Analyze existing notes and suggest a schema definition.
Examines observation categories and relation types across all notes
of the given type. Returns frequency analysis and suggested Picoschema
YAML that can be saved as a schema note.
Frequency thresholds:
- 95%+ present -> required field
- threshold+ present -> optional field
- Below threshold -> excluded (but noted)
Project Resolution:
Server resolves projects in this order: Single Project Mode -> project parameter -> default.
If project unknown, use list_memory_projects() first.
Args:
entity_type: The entity type to analyze (e.g., "Person", "meeting").
threshold: Minimum frequency (0-1) for a field to be suggested as optional.
Default 0.25 (25%). Fields above 95% become required.
project: Project name. Optional -- server will resolve.
context: Optional FastMCP context for performance caching.
Returns:
InferenceReport with frequency data and suggested schema, or error string
Examples:
# Infer schema for Person notes
schema_infer("Person")
# Use a higher threshold (50% minimum)
schema_infer("meeting", threshold=0.5)
# Infer in a specific project
schema_infer("Person", project="my-research")
"""
async with get_client() as client:
active_project = await get_active_project(client, project, context)
logger.info(
f"MCP tool call tool=schema_infer project={active_project.name} "
f"entity_type={entity_type} threshold={threshold}"
)
try:
from basic_memory.mcp.clients.schema import SchemaClient
schema_client = SchemaClient(client, active_project.external_id)
result = await schema_client.infer(entity_type, threshold=threshold)
logger.info(
f"MCP tool response: tool=schema_infer project={active_project.name} "
f"entity_type={entity_type} notes_analyzed={result.notes_analyzed} "
f"required={len(result.suggested_required)} "
f"optional={len(result.suggested_optional)}"
)
return result
except Exception as e:
logger.error(f"Schema inference failed: {e}, project: {active_project.name}")
return (
f"# Schema Inference Failed\n\n"
f"Error inferring schema for '{entity_type}': {e}\n\n"
f"## Troubleshooting\n"
f"1. Ensure notes of type '{entity_type}' exist in the project\n"
f'2. Try searching: `search_notes("{entity_type}", types=["{entity_type}"])`\n'
f"3. Verify the project has been synced: `basic-memory status`\n"
)
@mcp.tool(
description="Detect drift between a schema definition and actual note usage.",
)
async def schema_diff(
entity_type: str,
project: Optional[str] = None,
context: Context | None = None,
) -> DriftReport | str:
"""Detect drift between a schema definition and actual note usage.
Compares the existing schema for an entity type against how notes of
that type are actually structured. Identifies new fields that have
appeared, declared fields that are rarely used, and cardinality changes
(single-value vs array).
Useful for evolving schemas as your knowledge base grows -- run
periodically to see if your schema still matches reality.
Project Resolution:
Server resolves projects in this order: Single Project Mode -> project parameter -> default.
If project unknown, use list_memory_projects() first.
Args:
entity_type: The entity type to check for drift (e.g., "Person").
project: Project name. Optional -- server will resolve.
context: Optional FastMCP context for performance caching.
Returns:
DriftReport with new fields, dropped fields, and cardinality changes,
or error guidance string
Examples:
# Check drift for Person schema
schema_diff("Person")
# Check drift in a specific project
schema_diff("Person", project="my-research")
"""
async with get_client() as client:
active_project = await get_active_project(client, project, context)
logger.info(
f"MCP tool call tool=schema_diff project={active_project.name} "
f"entity_type={entity_type}"
)
try:
from basic_memory.mcp.clients.schema import SchemaClient
schema_client = SchemaClient(client, active_project.external_id)
result = await schema_client.diff(entity_type)
logger.info(
f"MCP tool response: tool=schema_diff project={active_project.name} "
f"entity_type={entity_type} "
f"new_fields={len(result.new_fields)} "
f"dropped_fields={len(result.dropped_fields)} "
f"cardinality_changes={len(result.cardinality_changes)}"
)
return result
except Exception as e:
logger.error(f"Schema diff failed: {e}, project: {active_project.name}")
return (
f"# Schema Diff Failed\n\n"
f"Error detecting drift for '{entity_type}': {e}\n\n"
f"## Troubleshooting\n"
f"1. Ensure a schema note exists for entity type '{entity_type}'\n"
f"2. Ensure notes of type '{entity_type}' exist in the project\n"
f"3. Verify the project has been synced: `basic-memory status`\n"
)
+58
View File
@@ -0,0 +1,58 @@
"""Schema system for Basic Memory.
Provides Picoschema-based validation for notes using observation/relation mapping.
Schemas are just notes with type: schema — no new data model, no migration.
"""
from basic_memory.schema.parser import (
SchemaField,
SchemaDefinition,
parse_picoschema,
parse_schema_note,
)
from basic_memory.schema.resolver import resolve_schema
from basic_memory.schema.validator import (
FieldResult,
ValidationResult,
validate_note,
)
from basic_memory.schema.inference import (
FieldFrequency,
InferenceResult,
ObservationData,
RelationData,
NoteData,
infer_schema,
analyze_observations,
analyze_relations,
)
from basic_memory.schema.diff import (
SchemaDrift,
diff_schema,
)
__all__ = [
# Parser
"SchemaField",
"SchemaDefinition",
"parse_picoschema",
"parse_schema_note",
# Resolver
"resolve_schema",
# Validator
"FieldResult",
"ValidationResult",
"validate_note",
# Inference
"FieldFrequency",
"InferenceResult",
"ObservationData",
"RelationData",
"NoteData",
"infer_schema",
"analyze_observations",
"analyze_relations",
# Diff
"SchemaDrift",
"diff_schema",
]
+111
View File
@@ -0,0 +1,111 @@
"""Schema diff for Basic Memory.
Compares a schema definition against actual note usage to detect drift.
Drift happens naturally as notes evolve -- new observation categories appear,
old ones fall out of use, single-value fields become multi-value.
The diff engine reuses inference analysis internally, comparing inferred
frequencies against the declared schema fields to surface:
- New fields: common in notes but not declared in schema
- Dropped fields: declared in schema but rare in actual notes
- Cardinality changes: field changed from single to array or vice versa
"""
from dataclasses import dataclass, field
from basic_memory.schema.inference import (
FieldFrequency,
NoteData,
analyze_observations,
analyze_relations,
)
from basic_memory.schema.parser import SchemaDefinition
@dataclass
class SchemaDrift:
"""Result of comparing a schema against actual note usage."""
new_fields: list[FieldFrequency] = field(default_factory=list)
dropped_fields: list[FieldFrequency] = field(default_factory=list)
cardinality_changes: list[str] = field(default_factory=list)
def diff_schema(
schema: SchemaDefinition,
notes: list[NoteData],
new_field_threshold: float = 0.25,
dropped_field_threshold: float = 0.10,
) -> SchemaDrift:
"""Compare a schema against actual note usage to detect drift.
Args:
schema: The current schema definition.
notes: List of NoteData objects representing actual notes.
new_field_threshold: Frequency above which an undeclared field is considered
"new" and worth adding to the schema.
dropped_field_threshold: Frequency below which a declared field is considered
"dropped" and worth removing from the schema.
Returns:
A SchemaDrift describing the differences between schema and reality.
"""
total = len(notes)
if total == 0:
return SchemaDrift()
# --- Analyze actual usage ---
obs_frequencies = analyze_observations(notes, total, max_sample_values=3)
rel_frequencies = analyze_relations(notes, total, max_sample_values=3)
# Build lookup from schema fields
schema_field_names = {f.name for f in schema.fields}
# Build lookup from actual frequencies
obs_freq_by_name = {f.name: f for f in obs_frequencies}
rel_freq_by_name = {f.name: f for f in rel_frequencies}
all_freq_by_name = {**obs_freq_by_name, **rel_freq_by_name}
result = SchemaDrift()
# --- Detect new fields ---
# Fields that appear frequently in notes but aren't declared in the schema
for freq in obs_frequencies + rel_frequencies:
if freq.name not in schema_field_names and freq.percentage >= new_field_threshold:
result.new_fields.append(freq)
# --- Detect dropped fields ---
# Fields declared in the schema but rarely appearing in actual notes
for schema_field in schema.fields:
freq = all_freq_by_name.get(schema_field.name)
if freq is None:
# Field doesn't appear at all in any note
result.dropped_fields.append(
FieldFrequency(
name=schema_field.name,
source="relation" if schema_field.is_entity_ref else "observation",
count=0,
total=total,
percentage=0.0,
)
)
elif freq.percentage < dropped_field_threshold:
result.dropped_fields.append(freq)
# --- Detect cardinality changes ---
# Fields where the schema says single but usage shows array, or vice versa
for schema_field in schema.fields:
freq = all_freq_by_name.get(schema_field.name)
if freq is None:
continue
if schema_field.is_array and not freq.is_array:
result.cardinality_changes.append(
f"{schema_field.name}: schema declares array but usage is typically single-value"
)
elif not schema_field.is_array and freq.is_array:
result.cardinality_changes.append(
f"{schema_field.name}: schema declares single-value but usage is typically array"
)
return result
+323
View File
@@ -0,0 +1,323 @@
"""Schema inference engine for Basic Memory.
Analyzes notes of a given type and suggests a schema based on observation
and relation frequency. Instead of requiring users to define schemas upfront,
schemas emerge from actual usage patterns:
Write notes freely -> Patterns emerge -> Crystallize into schema
Frequency thresholds:
- 95%+ present -> required field
- 25%+ present -> optional field
- Below 25% -> excluded from suggestion (but noted)
"""
from collections import Counter
from dataclasses import dataclass, field
# --- Result Data Model ---
@dataclass
class FieldFrequency:
"""Frequency analysis for a single field across notes of a type."""
name: str
source: str # "observation" | "relation"
count: int # notes containing this field
total: int # total notes analyzed
percentage: float
sample_values: list[str] = field(default_factory=list)
is_array: bool = False # True if typically appears multiple times per note
target_type: str | None = None # For relations, the most common target entity type
@dataclass
class InferenceResult:
"""Complete inference result with frequency analysis and suggested schema."""
entity_type: str
notes_analyzed: int
field_frequencies: list[FieldFrequency]
suggested_schema: dict # Ready-to-use Picoschema YAML dict
suggested_required: list[str]
suggested_optional: list[str]
excluded: list[str] # Below threshold
# --- Note Data Abstraction ---
# Instead of depending on the ORM Entity model, we accept simple data structures.
# This keeps the inference engine decoupled from the data access layer.
@dataclass
class ObservationData:
"""Lightweight observation for schema analysis. Decoupled from ORM."""
category: str
content: str
@dataclass
class RelationData:
"""Lightweight relation for schema analysis. Decoupled from ORM."""
relation_type: str
target_name: str
target_entity_type: str | None = None
@dataclass
class NoteData:
"""Minimal note representation for inference analysis.
Decoupled from ORM models so the inference engine can work with
any data source (database, files, API responses).
"""
identifier: str
observations: list[ObservationData]
relations: list[RelationData]
# --- Inference Logic ---
def infer_schema(
entity_type: str,
notes: list[NoteData],
required_threshold: float = 0.95,
optional_threshold: float = 0.25,
max_sample_values: int = 5,
) -> InferenceResult:
"""Analyze notes and suggest a Picoschema definition.
Examines observation categories and relation types across all provided notes.
Fields that appear in a high percentage of notes become required; those that
appear less frequently become optional.
Args:
entity_type: The entity type being analyzed (e.g., "Person").
notes: List of NoteData objects to analyze.
required_threshold: Frequency at or above which a field is required (default 0.95).
optional_threshold: Frequency at or above which a field is optional (default 0.25).
max_sample_values: Maximum number of sample values to include per field.
Returns:
An InferenceResult with frequency analysis and suggested Picoschema dict.
"""
total = len(notes)
if total == 0:
return InferenceResult(
entity_type=entity_type,
notes_analyzed=0,
field_frequencies=[],
suggested_schema={},
suggested_required=[],
suggested_optional=[],
excluded=[],
)
# --- Analyze observation frequencies ---
obs_frequencies = analyze_observations(notes, total, max_sample_values)
# --- Analyze relation frequencies ---
rel_frequencies = analyze_relations(notes, total, max_sample_values)
# --- Classify fields by threshold ---
all_frequencies = obs_frequencies + rel_frequencies
suggested_required: list[str] = []
suggested_optional: list[str] = []
excluded: list[str] = []
for freq in all_frequencies:
if freq.percentage >= required_threshold:
suggested_required.append(freq.name)
elif freq.percentage >= optional_threshold:
suggested_optional.append(freq.name)
else:
excluded.append(freq.name)
# --- Build suggested Picoschema dict ---
suggested_schema = _build_picoschema_dict(
all_frequencies, required_threshold, optional_threshold
)
return InferenceResult(
entity_type=entity_type,
notes_analyzed=total,
field_frequencies=all_frequencies,
suggested_schema=suggested_schema,
suggested_required=suggested_required,
suggested_optional=suggested_optional,
excluded=excluded,
)
# --- Observation Analysis ---
def analyze_observations(
notes: list[NoteData],
total: int,
max_sample_values: int,
) -> list[FieldFrequency]:
"""Count observation category frequencies across notes.
A category is counted once per note (presence), not per occurrence.
Array detection: if a category appears multiple times in a single note
in more than half the notes where it appears, it's flagged as an array.
"""
# Count how many notes contain each category (presence per note)
category_note_count: Counter[str] = Counter()
# Count how many notes have multiple occurrences (for array detection)
category_multi_count: Counter[str] = Counter()
# Collect sample values
category_samples: dict[str, list[str]] = {}
for note in notes:
# Group observations by category within this note
note_categories: dict[str, list[str]] = {}
for obs in note.observations:
note_categories.setdefault(obs.category, []).append(obs.content)
for category, values in note_categories.items():
category_note_count[category] += 1
if len(values) > 1:
category_multi_count[category] += 1
# Collect sample values (deduplicated)
samples = category_samples.setdefault(category, [])
for v in values:
if v not in samples and len(samples) < max_sample_values:
samples.append(v)
# Build FieldFrequency objects
frequencies: list[FieldFrequency] = []
for category, count in category_note_count.most_common():
# Array detection: if more than half of notes with this category have
# multiple occurrences, treat it as an array field
multi_count = category_multi_count.get(category, 0)
is_array = multi_count > (count / 2)
frequencies.append(
FieldFrequency(
name=category,
source="observation",
count=count,
total=total,
percentage=count / total,
sample_values=category_samples.get(category, []),
is_array=is_array,
)
)
return frequencies
# --- Relation Analysis ---
def analyze_relations(
notes: list[NoteData],
total: int,
max_sample_values: int,
) -> list[FieldFrequency]:
"""Count relation type frequencies across notes.
Similar to observations, a relation type is counted once per note.
Array detection follows the same logic.
"""
rel_note_count: Counter[str] = Counter()
rel_multi_count: Counter[str] = Counter()
rel_samples: dict[str, list[str]] = {}
# Track target entity types to suggest the type in the schema
rel_target_types: dict[str, Counter[str]] = {}
for note in notes:
note_rels: dict[str, list[str]] = {}
note_rel_objects: dict[str, list[RelationData]] = {}
for rel in note.relations:
note_rels.setdefault(rel.relation_type, []).append(rel.target_name)
note_rel_objects.setdefault(rel.relation_type, []).append(rel)
for rel_type, targets in note_rels.items():
rel_note_count[rel_type] += 1
if len(targets) > 1:
rel_multi_count[rel_type] += 1
samples = rel_samples.setdefault(rel_type, [])
for t in targets:
if t not in samples and len(samples) < max_sample_values:
samples.append(t)
# Track target entity types from individual relations (not the source note)
target_counter = rel_target_types.setdefault(rel_type, Counter())
for rel in note_rel_objects[rel_type]:
if rel.target_entity_type:
target_counter[rel.target_entity_type] += 1
frequencies: list[FieldFrequency] = []
for rel_type, count in rel_note_count.most_common():
multi_count = rel_multi_count.get(rel_type, 0)
is_array = multi_count > (count / 2)
# Determine most common target type
target_counter = rel_target_types.get(rel_type, Counter())
most_common_target = target_counter.most_common(1)[0][0] if target_counter else None
frequencies.append(
FieldFrequency(
name=rel_type,
source="relation",
count=count,
total=total,
percentage=count / total,
sample_values=rel_samples.get(rel_type, []),
is_array=is_array,
target_type=most_common_target,
)
)
return frequencies
# --- Schema Generation ---
def _build_picoschema_dict(
frequencies: list[FieldFrequency],
required_threshold: float,
optional_threshold: float,
) -> dict:
"""Build a Picoschema YAML dict from field frequencies.
Only includes fields at or above the optional threshold.
"""
schema: dict = {}
for freq in frequencies:
if freq.percentage < optional_threshold:
continue
is_required = freq.percentage >= required_threshold
# --- Build the field key ---
key = freq.name
if not is_required:
key += "?"
if freq.is_array:
key += "(array)"
# --- Build the field value ---
if freq.source == "relation":
# Relations become entity reference fields
target = freq.target_type or "string"
# Capitalize first letter for entity ref convention
target = target[0].upper() + target[1:] if target != "string" else "string"
schema[key] = target
else:
schema[key] = "string"
return schema
+236
View File
@@ -0,0 +1,236 @@
"""Picoschema parser for Basic Memory.
Parses Picoschema YAML dicts (from note frontmatter) into typed dataclass
representations. Picoschema is a compact schema notation from Google's Dotprompt
that fits naturally in YAML frontmatter.
Syntax reference:
field: type, description # required field
field?: type, description # optional field
field(array): type # array of values
field?(enum): [val1, val2] # enumeration
field?(object): # nested object
sub_field: type
EntityName as type (capitalized) # entity reference
"""
from dataclasses import dataclass, field
# --- Data Model ---
@dataclass
class SchemaField:
"""A single field in a Picoschema definition.
Maps to either an observation category or a relation type in Basic Memory notes.
"""
name: str
type: str # string, integer, number, boolean, any, or EntityName
required: bool # True unless field name ends with ?
is_array: bool = False # True if (array) notation
is_enum: bool = False # True if (enum) notation
enum_values: list[str] = field(default_factory=list)
description: str | None = None # Text after comma
is_entity_ref: bool = False # True if type is capitalized (entity reference)
children: list["SchemaField"] = field(default_factory=list) # For (object) types
@dataclass
class SchemaDefinition:
"""A complete schema definition parsed from a schema note's frontmatter.
Combines the parsed fields with metadata about the schema itself.
"""
entity: str # The entity type this schema describes
version: int # Schema version
fields: list[SchemaField] # Parsed fields
validation_mode: str # "warn" | "strict" | "off"
# --- Built-in scalar types ---
# Types that are NOT entity references. Anything not in this set and starting
# with an uppercase letter is treated as an entity reference.
SCALAR_TYPES = frozenset({"string", "integer", "number", "boolean", "any"})
# --- Field Name Parsing ---
def _parse_field_key(key: str) -> tuple[str, bool, bool, bool, bool]:
"""Parse a Picoschema field key into its components.
Returns (name, required, is_array, is_enum, is_object).
The key format is: name[?][(array|enum|object)]
Examples:
"name" -> ("name", True, False, False)
"role?" -> ("role", False, False, False)
"tags?(array)" -> ("tags", False, True, False)
"status?(enum)" -> ("status", False, False, True)
"metadata?(object)" -> ("metadata", False, False, False) + children
"""
required = True
is_array = False
is_enum = False
is_object = False
# Check for modifier suffix: (array), (enum), (object)
if key.endswith("(array)"):
is_array = True
key = key[: -len("(array)")]
elif key.endswith("(enum)"):
is_enum = True
key = key[: -len("(enum)")]
elif key.endswith("(object)"):
is_object = True
key = key[: -len("(object)")]
# Check for optional marker
if key.endswith("?"):
required = False
key = key[:-1]
return key, required, is_array, is_enum, is_object
def _parse_type_and_description(value: str) -> tuple[str, str | None]:
"""Parse a type string that may include a comma-separated description.
Examples:
"string" -> ("string", None)
"string, full name" -> ("string", "full name")
"Organization, employer" -> ("Organization", "employer")
"""
if "," in value:
type_str, desc = value.split(",", 1)
return type_str.strip(), desc.strip()
return value.strip(), None
def _is_entity_ref_type(type_str: str) -> bool:
"""Determine if a type string represents an entity reference.
Entity references are capitalized type names that are not built-in scalar types.
"""
if type_str in SCALAR_TYPES:
return False
# Capitalized first letter = entity reference
return len(type_str) > 0 and type_str[0].isupper()
# --- Main Parser ---
def parse_picoschema(yaml_dict: dict) -> list[SchemaField]:
"""Parse a Picoschema YAML dict into a list of SchemaField objects.
This is the core parser that converts YAML frontmatter schema definitions
into structured SchemaField dataclasses.
Args:
yaml_dict: The schema dict from YAML frontmatter. Keys are field
declarations (e.g., "name", "role?", "tags?(array)"), values are
type declarations (e.g., "string", "string, description").
Returns:
List of SchemaField objects representing the schema.
"""
fields: list[SchemaField] = []
for key, value in yaml_dict.items():
name, required, is_array, is_enum, is_object = _parse_field_key(key)
# --- Enum fields ---
# Trigger: value is a list (e.g., [active, inactive])
# Why: enums declare allowed values directly as a YAML list
# Outcome: SchemaField with is_enum=True and enum_values populated
if is_enum:
enum_values = value if isinstance(value, list) else [str(value)]
fields.append(
SchemaField(
name=name,
type="enum",
required=required,
is_enum=True,
enum_values=[str(v) for v in enum_values],
)
)
continue
# --- Object fields ---
# Trigger: value is a dict (nested sub-fields)
# Why: objects contain child fields parsed recursively
# Outcome: SchemaField with children populated via recursive parse
if is_object or (isinstance(value, dict) and not is_enum):
children = parse_picoschema(value) if isinstance(value, dict) else []
fields.append(
SchemaField(
name=name,
type="object",
required=required,
children=children,
)
)
continue
# --- Scalar and entity ref fields ---
type_str, description = _parse_type_and_description(str(value))
is_entity_ref = _is_entity_ref_type(type_str)
fields.append(
SchemaField(
name=name,
type=type_str,
required=required,
is_array=is_array,
description=description,
is_entity_ref=is_entity_ref,
)
)
return fields
def parse_schema_note(frontmatter: dict) -> SchemaDefinition:
"""Parse a full schema note's frontmatter into a SchemaDefinition.
A schema note has type: schema and contains:
- entity: the entity type this schema describes
- version: schema version number
- schema: the Picoschema dict
- settings.validation: validation mode (warn/strict/off)
Args:
frontmatter: The complete YAML frontmatter dict from a schema note.
Returns:
A SchemaDefinition with parsed fields and metadata.
Raises:
ValueError: If required fields (entity, schema) are missing.
"""
entity = frontmatter.get("entity")
if not entity:
raise ValueError("Schema note missing required 'entity' field in frontmatter")
schema_dict = frontmatter.get("schema")
if not schema_dict or not isinstance(schema_dict, dict):
raise ValueError("Schema note missing required 'schema' dict in frontmatter")
version = frontmatter.get("version", 1)
settings = frontmatter.get("settings", {})
validation_mode = settings.get("validation", "warn") if isinstance(settings, dict) else "warn"
fields = parse_picoschema(schema_dict)
return SchemaDefinition(
entity=entity,
version=version,
fields=fields,
validation_mode=validation_mode,
)
+118
View File
@@ -0,0 +1,118 @@
"""Schema resolver for Basic Memory.
Finds the applicable schema for a note using a priority-based resolution order:
1. Inline schema -> frontmatter['schema'] is a dict
2. Explicit ref -> frontmatter['schema'] is a string (entity name or permalink)
3. Implicit by type -> frontmatter['type'] matches a schema note's entity field
4. No schema -> returns None (perfectly fine)
The resolver takes a search function as a dependency instead of importing
repository code directly, keeping the schema package decoupled from the
data access layer.
"""
from collections.abc import Callable, Awaitable
from basic_memory.schema.parser import SchemaDefinition, parse_picoschema, parse_schema_note
# Type alias for the search function dependency.
# Given a query string, returns a list of frontmatter dicts from matching schema notes.
type SchemaSearchFn = Callable[[str], Awaitable[list[dict]]]
async def resolve_schema(
note_frontmatter: dict,
search_fn: SchemaSearchFn,
) -> SchemaDefinition | None:
"""Resolve the schema for a note based on its frontmatter.
Resolution order:
1. Inline schema (frontmatter['schema'] is a dict) - parsed directly
2. Explicit reference (frontmatter['schema'] is a string) - looked up via search_fn
3. Implicit by type (frontmatter['type']) - searches for schema note with matching entity
4. No schema - returns None
Args:
note_frontmatter: The YAML frontmatter dict from the note being validated.
search_fn: An async callable that takes a query string and returns a list
of frontmatter dicts from matching schema notes. This keeps the resolver
decoupled from the repository/service layer.
Returns:
A SchemaDefinition if a schema is found, None otherwise.
"""
schema_value = note_frontmatter.get("schema")
# --- 1. Inline schema ---
# Trigger: schema field is a dict (the Picoschema definition lives in this note)
# Why: inline schemas are self-contained, no lookup needed
# Outcome: parse and return immediately
if isinstance(schema_value, dict):
return _schema_from_inline(schema_value, note_frontmatter)
# --- 2. Explicit reference ---
# Trigger: schema field is a string (entity name or permalink)
# Why: the note points to a specific schema note by name
# Outcome: search for the referenced schema note and parse it
if isinstance(schema_value, str):
result = await _schema_from_reference(schema_value, search_fn)
if result is not None:
return result
# --- 3. Implicit by type ---
# Trigger: no schema field, but the note has a type field
# Why: convention — a note with type: Person looks for a schema with entity: Person
# Outcome: search for a schema note whose entity matches the note's type
note_type = note_frontmatter.get("type")
if note_type:
result = await _schema_from_type(note_type, search_fn)
if result is not None:
return result
# --- 4. No schema ---
return None
def _schema_from_inline(schema_dict: dict, frontmatter: dict) -> SchemaDefinition:
"""Build a SchemaDefinition from an inline schema dict.
For inline schemas, we derive metadata from the note's own frontmatter
since there's no separate schema note.
"""
fields = parse_picoschema(schema_dict)
entity = frontmatter.get("type", "unknown")
settings = frontmatter.get("settings", {})
validation_mode = settings.get("validation", "warn") if isinstance(settings, dict) else "warn"
return SchemaDefinition(
entity=entity,
version=1,
fields=fields,
validation_mode=validation_mode,
)
async def _schema_from_reference(
ref: str,
search_fn: SchemaSearchFn,
) -> SchemaDefinition | None:
"""Look up a schema by entity name or permalink reference.
The search function is expected to find schema notes matching the reference.
"""
results = await search_fn(ref)
if not results:
return None
return parse_schema_note(results[0])
async def _schema_from_type(
note_type: str,
search_fn: SchemaSearchFn,
) -> SchemaDefinition | None:
"""Look up a schema implicitly by matching the note's type to a schema's entity field."""
results = await search_fn(note_type)
if not results:
return None
return parse_schema_note(results[0])
+259
View File
@@ -0,0 +1,259 @@
"""Schema validator for Basic Memory.
Validates a note's observations and relations against a resolved schema definition.
The mapping rules ground schema fields in the existing Basic Memory note format:
Schema Declaration -> Grounded In
-----------------------------------------------
field: string -> observation [field] value
field?(array): string -> multiple [field] observations
field?: EntityType -> relation 'field [[Target]]'
field?(array): EntityType -> multiple 'field' relations
field?(enum): [values] -> observation [field] value where value is in set
Validation is soft by default (warn mode). Unmatched observations and relations
are informational, not errors -- schemas are a subset, not a straitjacket.
"""
from dataclasses import dataclass, field as dataclass_field
from basic_memory.schema.inference import ObservationData, RelationData
from basic_memory.schema.parser import SchemaDefinition, SchemaField
# --- Result Data Model ---
@dataclass
class FieldResult:
"""Validation result for a single schema field."""
field: SchemaField
status: str # "present" | "missing" | "enum_mismatch"
values: list[str] = dataclass_field(default_factory=list) # Matched values
message: str | None = None
@dataclass
class ValidationResult:
"""Complete validation result for a note against a schema."""
note_identifier: str
schema_entity: str
passed: bool # True if no errors (warnings are OK)
field_results: list[FieldResult] = dataclass_field(default_factory=list)
unmatched_observations: dict[str, int] = dataclass_field(default_factory=dict) # cat -> count
unmatched_relations: list[str] = dataclass_field(default_factory=list) # types not in schema
warnings: list[str] = dataclass_field(default_factory=list)
errors: list[str] = dataclass_field(default_factory=list)
# --- Validation Logic ---
def validate_note(
note_identifier: str,
schema: SchemaDefinition,
observations: list[ObservationData],
relations: list[RelationData],
) -> ValidationResult:
"""Validate a note against a schema definition.
Args:
note_identifier: The note's title, permalink, or file path for reporting.
schema: The resolved SchemaDefinition to validate against.
observations: List of ObservationData from the note's observations.
relations: List of RelationData from the note's relations.
Returns:
A ValidationResult with per-field results, unmatched items, and warnings/errors.
"""
result = ValidationResult(
note_identifier=note_identifier,
schema_entity=schema.entity,
passed=True,
)
# Build lookup structures from the note's actual content
obs_by_category = _group_observations(observations)
rel_by_type = _group_relations(relations)
# Track which observation categories and relation types are matched by schema fields
matched_categories: set[str] = set()
matched_relation_types: set[str] = set()
# --- Validate each schema field ---
for schema_field in schema.fields:
field_result = _validate_field(schema_field, obs_by_category, rel_by_type)
result.field_results.append(field_result)
# Track which categories/relation types this field consumed
if schema_field.is_entity_ref:
matched_relation_types.add(schema_field.name)
else:
matched_categories.add(schema_field.name)
# --- Generate warnings or errors based on validation mode ---
# Trigger: field declared in schema but not found in note
# Why: required missing = warning (or error in strict); optional missing = silent
# Outcome: only required missing fields produce diagnostics
if field_result.status == "missing" and schema_field.required:
msg = _missing_field_message(schema_field)
if schema.validation_mode == "strict":
result.errors.append(msg)
result.passed = False
else:
result.warnings.append(msg)
elif field_result.status == "enum_mismatch":
msg = field_result.message or f"Field '{schema_field.name}' has invalid enum value"
if schema.validation_mode == "strict":
result.errors.append(msg)
result.passed = False
else:
result.warnings.append(msg)
# --- Collect unmatched observations ---
for category, values in obs_by_category.items():
if category not in matched_categories:
result.unmatched_observations[category] = len(values)
# --- Collect unmatched relations ---
for rel_type in rel_by_type:
if rel_type not in matched_relation_types:
result.unmatched_relations.append(rel_type)
return result
# --- Field Validation ---
def _validate_field(
schema_field: SchemaField,
obs_by_category: dict[str, list[str]],
rel_by_type: dict[str, list[str]],
) -> FieldResult:
"""Validate a single schema field against the note's data.
Entity ref fields map to relations; all other fields map to observations.
"""
# --- Entity reference fields map to relations ---
if schema_field.is_entity_ref:
return _validate_entity_ref_field(schema_field, rel_by_type)
# --- Enum fields require value membership check ---
if schema_field.is_enum:
return _validate_enum_field(schema_field, obs_by_category)
# --- Scalar and array fields map to observations ---
return _validate_observation_field(schema_field, obs_by_category)
def _validate_observation_field(
schema_field: SchemaField,
obs_by_category: dict[str, list[str]],
) -> FieldResult:
"""Validate a field that maps to observation categories."""
values = obs_by_category.get(schema_field.name, [])
if not values:
return FieldResult(
field=schema_field,
status="missing",
message=_missing_field_message(schema_field),
)
return FieldResult(
field=schema_field,
status="present",
values=values,
)
def _validate_entity_ref_field(
schema_field: SchemaField,
rel_by_type: dict[str, list[str]],
) -> FieldResult:
"""Validate a field that maps to relations (entity references)."""
targets = rel_by_type.get(schema_field.name, [])
if not targets:
return FieldResult(
field=schema_field,
status="missing",
message=f"Missing relation: {schema_field.name} (no '{schema_field.name} [[...]]' "
f"relation found)",
)
return FieldResult(
field=schema_field,
status="present",
values=targets,
)
def _validate_enum_field(
schema_field: SchemaField,
obs_by_category: dict[str, list[str]],
) -> FieldResult:
"""Validate an enum field -- value must be in the allowed set."""
values = obs_by_category.get(schema_field.name, [])
if not values:
return FieldResult(
field=schema_field,
status="missing",
message=_missing_field_message(schema_field),
)
# Check each value against the allowed enum values
invalid_values = [v for v in values if v not in schema_field.enum_values]
if invalid_values:
allowed = ", ".join(schema_field.enum_values)
invalid = ", ".join(invalid_values)
return FieldResult(
field=schema_field,
status="enum_mismatch",
values=values,
message=f"Field '{schema_field.name}' has invalid value(s): {invalid} "
f"(allowed: {allowed})",
)
return FieldResult(
field=schema_field,
status="present",
values=values,
)
# --- Helper Functions ---
def _group_observations(observations: list[ObservationData]) -> dict[str, list[str]]:
"""Group observations by category."""
result: dict[str, list[str]] = {}
for obs in observations:
result.setdefault(obs.category, []).append(obs.content)
return result
def _group_relations(relations: list[RelationData]) -> dict[str, list[str]]:
"""Group relations by relation type."""
result: dict[str, list[str]] = {}
for rel in relations:
result.setdefault(rel.relation_type, []).append(rel.target_name)
return result
def _missing_field_message(schema_field: SchemaField) -> str:
"""Generate a human-readable message for a missing field."""
kind = "required" if schema_field.required else "optional"
if schema_field.is_entity_ref:
return (
f"Missing {kind} field: {schema_field.name} "
f"(no '{schema_field.name} [[...]]' relation found)"
)
return f"Missing {kind} field: {schema_field.name} (expected [{schema_field.name}] observation)"
+121
View File
@@ -0,0 +1,121 @@
"""Pydantic response models for the schema system.
These models define the API response format for schema validation,
inference, and drift detection operations. They mirror the dataclass
structures in basic_memory.schema but are Pydantic models suitable
for API serialization.
"""
from pydantic import BaseModel, Field
# --- Validation Response Models ---
class FieldResultResponse(BaseModel):
"""Result of validating a single schema field against a note."""
field_name: str
field_type: str
required: bool
status: str = Field(description="One of: present, missing, type_mismatch")
values: list[str] = Field(default_factory=list, description="Matched values from the note")
message: str | None = None
class NoteValidationResponse(BaseModel):
"""Validation result for a single note against a schema."""
note_identifier: str
schema_entity: str
passed: bool = Field(description="True if no errors (warnings are OK)")
field_results: list[FieldResultResponse] = Field(default_factory=list)
unmatched_observations: dict[str, int] = Field(
default_factory=dict,
description="Observation categories not covered by schema, with counts",
)
unmatched_relations: list[str] = Field(
default_factory=list,
description="Relation types not covered by schema",
)
warnings: list[str] = Field(default_factory=list)
errors: list[str] = Field(default_factory=list)
class ValidationReport(BaseModel):
"""Full validation report for one or more notes."""
entity_type: str | None = None
total_notes: int = 0
valid_count: int = 0
warning_count: int = 0
error_count: int = 0
results: list[NoteValidationResponse] = Field(default_factory=list)
# --- Inference Response Models ---
class FieldFrequencyResponse(BaseModel):
"""Frequency analysis for a single field across notes."""
name: str
source: str = Field(description="One of: observation, relation")
count: int = Field(description="Number of notes containing this field")
total: int = Field(description="Total notes analyzed")
percentage: float
sample_values: list[str] = Field(default_factory=list)
is_array: bool = Field(
default=False,
description="True if field typically appears multiple times per note",
)
target_type: str | None = Field(
default=None,
description="For relations, the most common target entity type",
)
class InferenceReport(BaseModel):
"""Inference result with suggested schema definition."""
entity_type: str
notes_analyzed: int
field_frequencies: list[FieldFrequencyResponse] = Field(default_factory=list)
suggested_schema: dict = Field(
default_factory=dict,
description="Ready-to-use Picoschema YAML dict",
)
suggested_required: list[str] = Field(default_factory=list)
suggested_optional: list[str] = Field(default_factory=list)
excluded: list[str] = Field(
default_factory=list,
description="Fields below the inclusion threshold",
)
# --- Drift Response Models ---
class DriftFieldResponse(BaseModel):
"""A field involved in schema drift."""
name: str
source: str
count: int
total: int
percentage: float
class DriftReport(BaseModel):
"""Schema drift analysis comparing schema definition to actual usage."""
entity_type: str
new_fields: list[DriftFieldResponse] = Field(
default_factory=list,
description="Fields common in notes but not in schema",
)
dropped_fields: list[DriftFieldResponse] = Field(
default_factory=list,
description="Fields in schema but rare in notes",
)
cardinality_changes: list[str] = Field(default_factory=list)
@@ -0,0 +1,17 @@
---
title: Alice Chen
type: Person
tags: [engineering, drift-test]
---
# Alice Chen
## Observations
- [name] Alice Chen
- [github] alicechen
- [expertise] Backend systems
- [fact] Leads the platform team
## Relations
- works_at [[Acme Corp]]
- works_at [[Open Source Collective]]
@@ -0,0 +1,16 @@
---
title: Bob Martinez
type: Person
tags: [design, drift-test]
---
# Bob Martinez
## Observations
- [name] Bob Martinez
- [github] bobmartinez
- [fact] Design systems advocate
## Relations
- works_at [[DesignCo]]
- works_at [[Figma Community]]
@@ -0,0 +1,16 @@
---
title: Carol Wu
type: Person
tags: [data-science, drift-test]
---
# Carol Wu
## Observations
- [name] Carol Wu
- [github] carolwu
- [expertise] Machine learning
## Relations
- works_at [[DataLab]]
- works_at [[University Research Group]]
@@ -0,0 +1,15 @@
---
title: David Kim
type: Person
tags: [security, drift-test]
---
# David Kim
## Observations
- [name] David Kim
- [github] davidkim
- [fact] Security researcher
## Relations
- works_at [[SecureTech]]
@@ -0,0 +1,16 @@
---
title: Elena Petrova
type: Person
tags: [devops, drift-test]
---
# Elena Petrova
## Observations
- [name] Elena Petrova
- [github] elenap
- [expertise] Infrastructure
## Relations
- works_at [[CloudOps]]
- works_at [[CNCF]]
@@ -0,0 +1,16 @@
---
title: Frank Nguyen
type: Person
tags: [mobile, drift-test]
---
# Frank Nguyen
## Observations
- [name] Frank Nguyen
- [github] frankn
- [fact] iOS developer
## Relations
- works_at [[AppWorks]]
- works_at [[Swift Community]]
@@ -0,0 +1,17 @@
---
title: Grace Okafor
type: Person
tags: [frontend, drift-test]
---
# Grace Okafor
## Observations
- [name] Grace Okafor
- [github] graceo
- [expertise] React and TypeScript
- [fact] Open source contributor
## Relations
- works_at [[WebDev Inc]]
- works_at [[React Core Team]]
@@ -0,0 +1,17 @@
---
title: Hassan Ali
type: Person
tags: [backend, drift-test]
---
# Hassan Ali
## Observations
- [name] Hassan Ali
- [github] hassanali
- [fact] Distributed systems engineer
- [email] hassan@example.com
## Relations
- works_at [[ScaleCo]]
- works_at [[Apache Foundation]]
@@ -0,0 +1,16 @@
---
title: Iris Zhang
type: Person
tags: [research, drift-test]
---
# Iris Zhang
## Observations
- [name] Iris Zhang
- [github] irisz
- [expertise] NLP research
- [fact] Published 30+ papers
## Relations
- works_at [[AI Research Lab]]
@@ -0,0 +1,16 @@
---
title: Jake Thompson
type: Person
tags: [gaming, drift-test]
---
# Jake Thompson
## Observations
- [name] Jake Thompson
- [github] jaket
- [fact] Game engine developer
## Relations
- works_at [[GameStudios]]
- works_at [[Unreal Community]]
@@ -0,0 +1,15 @@
---
title: Karen Liu
type: Person
tags: [database, drift-test]
---
# Karen Liu
## Observations
- [name] Karen Liu
- [github] karenliu
- [expertise] Database internals
## Relations
- works_at [[DataStore]]
@@ -0,0 +1,17 @@
---
title: Luis Rivera
type: Person
tags: [embedded, drift-test]
---
# Luis Rivera
## Observations
- [name] Luis Rivera
- [github] luisr
- [fact] Embedded systems specialist
- [expertise] Real-time operating systems
## Relations
- works_at [[IoT Solutions]]
- works_at [[RISC-V Foundation]]
@@ -0,0 +1,15 @@
---
title: Maria Santos
type: Person
tags: [product, drift-test]
---
# Maria Santos
## Observations
- [name] Maria Santos
- [fact] Product manager
- [role] Senior Product Manager
## Relations
- works_at [[TechCorp]]
@@ -0,0 +1,16 @@
---
title: Nate Johnson
type: Person
tags: [sre, drift-test]
---
# Nate Johnson
## Observations
- [name] Nate Johnson
- [fact] Site reliability engineer
- [expertise] Kubernetes
## Relations
- works_at [[CloudPlatform]]
- works_at [[SRE Community]]
@@ -0,0 +1,15 @@
---
title: Olivia Park
type: Person
tags: [compiler, drift-test]
---
# Olivia Park
## Observations
- [name] Olivia Park
- [fact] Compiler engineer
- [expertise] LLVM
## Relations
- works_at [[CompilerCo]]
@@ -0,0 +1,15 @@
---
title: Paul Schmidt
type: Person
tags: [networking, drift-test]
---
# Paul Schmidt
## Observations
- [name] Paul Schmidt
- [fact] Network protocol designer
## Relations
- works_at [[NetDev]]
- works_at [[IETF]]
@@ -0,0 +1,14 @@
---
title: Quinn Taylor
type: Person
tags: [testing, drift-test]
---
# Quinn Taylor
## Observations
- [name] Quinn Taylor
- [fact] Testing infrastructure lead
## Relations
- works_at [[QualityCo]]
@@ -0,0 +1,16 @@
---
title: Rachel Lee
type: Person
tags: [crypto, drift-test]
---
# Rachel Lee
## Observations
- [name] Rachel Lee
- [fact] Cryptography researcher
- [expertise] Post-quantum cryptography
## Relations
- works_at [[CryptoLab]]
- works_at [[NIST Advisory Board]]
@@ -0,0 +1,15 @@
---
title: Sam Foster
type: Person
tags: [accessibility, drift-test]
---
# Sam Foster
## Observations
- [name] Sam Foster
- [fact] Accessibility advocate
- [email] sam@a11y.org
## Relations
- works_at [[A11y Foundation]]
@@ -0,0 +1,16 @@
---
title: Tanya Ivanova
type: Person
tags: [quantum, drift-test]
---
# Tanya Ivanova
## Observations
- [name] Tanya Ivanova
- [fact] Quantum computing researcher
- [expertise] Quantum algorithms
## Relations
- works_at [[QuantumLab]]
- works_at [[University of Waterloo]]
@@ -0,0 +1,18 @@
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
email?: string, contact email
settings:
validation: warn
---
# Person
A v1 Person schema used for drift detection testing. Usage has diverged
from this definition over time.
@@ -0,0 +1,17 @@
---
title: Solo Standup
type: Meeting
tags: [edge-case, array-single]
---
# Solo Standup
An array field with only one observation. Should still pass validation.
## Observations
- [attendees] Alice
- [decisions] Continue current approach
- [action_items] Alice to finish implementation
## Relations
- part_of [[Q1 Sprint]]
@@ -0,0 +1,5 @@
---
title: Empty Note
type: Person
tags: [test]
---
@@ -0,0 +1,20 @@
---
title: Project Kickoff Notes
type: Person
schema: Meeting
tags: [edge-case, explicit-ref]
---
# Project Kickoff Notes
Has schema: Meeting AND type: Person. The explicit schema reference should win.
## Observations
- [attendees] Alice
- [attendees] Bob
- [decisions] Use microservices architecture
- [action_items] Alice to draft architecture doc
- [date] 2024-03-15
## Relations
- followed_by [[Sprint 1 Standup]]
@@ -0,0 +1,20 @@
---
title: Design Review 2024-03-01
type: Person
schema:
topic: string, review subject
outcome?: string, decision made
reviewer?(array): string, who reviewed
tags: [edge-case, inline-schema]
---
# Design Review 2024-03-01
Has BOTH an inline schema dict AND type: Person. The inline schema should win.
## Observations
- [topic] API versioning strategy
- [outcome] Adopt URL-based versioning
- [reviewer] Alice
- [reviewer] Bob
- [reviewer] Carol
@@ -0,0 +1,11 @@
# A Note Without Frontmatter
This markdown file has no YAML frontmatter at all. The schema system should
handle this gracefully and skip validation entirely.
## Observations
- [fact] Some observation here
- [note] Another observation
## Relations
- relates_to [[Some Other Note]]
@@ -0,0 +1,15 @@
---
title: Architecture Links
type: SoftwareProject
tags: [edge-case, relation-only]
---
# Architecture Links
A note with relations but no observations at all.
## Relations
- maintained_by [[Pedro Hernandez]]
- uses [[FastAPI]]
- uses [[SQLite]]
- depends_on [[Python]]
@@ -0,0 +1,17 @@
---
title: Some Random Entity
type: SomeRandomType
tags: [untyped, edge-case]
---
# Some Random Entity
This note has a type that does not match any schema definition. The schema
system should recognize there is no matching schema and skip validation.
## Observations
- [fact] This type has no corresponding schema note
- [status] Active
## Relations
- relates_to [[Another Entity]]
@@ -0,0 +1,19 @@
---
title: Yukihiro Matsumoto Profile
type: Person
tags: [edge-case, unicode]
---
# Yukihiro Matsumoto Profile
Observations with unicode content.
## Observations
- [name] まつもと ゆきひろ (Yukihiro Matsumoto)
- [role] プログラミング言語設計者
- [expertise] オブジェクト指向プログラミング
- [fact] Rubyの哲学: 「プログラマの幸福」
## Relations
- works_at [[Ruby Association]]
- created [[Ruby]]
@@ -0,0 +1,20 @@
---
title: Ada Lovelace
type: Person
tags: [mathematics, computing, history]
---
# Ada Lovelace
Mathematician and writer, often regarded as the first computer programmer.
## Observations
- [name] Ada Lovelace
- [role] Mathematician and writer
- [born] 1815
- [fact] Wrote the first computer program for Charles Babbage's Analytical Engine
- [fact] Was the daughter of poet Lord Byron
- [achievement] Recognized as the first person to see the potential of computers beyond calculation
## Relations
- collaborated_with [[Charles Babbage]]
@@ -0,0 +1,24 @@
---
title: Alan Turing
type: Person
tags: [mathematics, cryptography, computing, ai]
---
# Alan Turing
Father of theoretical computer science and artificial intelligence.
## Observations
- [name] Alan Turing
- [role] Mathematician, logician, and cryptanalyst
- [expertise] Mathematical logic
- [expertise] Cryptanalysis
- [expertise] Theoretical computer science
- [born] 1912
- [fact] Broke the Enigma code during World War II
- [fact] Proposed the Turing test for machine intelligence
## Relations
- works_at [[Bletchley Park]]
- works_at [[University of Manchester]]
- created [[Turing Machine]]
@@ -0,0 +1,25 @@
---
title: Anders Hejlsberg
type: Person
tags: [typescript, csharp, turbo-pascal, microsoft]
---
# Anders Hejlsberg
Creator of TypeScript, C#, and lead architect of Turbo Pascal and Delphi.
## Observations
- [name] Anders Hejlsberg
- [role] Technical Fellow and language designer
- [expertise] Programming language design
- [expertise] Compiler engineering
- [expertise] Type systems
- [email] anders@microsoft.com
- [fact] Created TypeScript to bring static typing to JavaScript
- [fact] Lead architect of C# and Turbo Pascal
## Relations
- works_at [[Microsoft]]
- created [[TypeScript]]
- created [[C Sharp]]
- created [[Turbo Pascal]]
@@ -0,0 +1,21 @@
---
title: Andrej Karpathy
type: Person
tags: [ai, deep-learning, tesla, openai]
---
# Andrej Karpathy
AI researcher and educator known for making deep learning accessible.
## Observations
- [name] Andrej Karpathy
- [role] AI researcher and educator
- [expertise] Deep learning
- [expertise] Computer vision
- [expertise] Natural language processing
- [fact] Former director of AI at Tesla for Autopilot
- [fact] Created popular deep learning courses and tutorials
## Relations
- works_at [[Eureka Labs]]
@@ -0,0 +1,22 @@
---
title: Barbara Liskov
type: Person
tags: [programming-languages, oop, distributed-systems]
---
# Barbara Liskov
Pioneer of programming language design and data abstraction.
## Observations
- [name] Barbara Liskov
- [role] Institute Professor at MIT
- [expertise] Programming language design
- [expertise] Distributed systems
- [expertise] Data abstraction
- [fact] Formulated the Liskov Substitution Principle
- [achievement] Won the Turing Award in 2008
## Relations
- works_at [[MIT]]
- created [[CLU]]
@@ -0,0 +1,20 @@
---
title: Bjarne Stroustrup
type: Person
tags: [cpp, programming-languages, bell-labs]
---
# Bjarne Stroustrup
Creator of C++ and advocate for type-safe systems programming.
## Observations
- [name] Bjarne Stroustrup
- [role] Computer scientist and professor
- [expertise] Programming language design
- [expertise] Systems programming
- [fact] Created C++ starting in 1979 as "C with Classes"
## Relations
- works_at [[Columbia University]]
- created [[C++]]
@@ -0,0 +1,22 @@
---
title: Brendan Eich
type: Person
tags: [javascript, web, browsers, mozilla]
---
# Brendan Eich
Creator of JavaScript and co-founder of the Mozilla project.
## Observations
- [name] Brendan Eich
- [role] Technology executive and programming language creator
- [expertise] Programming language design
- [expertise] Web browser technology
- [fact] Created JavaScript in 10 days at Netscape in 1995
- [fact] Co-founded the Mozilla project and later co-founded Brave Software
## Relations
- works_at [[Brave Software]]
- created [[JavaScript]]
- co-founded [[Mozilla]]
@@ -0,0 +1,22 @@
---
title: Claude Shannon
type: Person
tags: [information-theory, mathematics, bell-labs]
---
# Claude Shannon
Father of information theory.
## Observations
- [name] Claude Shannon
- [role] Mathematician and electrical engineer
- [expertise] Information theory
- [expertise] Boolean algebra
- [born] 1916
- [fact] Published "A Mathematical Theory of Communication" in 1948
## Relations
- works_at [[Bell Labs]]
- works_at [[MIT]]
- created [[Information Theory]]
@@ -0,0 +1,23 @@
---
title: Dennis Ritchie
type: Person
tags: [c, unix, bell-labs, programming-languages]
---
# Dennis Ritchie
Creator of the C programming language and co-creator of Unix.
## Observations
- [name] Dennis Ritchie
- [role] Computer scientist and language designer
- [expertise] Programming language design
- [expertise] Operating systems
- [born] 1941
- [fact] Created C in 1972 at Bell Labs
- [fact] Co-authored "The C Programming Language" with Brian Kernighan
## Relations
- works_at [[Bell Labs]]
- created [[C Programming Language]]
- co-created [[Unix]]
@@ -0,0 +1,21 @@
---
title: Edsger Dijkstra
type: Person
tags: [algorithms, structured-programming, formal-methods]
---
# Edsger Dijkstra
Pioneer of structured programming and graph algorithms.
## Observations
- [name] Edsger Dijkstra
- [role] Computer scientist and professor
- [expertise] Algorithm design
- [expertise] Formal verification
- [fact] Developed the shortest path algorithm that bears his name
- [fact] Advocated for structured programming and formal methods
## Relations
- works_at [[University of Texas at Austin]]
- created [[Dijkstra's Algorithm]]
@@ -0,0 +1,22 @@
---
title: Donald Knuth
type: Person
tags: [algorithms, tex, computer-science, mathematics]
---
# Donald Knuth
Author of The Art of Computer Programming and creator of TeX.
## Observations
- [name] Donald Knuth
- [role] Professor Emeritus of Computer Science
- [expertise] Analysis of algorithms
- [expertise] Digital typography
- [fact] Has been writing The Art of Computer Programming since 1962
- [fact] Created TeX typesetting system and METAFONT
## Relations
- works_at [[Stanford University]]
- created [[TeX]]
- authored [[The Art of Computer Programming]]
@@ -0,0 +1,20 @@
---
title: Fei-Fei Li
type: Person
tags: [ai, computer-vision, imagenet, stanford]
---
# Fei-Fei Li
Creator of ImageNet and pioneer in computer vision research.
## Observations
- [name] Fei-Fei Li
- [role] Professor of computer science
- [expertise] Computer vision
- [expertise] Machine learning
- [fact] Created ImageNet, the dataset that sparked the deep learning revolution
## Relations
- works_at [[Stanford University]]
- created [[ImageNet]]
@@ -0,0 +1,23 @@
---
title: Geoffrey Hinton
type: Person
tags: [ai, deep-learning, neural-networks, backpropagation]
---
# Geoffrey Hinton
Godfather of deep learning and pioneer of backpropagation.
## Observations
- [name] Geoffrey Hinton
- [role] Computer scientist and cognitive psychologist
- [expertise] Neural networks
- [expertise] Machine learning
- [expertise] Deep learning
- [born] 1947
- [fact] Helped popularize backpropagation for training neural networks
- [fact] Left Google in 2023 citing concerns about AI safety
## Relations
- works_at [[University of Toronto]]
- created [[Backpropagation Algorithm]]
@@ -0,0 +1,24 @@
---
title: Grace Hopper
type: Person
tags: [computing, navy, compilers]
---
# Grace Hopper
Pioneer of computer science and United States Navy rear admiral.
## Observations
- [name] Grace Hopper
- [role] Computer scientist and Navy rear admiral
- [expertise] Compiler design
- [expertise] Programming language theory
- [email] grace.hopper@navy.mil
- [fact] Developed one of the first linkers
- [fact] Coined the term "debugging" after finding an actual bug in a computer
- [achievement] Awarded the Presidential Medal of Freedom posthumously in 2016
## Relations
- works_at [[United States Navy]]
- contributed_to [[COBOL]]
- contributed_to [[UNIVAC]]
@@ -0,0 +1,22 @@
---
title: Guido van Rossum
type: Person
tags: [python, programming-languages, open-source]
---
# Guido van Rossum
Creator of Python and former Benevolent Dictator for Life of the Python community.
## Observations
- [name] Guido van Rossum
- [role] Programming language designer and software engineer
- [expertise] Programming language design
- [expertise] Software architecture
- [email] guido@python.org
- [fact] Created Python in 1991 while working at CWI in the Netherlands
- [fact] Served as Python's BDFL until stepping down in 2018
## Relations
- works_at [[Microsoft]]
- created [[Python]]
@@ -0,0 +1,21 @@
---
title: James Gosling
type: Person
tags: [java, programming-languages, sun-microsystems]
---
# James Gosling
Creator of the Java programming language.
## Observations
- [name] James Gosling
- [role] Computer scientist and software developer
- [expertise] Programming language design
- [expertise] Virtual machine architecture
- [email] james.gosling@aws.com
- [fact] Created Java at Sun Microsystems in the mid-1990s
## Relations
- works_at [[Amazon Web Services]]
- created [[Java]]
@@ -0,0 +1,24 @@
---
title: John Carmack
type: Person
tags: [games, graphics, vr, ai]
---
# John Carmack
Legendary game programmer and technical director.
## Observations
- [name] John Carmack
- [role] Programmer and technology entrepreneur
- [expertise] 3D graphics programming
- [expertise] Game engine development
- [expertise] Virtual reality
- [fact] Co-founded id Software and created the Doom and Quake engines
- [fact] Served as CTO of Oculus VR
## Relations
- works_at [[Keen Technologies]]
- co-founded [[id Software]]
- created [[Doom Engine]]
- created [[Quake Engine]]
@@ -0,0 +1,22 @@
---
title: Ken Thompson
type: Person
tags: [unix, go, bell-labs, plan9]
---
# Ken Thompson
Co-creator of Unix, inventor of the B programming language, and co-creator of Go.
## Observations
- [name] Ken Thompson
- [role] Computer scientist and systems programmer
- [expertise] Operating systems
- [expertise] Programming language design
- [fact] Co-created Unix with Dennis Ritchie at Bell Labs
- [fact] Co-created the Go programming language at Google
## Relations
- works_at [[Google]]
- co-created [[Unix]]
- co-created [[Go]]
@@ -0,0 +1,18 @@
---
title: Larry Page
type: Person
tags: [google, search, alphabet]
---
# Larry Page
Co-founder of Google and Alphabet.
## Observations
- [name] Larry Page
- [fact] Co-founded Google with Sergey Brin in 1998
- [fact] Developed the PageRank algorithm as a PhD student at Stanford
## Relations
- works_at [[Alphabet Inc]]
- co-founded [[Google]]
@@ -0,0 +1,24 @@
---
title: Leslie Lamport
type: Person
tags: [distributed-systems, latex, concurrency]
---
# Leslie Lamport
Pioneer of distributed systems theory and creator of LaTeX.
## Observations
- [name] Leslie Lamport
- [role] Computer scientist and researcher
- [expertise] Distributed systems
- [expertise] Concurrency
- [email] lamport@microsoft.com
- [fact] Created LaTeX document preparation system
- [fact] Invented the Paxos consensus algorithm
- [achievement] Won the Turing Award in 2013
## Relations
- works_at [[Microsoft Research]]
- created [[LaTeX]]
- created [[Paxos Algorithm]]
@@ -0,0 +1,26 @@
---
title: Linus Torvalds
type: Person
tags: [linux, git, open-source, kernel]
---
# Linus Torvalds
Creator of Linux and Git, two of the most influential open source projects in history.
## Observations
- [name] Linus Torvalds
- [role] Software engineer and open source leader
- [expertise] Operating system kernel development
- [expertise] Version control systems
- [expertise] Low-level systems programming
- [email] torvalds@linux-foundation.org
- [born] 1969
- [fact] Created Linux in 1991 as a university student in Helsinki
- [fact] Created Git in 2005 to manage Linux kernel development
- [quote] "Talk is cheap. Show me the code."
## Relations
- works_at [[Linux Foundation]]
- created [[Linux]]
- created [[Git]]
@@ -0,0 +1,24 @@
---
title: Margaret Hamilton
type: Person
tags: [nasa, software-engineering, apollo]
---
# Margaret Hamilton
Pioneering software engineer who led the team that developed the Apollo flight software.
## Observations
- [name] Margaret Hamilton
- [role] Software engineer and systems designer
- [expertise] Software engineering
- [expertise] Systems design
- [expertise] Error detection and recovery
- [fact] Coined the term "software engineering"
- [fact] Her software prevented an abort of the Apollo 11 moon landing
- [achievement] Awarded the Presidential Medal of Freedom in 2016
## Relations
- works_at [[MIT Instrumentation Laboratory]]
- works_at [[NASA]]
- created [[Apollo Flight Software]]
@@ -0,0 +1,18 @@
---
title: Rasmus Lerdorf
type: Person
tags: [php, web, programming-languages]
---
# Rasmus Lerdorf
Creator of the PHP programming language.
## Observations
- [name] Rasmus Lerdorf
- [expertise] Web development
- [fact] Created PHP in 1994 originally as Personal Home Page Tools
## Relations
- works_at [[Automattic]]
- created [[PHP]]
@@ -0,0 +1,23 @@
---
title: Richard Stallman
type: Person
tags: [free-software, gnu, emacs, gcc]
---
# Richard Stallman
Founder of the Free Software Foundation and GNU Project.
## Observations
- [name] Richard Stallman
- [role] Software freedom activist and programmer
- [expertise] Compiler design
- [expertise] Software licensing
- [fact] Founded the Free Software Foundation in 1985
- [fact] Created GNU Emacs, GCC, and the GPL license
## Relations
- works_at [[Free Software Foundation]]
- created [[GNU Project]]
- created [[GNU Emacs]]
- created [[GCC]]
@@ -0,0 +1,23 @@
---
title: Rob Pike
type: Person
tags: [go, plan9, utf8, bell-labs]
---
# Rob Pike
Co-creator of Go, Plan 9, and UTF-8.
## Observations
- [name] Rob Pike
- [role] Software engineer and systems researcher
- [expertise] Programming language design
- [expertise] Distributed systems
- [expertise] Text encoding
- [fact] Co-created UTF-8 encoding with Ken Thompson
## Relations
- works_at [[Google]]
- co-created [[Go]]
- co-created [[Plan 9]]
- co-created [[UTF-8]]
@@ -0,0 +1,18 @@
---
title: Satoshi Nakamoto
type: Person
tags: [bitcoin, cryptocurrency, blockchain]
---
# Satoshi Nakamoto
Pseudonymous creator of Bitcoin and the blockchain concept.
## Observations
- [name] Satoshi Nakamoto
- [fact] Published the Bitcoin whitepaper in 2008
- [fact] Mined the first Bitcoin block (genesis block) on January 3, 2009
- [fact] True identity remains unknown
## Relations
- created [[Bitcoin]]
@@ -0,0 +1,25 @@
---
title: Tim Berners-Lee
type: Person
tags: [web, internet, w3c, html]
---
# Tim Berners-Lee
Inventor of the World Wide Web and director of the W3C.
## Observations
- [name] Tim Berners-Lee
- [role] Computer scientist and web inventor
- [expertise] Web architecture
- [expertise] Distributed systems
- [expertise] Information management
- [email] timbl@w3.org
- [fact] Invented the World Wide Web in 1989 while at CERN
- [fact] Created the first web browser and web server
## Relations
- works_at [[W3C]]
- works_at [[MIT]]
- created [[World Wide Web]]
- created [[HTML]]
@@ -0,0 +1,21 @@
---
title: Vint Cerf
type: Person
tags: [internet, tcp-ip, networking]
---
# Vint Cerf
One of the fathers of the Internet, co-designer of TCP/IP.
## Observations
- [name] Vint Cerf
- [role] Internet pioneer and computer scientist
- [expertise] Network protocols
- [expertise] Internet architecture
- [email] vcerf@google.com
- [fact] Co-designed TCP/IP with Bob Kahn
## Relations
- works_at [[Google]]
- co-created [[TCP/IP]]
@@ -0,0 +1,24 @@
---
title: Yann LeCun
type: Person
tags: [ai, deep-learning, convolutional-networks, meta]
---
# Yann LeCun
Pioneer of convolutional neural networks and deep learning.
## Observations
- [name] Yann LeCun
- [role] VP and Chief AI Scientist
- [expertise] Deep learning
- [expertise] Convolutional neural networks
- [expertise] Computer vision
- [email] yann@cs.nyu.edu
- [fact] Developed convolutional neural networks in the late 1980s
- [achievement] Awarded the Turing Award in 2018
## Relations
- works_at [[Meta AI]]
- works_at [[New York University]]
- created [[LeNet]]
@@ -0,0 +1,19 @@
---
title: Yukihiro Matsumoto
type: Person
tags: [ruby, programming-languages, japan]
---
# Yukihiro Matsumoto
Creator of the Ruby programming language, known as Matz.
## Observations
- [name] Yukihiro Matsumoto
- [role] Programming language designer and software engineer
- [expertise] Programming language design
- [expertise] Object-oriented programming
- [fact] Created Ruby in 1995 with a focus on developer happiness
## Relations
- created [[Ruby]]
+22
View File
@@ -0,0 +1,22 @@
---
title: Book
type: schema
entity: Book
version: 1
schema:
title: string, book title
author?: string, author name
published_year?: integer, year of publication
genre?(enum): [fiction, nonfiction, technical]
settings:
validation: warn
---
# Book
A book or publication tracked in the knowledge base. The genre field uses
an enumeration to constrain values to fiction, nonfiction, or technical.
## Observations
- [purpose] Tracks books with structured metadata including genre classification
- [convention] published_year should be a four-digit year
@@ -0,0 +1,22 @@
---
title: Meeting
type: schema
entity: Meeting
version: 1
schema:
attendees(array): string, who was present
decisions(array): string, what was decided
action_items(array): string, follow-up tasks
date?: string, when the meeting occurred
settings:
validation: warn
---
# Meeting
A meeting or group discussion. Captures attendees, decisions made, and action items
for follow-up. All array fields can have multiple entries.
## Observations
- [purpose] Structured meeting notes that capture outcomes not just discussion
- [convention] Action items should include the responsible person where possible
@@ -0,0 +1,27 @@
---
title: Person
type: schema
entity: Person
version: 1
schema:
name: string, full name
role?: string, job title or position
works_at?: Organization, employer
expertise?(array): string, areas of knowledge
email?: string, contact email
settings:
validation: warn
---
# Person
A human individual in the knowledge graph. Represents people such as engineers,
scientists, authors, founders, or any notable individual tracked in the knowledge base.
## Observations
- [purpose] Defines the canonical structure for person notes
- [convention] The name field is always required for identification
## Relations
- used_by [[Paul Graham]]
- used_by [[Rich Hickey]]
@@ -0,0 +1,23 @@
---
title: SoftwareProject
type: schema
entity: SoftwareProject
version: 1
schema:
name: string, project name
language?: string, primary programming language
license?: string, open source license type
repo_url?: string, repository URL
maintained_by?: Person, primary maintainer
settings:
validation: warn
---
# SoftwareProject
A software project or library tracked in the knowledge base. Can be open source
or proprietary. Links to the people who maintain it and the technologies used.
## Observations
- [purpose] Tracks software projects and their key metadata
- [convention] repo_url should be a full URL including protocol
@@ -0,0 +1,22 @@
---
title: StrictSchema
type: schema
entity: StrictEntity
version: 1
schema:
name: string, entity name
category: string, required classification
priority?: integer, priority level
settings:
validation: strict
---
# StrictSchema
A schema with strict validation mode enabled. Notes validated against this schema
will produce errors instead of warnings for missing required fields. Useful for
CI/CD enforcement where structural compliance is mandatory.
## Observations
- [purpose] Demonstrates strict validation mode for required field enforcement
- [mode] Strict validation blocks sync on missing required fields
@@ -0,0 +1,27 @@
---
title: Basic Memory
type: SoftwareProject
tags: [mcp, knowledge-management, local-first, python]
---
# Basic Memory
Local-first knowledge management system built on the Model Context Protocol.
Enables bidirectional communication between LLMs and markdown files to create
a personal knowledge graph.
## Observations
- [name] Basic Memory
- [language] Python
- [license] MIT
- [repo_url] https://github.com/basicmachines-co/basic-memory
- [fact] Uses SQLite for indexing and markdown files as source of truth
- [fact] Provides 17 MCP tools for AI integration
- [version] 0.13.8
## Relations
- maintained_by [[Pedro Hernandez]]
- uses [[Model Context Protocol]]
- uses [[SQLite]]
- uses [[FastAPI]]
- integrates_with [[Claude Desktop]]
@@ -0,0 +1,24 @@
---
title: Grace Hopper
schema: Person
tags: [computing, navy, compilers]
---
# Grace Hopper
Pioneer of computer science and United States Navy rear admiral. This note uses
an explicit schema reference rather than relying on type matching.
## Observations
- [name] Grace Hopper
- [role] Computer scientist and Navy rear admiral
- [expertise] Compiler design
- [expertise] Programming language theory
- [email] grace.hopper@navy.mil
- [fact] Developed one of the first linkers and popularized machine-independent languages
- [fact] Coined the term "debugging" after finding an actual bug in a computer
## Relations
- works_at [[United States Navy]]
- contributed_to [[COBOL]]
- contributed_to [[UNIVAC]]
@@ -0,0 +1,17 @@
---
title: Random Thought About Coffee
tags: [coffee, thoughts]
---
# Random Thought About Coffee
Just a freeform note with no type and no schema. Should pass through validation
with no checks applied.
## Observations
- [thought] Ethiopian Yirgacheffe has a blueberry note that pairs well with dark chocolate
- [preference] Pour over is superior to French press for lighter roasts
- [fact] Coffee plants originated in Ethiopia
## Relations
- relates_to [[Coffee Brewing Methods]]
@@ -0,0 +1,29 @@
---
title: Paul Graham
type: Person
tags: [startups, essays, lisp, venture-capital]
---
# Paul Graham
Co-founder of Y Combinator and prolific essayist whose writing on startups, programming,
and technology has influenced a generation of founders and engineers.
## Observations
- [name] Paul Graham
- [role] Essayist, programmer, and venture capitalist
- [expertise] Startups and early-stage investing
- [expertise] Lisp and functional programming
- [expertise] Essay writing and communication
- [expertise] Programming language design
- [email] pg@ycombinator.com
- [fact] Co-created Viaweb, one of the first web applications, sold to Yahoo in 1998
- [fact] Wrote "Hackers and Painters", a collection of essays on programming and startups
- [born] 1964
## Relations
- works_at [[Y Combinator]]
- authored [[Hackers and Painters]]
- authored [[On Lisp]]
- founded [[Viaweb]]
- influenced [[Sam Altman]]
@@ -0,0 +1,24 @@
---
title: Rich Hickey
type: Person
tags: [clojure, functional-programming, databases]
---
# Rich Hickey
Creator of Clojure and Datomic. Known for deep thinking about state, identity, and
the problems with object-oriented programming.
## Observations
- [name] Rich Hickey
- [role] Language designer and software architect
- [expertise] Programming language design
- [expertise] Database architecture
- [expertise] Functional programming
- [fact] Spent two years developing Clojure before releasing it publicly
- [fact] His talk "Simple Made Easy" is one of the most-watched programming talks
## Relations
- works_at [[Nubank]]
- created [[Clojure]]
- created [[Datomic]]
@@ -0,0 +1,32 @@
---
title: Team Standup 2024-01-15
type: Meeting
schema:
attendees(array): string, who was there
decisions(array): string, what was decided
action_items(array): string, follow-ups
blockers?(array): string, anything stuck
date?: string, meeting date
tags: [standup, team, weekly]
---
# Team Standup 2024-01-15
Weekly standup meeting to review progress and plan the sprint.
## Observations
- [attendees] Paul
- [attendees] Sarah
- [attendees] James
- [attendees] Maria
- [decisions] Ship v2 by Friday
- [decisions] Defer mobile app to next quarter
- [action_items] Paul to review PR #42
- [action_items] Sarah to update API docs
- [action_items] James to fix flaky CI tests
- [blockers] Waiting on API credentials from vendor
- [date] 2024-01-15
## Relations
- part_of [[Q1 2024 Sprint]]
- followed_by [[Team Standup 2024-01-22]]
@@ -0,0 +1,27 @@
---
title: Nikola Tesla
type: Person
tags: [electricity, invention, engineering]
---
# Nikola Tesla
Inventor and electrical engineer. This note has all required fields plus
observations not defined in the Person schema. Extra observations should
generate informational messages, not warnings.
## Observations
- [name] Nikola Tesla
- [role] Inventor and electrical engineer
- [expertise] Electrical engineering
- [expertise] Alternating current systems
- [nationality] Serbian-American
- [born] 1856
- [died] 1943
- [patent_count] Over 300 patents worldwide
- [fact] Developed the alternating current electrical system
- [fact] Had an eidetic memory and could visualize complex machines in his mind
## Relations
- works_at [[Westinghouse Electric]]
- rival_of [[Thomas Edison]]
@@ -0,0 +1,19 @@
---
title: Alan Kay
type: Person
tags: [computing, oop, xerox-parc]
---
# Alan Kay
Computer scientist known for pioneering object-oriented programming.
This note is intentionally missing both required [name] AND optional [role].
## Observations
- [fact] Coined the phrase "The best way to predict the future is to invent it"
- [expertise] Object-oriented programming
- [expertise] User interface design
- [born] 1940
## Relations
- works_at [[Xerox PARC]]
@@ -0,0 +1,19 @@
---
title: Ada Lovelace
type: Person
tags: [computing, mathematics, history]
---
# Ada Lovelace
Mathematician and writer, often regarded as the first computer programmer.
This note is intentionally missing the required [name] observation.
## Observations
- [fact] Wrote the first computer program for Charles Babbage's Analytical Engine
- [fact] Was the daughter of poet Lord Byron
- [born] 1815
- [role] Mathematician and writer
## Relations
- collaborated_with [[Charles Babbage]]
@@ -0,0 +1,21 @@
---
title: The Art of War
type: Book
tags: [strategy, military, classic]
---
# The Art of War
Ancient Chinese military treatise attributed to Sun Tzu. This note has a genre
value that is not in the allowed enum set [fiction, nonfiction, technical].
## Observations
- [title] The Art of War
- [author] Sun Tzu
- [published_year] -500
- [genre] philosophy
- [fact] One of the oldest and most influential works on military strategy
## Relations
- written_by [[Sun Tzu]]
- influenced [[Machiavelli]]
+1
View File
@@ -0,0 +1 @@
"""Integration tests for the Basic Memory schema system."""
+85
View File
@@ -0,0 +1,85 @@
"""Shared fixtures for schema integration tests.
Helper functions and path constants live in helpers.py for explicit import.
This file only contains pytest fixtures (auto-injected by pytest).
"""
from pathlib import Path
import pytest
from test_schema.helpers import (
SCHEMAS_DIR,
VALID_DIR,
WARNINGS_DIR,
EDGE_CASES_DIR,
INFERENCE_DIR,
DRIFT_SCHEMA_DIR,
DRIFT_PEOPLE_DIR,
parse_frontmatter,
)
@pytest.fixture
def schemas_dir() -> Path:
return SCHEMAS_DIR
@pytest.fixture
def valid_dir() -> Path:
return VALID_DIR
@pytest.fixture
def warnings_dir() -> Path:
return WARNINGS_DIR
@pytest.fixture
def edge_cases_dir() -> Path:
return EDGE_CASES_DIR
@pytest.fixture
def inference_dir() -> Path:
return INFERENCE_DIR
@pytest.fixture
def drift_schema_dir() -> Path:
return DRIFT_SCHEMA_DIR
@pytest.fixture
def drift_people_dir() -> Path:
return DRIFT_PEOPLE_DIR
@pytest.fixture
def person_schema_frontmatter(schemas_dir) -> dict:
"""Load Person schema frontmatter from fixture."""
return parse_frontmatter(schemas_dir / "Person.md")
@pytest.fixture
def book_schema_frontmatter(schemas_dir) -> dict:
"""Load Book schema frontmatter from fixture."""
return parse_frontmatter(schemas_dir / "Book.md")
@pytest.fixture
def meeting_schema_frontmatter(schemas_dir) -> dict:
"""Load Meeting schema frontmatter from fixture."""
return parse_frontmatter(schemas_dir / "Meeting.md")
@pytest.fixture
def software_project_schema_frontmatter(schemas_dir) -> dict:
"""Load SoftwareProject schema frontmatter from fixture."""
return parse_frontmatter(schemas_dir / "SoftwareProject.md")
@pytest.fixture
def strict_schema_frontmatter(schemas_dir) -> dict:
"""Load StrictSchema frontmatter from fixture."""
return parse_frontmatter(schemas_dir / "StrictSchema.md")
+61
View File
@@ -0,0 +1,61 @@
"""Shared helpers and constants for schema integration tests.
Separated from conftest.py so they can be explicitly imported by test modules.
Conftest fixtures are auto-injected by pytest and don't need explicit import.
"""
import re
from pathlib import Path
import yaml
from basic_memory.schema.inference import ObservationData, RelationData
# --- Fixture Paths ---
FIXTURES_ROOT = Path(__file__).parent.parent / "fixtures" / "schema"
SCHEMAS_DIR = FIXTURES_ROOT / "schemas"
VALID_DIR = FIXTURES_ROOT / "valid"
WARNINGS_DIR = FIXTURES_ROOT / "warnings"
EDGE_CASES_DIR = FIXTURES_ROOT / "edge-cases"
INFERENCE_DIR = FIXTURES_ROOT / "inference" / "people"
DRIFT_SCHEMA_DIR = FIXTURES_ROOT / "drift" / "schema"
DRIFT_PEOPLE_DIR = FIXTURES_ROOT / "drift" / "people"
# --- Frontmatter Parsing ---
def parse_frontmatter(filepath: Path) -> dict:
"""Extract YAML frontmatter from a markdown file."""
text = filepath.read_text(encoding="utf-8")
match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
if not match:
return {}
return yaml.safe_load(match.group(1)) or {}
def parse_observations(filepath: Path) -> list[ObservationData]:
"""Extract ObservationData from a markdown file.
Parses lines matching: - [category] content
"""
text = filepath.read_text(encoding="utf-8")
pattern = re.compile(r"^- \[([^\]]+)\] (.+)$", re.MULTILINE)
return [ObservationData(m.group(1), m.group(2)) for m in pattern.finditer(text)]
def parse_relations(filepath: Path) -> list[RelationData]:
"""Extract RelationData from a markdown file.
Parses lines matching: - relation_type [[Target]]
Only matches lines under a ## Relations heading.
"""
text = filepath.read_text(encoding="utf-8")
relations_match = re.search(r"## Relations\n(.*?)(?=\n## |\Z)", text, re.DOTALL)
if not relations_match:
return []
relations_text = relations_match.group(1)
pattern = re.compile(r"^- (\S+) \[\[([^\]]+)\]\]$", re.MULTILINE)
return [RelationData(m.group(1), m.group(2)) for m in pattern.finditer(relations_text)]
@@ -0,0 +1,72 @@
"""Integration tests for schema drift detection using drift fixtures."""
import pytest
from basic_memory.schema.parser import parse_schema_note
from basic_memory.schema.diff import diff_schema
from basic_memory.schema.inference import NoteData
from test_schema.helpers import (
parse_frontmatter,
parse_observations,
parse_relations,
DRIFT_SCHEMA_DIR,
DRIFT_PEOPLE_DIR,
)
def load_drift_notes() -> list[NoteData]:
"""Load all drift person fixture files as NoteData objects."""
return [
NoteData(
identifier=filepath.stem,
observations=parse_observations(filepath),
relations=parse_relations(filepath),
)
for filepath in sorted(DRIFT_PEOPLE_DIR.glob("*.md"))
]
@pytest.fixture
def drift_schema():
frontmatter = parse_frontmatter(DRIFT_SCHEMA_DIR / "Person.md")
return parse_schema_note(frontmatter)
@pytest.fixture
def drift_notes():
return load_drift_notes()
@pytest.fixture
def drift_result(drift_schema, drift_notes):
return diff_schema(drift_schema, drift_notes)
class TestDriftCorpusSize:
def test_minimum_drift_notes(self, drift_notes):
assert len(drift_notes) >= 20
class TestNewFieldDetection:
"""github field appears in 60% of notes but is not in the schema."""
def test_github_detected_as_new_field(self, drift_result):
new_field_names = [f.name for f in drift_result.new_fields]
assert "github" in new_field_names
class TestDroppedFieldDetection:
"""role field is in schema but only 5% of notes have it."""
def test_role_detected_as_dropped(self, drift_result):
dropped_names = [f.name for f in drift_result.dropped_fields]
assert "role" in dropped_names
class TestCardinalityChanges:
"""works_at changed from single to multiple per note."""
def test_works_at_cardinality_change_detected(self, drift_result):
changes = [msg for msg in drift_result.cardinality_changes if "works_at" in msg]
assert len(changes) >= 1
@@ -0,0 +1,112 @@
"""Integration tests for schema inference using the 30+ person fixture corpus."""
import pytest
from basic_memory.schema.inference import infer_schema, NoteData
from test_schema.helpers import parse_observations, parse_relations, INFERENCE_DIR
def load_inference_notes() -> list[NoteData]:
"""Load all person fixture files as NoteData objects."""
return [
NoteData(
identifier=filepath.stem,
observations=parse_observations(filepath),
relations=parse_relations(filepath),
)
for filepath in sorted(INFERENCE_DIR.glob("*.md"))
]
@pytest.fixture
def person_notes() -> list[NoteData]:
return load_inference_notes()
@pytest.fixture
def inference_result(person_notes):
return infer_schema("Person", person_notes)
class TestInferenceCorpusSize:
def test_minimum_notes_loaded(self, person_notes):
assert len(person_notes) >= 30
def test_notes_analyzed_matches(self, inference_result, person_notes):
assert inference_result.notes_analyzed == len(person_notes)
class TestNameFieldInference:
"""Name should be ~100% -> required."""
def test_name_is_required(self, inference_result):
assert "name" in inference_result.suggested_required
def test_name_frequency_near_100(self, inference_result):
name_freq = next(f for f in inference_result.field_frequencies if f.name == "name")
assert name_freq.percentage >= 0.95
class TestRoleFieldInference:
"""Role should be ~90% -> optional."""
def test_role_is_optional(self, inference_result):
assert "role" in inference_result.suggested_optional
def test_role_frequency_around_90(self, inference_result):
role_freq = next(f for f in inference_result.field_frequencies if f.name == "role")
assert 0.80 <= role_freq.percentage < 0.95
class TestExpertiseFieldInference:
"""Expertise should be ~60% -> optional, array."""
def test_expertise_is_optional(self, inference_result):
assert "expertise" in inference_result.suggested_optional
def test_expertise_is_array(self, inference_result):
freq = next(f for f in inference_result.field_frequencies if f.name == "expertise")
assert freq.is_array is True
class TestEmailFieldInference:
"""Email should be ~27% -> optional."""
def test_email_is_optional(self, inference_result):
assert "email" in inference_result.suggested_optional
class TestBornFieldInference:
"""Born should be ~17% -> excluded."""
def test_born_is_excluded(self, inference_result):
assert "born" in inference_result.excluded
def test_born_frequency_below_threshold(self, inference_result):
born_freq = next(f for f in inference_result.field_frequencies if f.name == "born")
assert born_freq.percentage < 0.25
class TestWorksAtRelationInference:
"""works_at relation should be ~73% -> optional."""
def test_works_at_is_optional(self, inference_result):
assert "works_at" in inference_result.suggested_optional
def test_works_at_is_relation(self, inference_result):
freq = next(f for f in inference_result.field_frequencies if f.name == "works_at")
assert freq.source == "relation"
class TestSuggestedSchema:
def test_suggested_schema_not_empty(self, inference_result):
assert len(inference_result.suggested_schema) > 0
def test_excluded_fields_not_in_schema(self, inference_result):
schema_names = set()
for key in inference_result.suggested_schema:
name = key.replace("?", "").split("(")[0]
schema_names.add(name)
for excluded in inference_result.excluded:
assert excluded not in schema_names
@@ -0,0 +1,83 @@
"""Integration tests for the Picoschema parser using real fixture files."""
import pytest
from basic_memory.schema.parser import parse_schema_note
class TestPersonSchemaParsing:
"""Parse the Person.md schema fixture and verify all fields."""
def test_person_schema_entity(self, person_schema_frontmatter):
schema = parse_schema_note(person_schema_frontmatter)
assert schema.entity == "Person"
def test_person_schema_version(self, person_schema_frontmatter):
schema = parse_schema_note(person_schema_frontmatter)
assert schema.version == 1
def test_person_schema_validation_mode(self, person_schema_frontmatter):
schema = parse_schema_note(person_schema_frontmatter)
assert schema.validation_mode == "warn"
def test_person_schema_field_count(self, person_schema_frontmatter):
schema = parse_schema_note(person_schema_frontmatter)
assert len(schema.fields) == 5
def test_person_name_field_is_required(self, person_schema_frontmatter):
schema = parse_schema_note(person_schema_frontmatter)
name_field = next(f for f in schema.fields if f.name == "name")
assert name_field.required is True
assert name_field.type == "string"
assert name_field.description == "full name"
def test_person_role_field_is_optional(self, person_schema_frontmatter):
schema = parse_schema_note(person_schema_frontmatter)
role_field = next(f for f in schema.fields if f.name == "role")
assert role_field.required is False
def test_person_works_at_is_entity_ref(self, person_schema_frontmatter):
schema = parse_schema_note(person_schema_frontmatter)
works_at = next(f for f in schema.fields if f.name == "works_at")
assert works_at.is_entity_ref is True
assert works_at.type == "Organization"
def test_person_expertise_is_array(self, person_schema_frontmatter):
schema = parse_schema_note(person_schema_frontmatter)
expertise = next(f for f in schema.fields if f.name == "expertise")
assert expertise.is_array is True
def test_person_email_is_optional(self, person_schema_frontmatter):
schema = parse_schema_note(person_schema_frontmatter)
email = next(f for f in schema.fields if f.name == "email")
assert email.required is False
class TestBookSchemaParsing:
"""Parse the Book.md schema fixture."""
def test_book_genre_is_enum(self, book_schema_frontmatter):
schema = parse_schema_note(book_schema_frontmatter)
genre = next(f for f in schema.fields if f.name == "genre")
assert genre.is_enum is True
assert set(genre.enum_values) == {"fiction", "nonfiction", "technical"}
class TestStrictSchemaParsing:
"""Verify strict validation mode is parsed."""
def test_strict_validation_mode(self, strict_schema_frontmatter):
schema = parse_schema_note(strict_schema_frontmatter)
assert schema.validation_mode == "strict"
class TestSchemaParsingErrors:
"""Verify parser raises on invalid frontmatter."""
def test_missing_entity_raises(self):
with pytest.raises(ValueError, match="entity"):
parse_schema_note({"type": "schema", "schema": {"name": "string"}})
def test_missing_schema_dict_raises(self):
with pytest.raises(ValueError, match="schema"):
parse_schema_note({"type": "schema", "entity": "Person"})

Some files were not shown because too many files have changed in this diff Show More