Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ This repo is targeting the **medium** architecture first:

This gives high value without memory bloat or premature overbuilding.

Start with the [fixture-mode quickstart](docs/quickstart.md) to seed a local
database, search fixture metadata, review extraction candidates, and run a
dry-run export without live MailPlus credentials.

## Runtime baseline

M0 uses a Python 3.12 package with SQLite-friendly local foundations.
Expand Down
128 changes: 128 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Quickstart

This walkthrough uses the synthetic fixture corpus only. It does not connect to
live MailPlus, IMAP, wiki, memory, or reminder surfaces.

## Setup

```bash
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -e .
```

Confirm the local fixture-mode environment:

```bash
mpi doctor
```

Expected shape:

```text
MailPlus Intelligence fixture doctor
- ok: runtime: python 3.12; expected >=3.12
- ok: storage: selected storage engine: sqlite
- ok: manifest: project.bootstrap.yaml present
- ok: fixtures: loaded metadata fixture corpus with 8 messages
- ok: schema: metadata schema user_version=1
- gated: live-mailplus: live MailPlus credentials intentionally unavailable in fixture mode
result: ok
```

For machine-readable output, run:

```bash
mpi doctor --json
```

The JSON response includes `ok` and a `checks` array. Each check has `name`,
`status`, `message`, and an optional `next_step`.

## Seed A Local Database

Use a file-backed database so queue decisions and sync state persist:

```bash
mpi --db ./mpi.db seed --from-fixtures fixtures/mailplus_metadata
```

Expected shape:

```text
Seeded fixture corpus: inserted=8, skipped=0, queued=4, queue_skipped=0.
```

Re-running the command is safe; indexed messages and deterministic queue items
are skipped when they already exist.

## Search And Inspect

```bash
mpi --db ./mpi.db search --keyword Atlas
```

Expected shape:

```text
2026-01-05T15:02:00Z <thread-a-003@example.test> Re: Project Atlas kickoff
locator: fixture-export-003 / uid=1002
```

Inspect the reconstructed thread:

```bash
mpi --db ./mpi.db thread thread-a
```

Expected shape:

```text
Thread: thread-a (3 messages)
2026-01-05T15:02:00Z <thread-a-003@example.test> Re: Project Atlas kickoff
```

## Review Queue

List candidates:

```bash
mpi --db ./mpi.db queue list
```

Expected shape:

```text
[candidate] <artifact-id> thread_summary thread-a
```

Inspect one artifact:

```bash
mpi --db ./mpi.db queue inspect <artifact-id>
```

Approve one artifact:

```bash
mpi --db ./mpi.db queue approve <artifact-id> --notes "Looks correct from fixture metadata"
```

## Dry-Run Export

Export approved or corrected candidates into inspectable files:

```bash
mpi --db ./mpi.db export --output ./out
```

Expected shape:

```text
Dry-run export: 1 artifact(s) -> out
memory/thread-summaries/<artifact-id>.md
```

Production writes to wiki, `memory/`, and reminders are not enabled in v0.1.
Review the generated files and `out/export-manifest.json` before any future live
promotion work.
8 changes: 8 additions & 0 deletions src/mailplus_intelligence/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
"""MailPlus Intelligence runtime foundations."""

from importlib.metadata import PackageNotFoundError, version

from .fixtures import MetadataFixtureCorpus, load_metadata_fixture_corpus
from .runtime import RuntimeProfile, default_runtime_profile
from .schema import apply_all_migrations, apply_schema_v0, current_schema_version
from .sqlite import connect_sqlite
from .suppression import SUPPRESSION_FAMILIES, SuppressionDecision, classify_noise_suppression

try:
__version__ = version("mailplus-intelligence")
except PackageNotFoundError:
__version__ = "0.1.0+source"

