Skip to main content

Data Model

Last updated: 2026-08-08

For the full field-by-field JSON Schema, see Schema Reference — this page covers the same records from a system-design angle. See also Developer Guide: Data Model for the lesson-author-facing view.


Overview

All data lives in data/ as a SQLite database and JSON support files.

FilePurposeEdit directly?
lessons.dbSource of truth — all lesson and candidate records (SQLite)No — use node scripts/lessons.mjs
lesson-manifest.jsonPre-compiled runtime manifestNo — run lessons build
config.jsonInjection and scanning configurationYes
scan-state.jsonPer-file byte offsets for incremental scanningNo — managed by scanner
obsoleted-lessons.jsonAppend-only ledger of lessons archived because the eval suite showed the model already handles themYes — appended by lessons maintainers

There is no lessons.json or cross-project-candidates.json file — both candidates and active lessons are rows in lessons.db, distinguished by status.


Lesson record (SQLite lessons.db)

All lesson fields are stored in the lessons table. Key columns:

id TEXT PRIMARY KEY -- ULID, generated on add
slug TEXT NOT NULL UNIQUE -- kebab-case summary + 4-char random suffix
status TEXT -- candidate | reviewed | active | disabled | archived
type TEXT NOT NULL DEFAULT 'hint'
-- directive | guard | hint | protocol (see Type taxonomy below)
summary TEXT NOT NULL -- >= 20 chars, no "..." suffix
problem TEXT NOT NULL -- >= 20 chars
solution TEXT NOT NULL -- >= 20 chars
toolNames TEXT NOT NULL DEFAULT '[]' -- JSON array, exact tool name match
commandPatterns TEXT NOT NULL DEFAULT '[]' -- JSON array of regex strings
pathPatterns TEXT NOT NULL DEFAULT '[]' -- JSON array of glob strings
commandMatchTarget TEXT -- NULL | 'full' | 'executable'
modelPatterns TEXT NOT NULL DEFAULT '[]' -- JSON array of regex strings, AND-gated
scope TEXT -- NULL = global; project-ID string = scoped
priority INTEGER NOT NULL DEFAULT 5 -- 1-10
confidence REAL NOT NULL DEFAULT 0.8 -- 0.0-1.0
tags TEXT NOT NULL DEFAULT '[]' -- JSON array of "category:value" strings
requires TEXT -- NULL | JSON descriptor/array (inclusion guard)
duplicatedBy TEXT -- NULL | JSON descriptor (exclusion guard)

Type taxonomy

type is the single signal controlling injection behavior:

TypeInjection behavior
hintInject as additionalContext on PreToolUse match
guardDeny the tool call entirely (block); message shown to agent
protocolInject at session start (protocol/reasoning reminders)
directiveInject at session start AND re-injected at context thresholds

For guard lessons, the rendered message supports a {command} placeholder that is substituted with the actual command (truncated to 120 chars) at block time.

Key field constraints

FieldConstraint
summary>= 20 chars, no ... suffix, no template placeholders
problem>= 20 chars, no template placeholders
solution>= 20 chars, no template placeholders
commandPatternsMust be valid regex; invalid patterns are dropped (with a warning) at build time
type = 'protocol'Use sparingly — fires on every session startup, no dedup within a session
confidenceBelow minConfidence → excluded from the manifest at build time
requiresExcludes the lesson from the manifest unless the named plugin/skill/mcp-server/github-issue is installed
duplicatedByExcludes the lesson from the manifest when the named artifact IS installed

Lesson manifest (lesson-manifest.json)

Generated by lessons build. This is the file the injection hook reads at runtime.

{
"type": "lessons-learned-manifest",
"version": 1,
"generatedAt": "2026-08-01T00:00:00Z",
"config": {
"injectionBudgetBytes": 4096,
"maxLessonsPerInjection": 3,
"minConfidence": 0.5,
"minPriority": 1,
"compactionReinjectionThreshold": 7,
},
"lessons": {
"<ulid>": {
"slug": "pytest-tty-hanging-k9m2",
"type": "guard", // directive | guard | hint | protocol
"priority": 8,
"toolNames": ["Bash"],
// Regex stored as { source, flags } for JSON-safe serialization
// Reconstructed with new RegExp(source, flags) at match time
"commandRegexSources": [{ "source": "\\bpytest\\b(?!...)", "flags": "" }],
"commandMatchTarget": "executable",
"pathRegexSources": [],
"modelRegexSources": [],
"tags": ["lang:python", "tool:pytest", "severity:hang"],
"scope": null,
"message": "## REQUIRED: pytest flags...",
"summary": "pytest hangs in non-interactive envs due to TTY detection",
"problem": "pytest attaches to a TTY by default...",
"solution": "Run with --no-header...",
},
},
}

Lessons are excluded from the manifest if confidence < minConfidence, priority < minPriority, a duplicatedBy artifact is detected as installed, or a requires artifact is NOT detected as installed. disabled-status lessons are always excluded from matching regardless of these checks.


Config (config.json)

{
"injectionBudgetBytes": 4096, // Max bytes of additionalContext per tool call
"maxLessonsPerInjection": 3, // Max lessons injected per tool call
"minConfidence": 0.5, // Exclude lessons below this from manifest
"minPriority": 1, // Exclude lessons below this from manifest
"compactionReinjectionThreshold": 7, // Lessons above this priority re-inject after context compaction

"scanPaths": ["~/.claude/projects/"],
"autoScanIntervalHours": 24,
"maxCandidatesPerScan": 50,

"scoring": {
"multiSessionBonus": 2, // Priority boost when seen in 2+ sessions
"multiProjectBonus": 1, // Priority boost per additional project
"hangTimeoutBonus": 1,
"userCorrectionBonus": 1,
"singleOccurrencePenalty": -1,
},
}

Candidate records (lessons.db, status = 'candidate')

Candidates are ordinary rows in the lessons table with status='candidate' — there is no separate candidate file. node scripts/lessons.mjs scan aggregate reads them and prints a ranked JSON view to stdout (this replaced the old scan candidates name, which is now a deprecated alias):

{
"generatedAt": "2026-08-01T00:00:00Z",
"totalCandidates": 1,
"candidates": [
{
"index": 1, // 1-based position in THIS output only — not stored, not usable by any command
"id": "01JQSEED00000000000000001", // the real, stable handle — use this with `promote --ids`
"slug": "git-stash-untracked-5x3q",
"tool": "Bash",
"confidence": 0.85, // base confidence + 0.1 per extra project seen, capped at 1.0
"priority": 7, // base priority + projectCount, capped at 10
"occurrenceCount": 3,
"sessionCount": 2,
"projectCount": 1,
"problem": "...",
"solution": "...",
"tags": [],
"sourceSessionIds": ["..."],
"createdAt": "...",
},
],
}

To promote a candidate, use its id (not index): node scripts/lessons.mjs promote --ids <id>. scan promote <index> has been removed — indexes are ephemeral (recomputed on every aggregate call) and were never a safe handle for a follow-up command.


Scan state (scan-state.json)

{
"files": {
"/abs/path/to/session.jsonl": 184320, // Last byte offset read
},
"lastFullScanAt": "2026-08-01T00:00:00Z",
}

The scanner resumes each file from its saved offset, processing only new bytes. A --full flag resets all offsets to 0.


Tag taxonomy

Tags follow category:value format. Established categories:

CategoryExamples
langpython, typescript, javascript, go
toolpytest, git, npm, docker
severityhang, data-loss, silent-failure, error
topictesting, auth, networking, types
candidatenode-gotchas-skill — flagged for future skill file aggregation