Documentation
¶
Overview ¶
Package migrate implements the one-shot migration from Engram's SQLite database to Thoughtline's storage layer. It is intentionally a standalone binary (cmd/migrate/main.go) so it can be built, audited, and discarded without touching the MCP server's public API surface.
Public types in this file are the shared vocabulary used by reader.go, mapper.go, writer.go, and main.go. No logic lives here — types only.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func MapRow ¶
func MapRow(o EngramObservation, logger Logger) (memory.Memory, error)
MapRow applies all column and type mappings, returning a memory.Memory ready for storage.Save(). It enforces the following transformations:
- project is lowercased (Engram stores mixed case in some old rows)
- preference scope is forced to personal regardless of source
- session_id is always cleared (Engram session IDs are not UUIDv7)
- NormalizedHash is left zero (storage.Save() recomputes it)
- title > 200 runes is truncated at rune boundary; logger receives one WARN
- content > MaxContentBytes causes MapRow to return an error
- invalid topic_key (fails Thoughtline's regex) is silently cleared
- origin-type:<x> tag is merged into Tags for coerced types
- bogus scope value returns error
func MapTimestamp ¶
MapTimestamp parses an ISO-8601 string and returns the Unix epoch in milliseconds. Always interprets the input in UTC to avoid timezone corruption.
Accepted formats:
- ISO 8601 with T separator: "2024-03-15T10:30:00Z", "2024-03-15T10:30:00+00:00"
- SQLite default datetime: "2024-03-15 10:30:00" (space separator, no timezone; assumed UTC)
func MapType ¶
MapType converts an Engram type string to a Thoughtline memory.Type plus any provenance tags to add to the migrated row.
Mapping table (spec engram-migration Requirement 4):
bugfix → bugfix (no tag) preference → preference (no tag) decision → decision (no tag) architecture → architecture (no tag) pattern → convention + origin-type:pattern config → convention + origin-type:config discovery → convention + origin-type:discovery manual → convention + origin-type:manual <anything> → convention + origin-type:<anything>
Types ¶
type Config ¶
type Config struct {
// Source is the absolute path to engram.db (read-only).
Source string
// Dest is the absolute path to thoughtline.db (read-write).
Dest string
// DryRun, when true, performs all reads and mapping but skips all writes.
DryRun bool
// Verbose, when true, causes each row result to be emitted to the logger
// as it is processed (not just at the end).
Verbose bool
}
Config holds the CLI flags resolved by main.go before calling Run.
type EngramObservation ¶
type EngramObservation struct {
SyncID string
Type string // open set: bugfix|decision|architecture|pattern|config|preference|discovery|manual
Title string
Content string
Project string
Scope string
TopicKey string
Tags []string // Engram has no tags column; always nil. Reserved for future.
NormalizedHash string // stored but not forwarded — storage.Save() recomputes it
RevisionCount int
CreatedAt string // ISO 8601
UpdatedAt string // ISO 8601
DeletedAt *string // nil = active row
}
EngramObservation is the raw row read from the Engram observations table. Columns that Engram stores but Thoughtline has no equivalent for (tool_name, duplicate_count, last_seen_at) are read and discarded by the reader — they never appear here.
func ReadObservations ¶
ReadObservations reads all active (non-soft-deleted) observations from the Engram database and returns them as a slice of EngramObservation. It accepts a *sql.DB opened by the caller with read-only WAL pragmas — it never opens its own connection.
Columns that Engram stores but Thoughtline has no equivalent for (tool_name, duplicate_count, last_seen_at) are scanned and discarded.
The full result set is returned in one slice. At ~291 rows × ~2 KB avg the memory budget is well within reason.
type Logger ¶
Logger is the interface mapper and writer functions use so they stay pure and testable. StructuredLogger and NopLogger both satisfy it. Log fields are key=value pairs; callers pass them as a flat map.
type NopLogger ¶
type NopLogger struct{}
NopLogger implements Logger by discarding every message. Use in tests that don't need to inspect log output, and in production contexts where no log destination is configured.
type RowResult ¶
type RowResult struct {
SyncID string
// Action is one of: "created", "skipped-deleted", "skipped-duplicate",
// "skipped-topic-collision", "error".
Action string
// Reason is populated when Action is "error" or any "skipped-*" variant.
Reason string
}
RowResult captures what happened to a single Engram row during migration. The full slice of RowResults is written to the structured log file; stdout shows only aggregate counters.
type StructuredLogger ¶
type StructuredLogger struct {
// contains filtered or unexported fields
}
StructuredLogger writes one log line per event to an io.Writer in the format:
level=INFO sync_id=obs-abc123 action=created
Keys are sorted for deterministic output. Both main.go (stdout + file) and tests that need to inspect output should use StructuredLogger; tests that don't care about log content should use NopLogger.
func NewStructuredLogger ¶
func NewStructuredLogger(w io.Writer) StructuredLogger
NewStructuredLogger creates a StructuredLogger writing to w.
type Summary ¶
type Summary struct {
Total int
Created int
SkippedDeleted int // soft-deleted source rows; not migrated by spec
SkippedDuplicate int // already in destination by sync_id
SkippedTopicCol int // (project, topic_key) collision with existing Thoughtline row
Errors int // rows that failed mapping or storage; do not abort the run
Truncations int // rows whose title was truncated at 200 runes
Rows []RowResult
}
Summary is the aggregate result of a migration run. Run() always returns a Summary even when individual rows errored — per-row errors are non-fatal.
func Run ¶
Run executes the full migration from Config.Source (Engram DB) to Config.Dest (Thoughtline DB). It processes all active rows, accumulating results in a Summary. Individual row errors are non-fatal — Run always processes every row and returns the complete Summary regardless of how many rows errored.
If Config.DryRun is true, Run reads and maps all rows but performs no writes to the destination. Counters in the returned Summary reflect what would have happened.