__all__ = [
"MetadataFixtureCorpus",
"RuntimeProfile",
"SUPPRESSION_FAMILIES",
"SuppressionDecision",
"__version__",
"apply_all_migrations",
"apply_schema_v0",
"classify_noise_suppression",
Expand Down
120 changes: 105 additions & 15 deletions src/mailplus_intelligence/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import argparse
import json
import sqlite3
import sys
from pathlib import Path

Expand All @@ -21,6 +22,35 @@ def _setup_db(db_path: str):
return conn


def _warn_if_ephemeral_db(args: argparse.Namespace) -> None:
if getattr(args, "db", ":memory:") != ":memory:":
return
if getattr(args, "command", None) in {"search", "thread", "queue", "export", "sync", "seed"}:
print(
"warning: --db :memory: does not persist approvals, queue decisions, or sync state; use --db ./mpi.db.",
file=sys.stderr,
)


def _runtime_configuration_errors() -> tuple[type[BaseException], ...]:
errors: list[type[BaseException]] = []
try:
from .live_adapter import LiveAdapterNotConfigured

errors.append(LiveAdapterNotConfigured)
except ImportError:
pass

try:
from .llm_extractor import LLMNotAvailable # type: ignore[attr-defined]

errors.append(LLMNotAvailable)
except (ImportError, AttributeError):
pass

return tuple(errors)


# ── search ────────────────────────────────────────────────────────────────────

def cmd_search(args: argparse.Namespace) -> int:
Expand Down Expand Up @@ -156,6 +186,41 @@ def cmd_export(args: argparse.Namespace) -> int:
return 0


# ── seed ──────────────────────────────────────────────────────────────────────

def cmd_seed(args: argparse.Namespace) -> int:
from .extractor import extract_from_corpus
from .fixtures import load_metadata_fixture_corpus
from .queue import enqueue_candidate
from .sync import sync_from_fixture_corpus
from .threading import reconstruct_fixture_threads

conn = _setup_db(args.db)
try:
result = sync_from_fixture_corpus(conn, args.from_fixtures)
corpus = load_metadata_fixture_corpus(args.from_fixtures)
threads = reconstruct_fixture_threads(corpus.messages)
candidates = extract_from_corpus(threads, corpus.messages)

queued = 0
skipped = 0
for candidate in candidates:
try:
enqueue_candidate(conn, candidate.__dict__)
queued += 1
except sqlite3.IntegrityError:
skipped += 1
finally:
conn.close()

print(
"Seeded fixture corpus: "
f"inserted={result.inserted}, skipped={result.skipped}, "
f"queued={queued}, queue_skipped={skipped}."
)
return 0 if result.success else 1


# ── sync ─────────────────────────────────────────────────────────────────────


Expand Down Expand Up @@ -225,12 +290,15 @@ def cmd_doctor(args: argparse.Namespace) -> int:
# ── parser ────────────────────────────────────────────────────────────────────

def build_parser() -> argparse.ArgumentParser:
from . import __version__

parser = argparse.ArgumentParser(
prog="mpi",
description="MailPlus Intelligence operator CLI",
)
parser.add_argument("--db", default=":memory:", help="Path to SQLite database (default: :memory:)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
sub = parser.add_subparsers(dest="command")

# search
Expand Down Expand Up @@ -273,6 +341,15 @@ def build_parser() -> argparse.ArgumentParser:
ep = sub.add_parser("export", help="Dry-run export of approved candidates")
ep.add_argument("--output", default="./export-artifacts", help="Output directory")

# seed
seedp = sub.add_parser("seed", help="Seed a local DB from fixture metadata")
seedp.add_argument(
"--from-fixtures",
dest="from_fixtures",
default="fixtures/mailplus_metadata",
help="Fixture corpus directory",
)

# sync
syp = sub.add_parser("sync", help="Sync job status and checkpoint inspection")
sya = syp.add_subparsers(dest="sync_action")
Expand All @@ -291,22 +368,35 @@ def build_parser() -> argparse.ArgumentParser:
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
_warn_if_ephemeral_db(args)

if args.command == "search":
return cmd_search(args)
elif args.command == "thread":
return cmd_thread(args)
elif args.command == "queue":
return cmd_queue(args)
elif args.command == "export":
return cmd_export(args)
elif args.command == "doctor":
return cmd_doctor(args)
elif args.command == "sync":
return cmd_sync(args)
else:
parser.print_help()
return 1
try:
if args.command == "search":
return cmd_search(args)
elif args.command == "thread":
return cmd_thread(args)
elif args.command == "queue":
return cmd_queue(args)
elif args.command == "export":
return cmd_export(args)
elif args.command == "seed":
return cmd_seed(args)
elif args.command == "doctor":
return cmd_doctor(args)
elif args.command == "sync":
return cmd_sync(args)
else:
parser.print_help()
return 1
except FileNotFoundError as exc:
print(f"error: file not found: {exc}. Check the fixture path or database parent directory.", file=sys.stderr)
return 2
except sqlite3.OperationalError as exc:
print(f"error: sqlite operation failed: {exc}. Check that the database parent directory exists.", file=sys.stderr)
return 2
except _runtime_configuration_errors() as exc:
print(f"error: {exc}", file=sys.stderr)
return 2


if __name__ == "__main__":
Expand Down
Loading