memgit 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {memgit-0.3.0 → memgit-0.3.1}/PKG-INFO +3 -3
  2. {memgit-0.3.0 → memgit-0.3.1}/README.md +2 -2
  3. {memgit-0.3.0 → memgit-0.3.1}/memgit/__init__.py +1 -1
  4. {memgit-0.3.0 → memgit-0.3.1}/memgit/cli.py +51 -14
  5. memgit-0.3.1/memgit/gitdigest.py +158 -0
  6. {memgit-0.3.0 → memgit-0.3.1}/memgit/mcp_server.py +3 -1
  7. {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/PKG-INFO +3 -3
  8. {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/SOURCES.txt +1 -0
  9. {memgit-0.3.0 → memgit-0.3.1}/pyproject.toml +1 -1
  10. {memgit-0.3.0 → memgit-0.3.1}/tests/test_v030.py +79 -0
  11. {memgit-0.3.0 → memgit-0.3.1}/LICENSE +0 -0
  12. {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/__init__.py +0 -0
  13. {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/client.py +0 -0
  14. {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/commands.py +0 -0
  15. {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/crypto.py +0 -0
  16. {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/state.py +0 -0
  17. {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/sync.py +0 -0
  18. {memgit-0.3.0 → memgit-0.3.1}/memgit/graph.py +0 -0
  19. {memgit-0.3.0 → memgit-0.3.1}/memgit/http_server.py +0 -0
  20. {memgit-0.3.0 → memgit-0.3.1}/memgit/importer.py +0 -0
  21. {memgit-0.3.0 → memgit-0.3.1}/memgit/models.py +0 -0
  22. {memgit-0.3.0 → memgit-0.3.1}/memgit/repo.py +0 -0
  23. {memgit-0.3.0 → memgit-0.3.1}/memgit/scorer.py +0 -0
  24. {memgit-0.3.0 → memgit-0.3.1}/memgit/store.py +0 -0
  25. {memgit-0.3.0 → memgit-0.3.1}/memgit/tokens.py +0 -0
  26. {memgit-0.3.0 → memgit-0.3.1}/memgit/toon.py +0 -0
  27. {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/dependency_links.txt +0 -0
  28. {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/entry_points.txt +0 -0
  29. {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/requires.txt +0 -0
  30. {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/top_level.txt +0 -0
  31. {memgit-0.3.0 → memgit-0.3.1}/setup.cfg +0 -0
  32. {memgit-0.3.0 → memgit-0.3.1}/tests/test_advanced.py +0 -0
  33. {memgit-0.3.0 → memgit-0.3.1}/tests/test_setup.py +0 -0
  34. {memgit-0.3.0 → memgit-0.3.1}/tests/test_store_repo.py +0 -0
  35. {memgit-0.3.0 → memgit-0.3.1}/tests/test_toon.py +0 -0
  36. {memgit-0.3.0 → memgit-0.3.1}/tests/test_v020.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memgit
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Git for AI memory — version-controlled context persistence across Claude, GPT, Gemini, Cursor, Windsurf, and more
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://memgit.dev
@@ -193,10 +193,10 @@ Memory tools have a cold-start problem: install one halfway through a project an
193
193
 
194
194
  ```bash
195
195
  cd your-project
196
- memgit onboard # prints the bootstrap brief
196
+ memgit onboard # mines the repo, prints the bootstrap brief
197
197
  ```
198
198
 
199
- The brief tells your AI agent exactly what to do: read the README/docs/manifests and recent git history, extract 10–20 durable facts (purpose, architecture, conventions, current state, gotchas), save each as a typed memory, and checkpoint the seed set. Paste it into a session — or don't: if the AI searches memory in a project that has none, the MCP server itself replies with the bootstrap instructions instead of a bare "no results."
199
+ `onboard` first extracts a **repo digest** deterministically — git history (recent commit subjects, hot files/directories by churn, authors, branch, tags), detected stack from manifests, and the docs worth reading — using bounded, read-only probes that stay near-instant even on huge repositories. The brief then tells your AI agent exactly what to do with it: read only the listed files (no tree crawling), extract 10–20 durable facts (purpose, architecture, conventions, current state, gotchas), save each as a typed memory, and checkpoint the seed set. Paste it into a session — or don't: if the AI searches memory in a project that has none, the MCP server itself replies with the bootstrap instructions instead of a bare "no results."
200
200
 
201
201
  Memories are **project-scoped**: each carries the workspace it belongs to, searches boost the project you're standing in (global rules still surface), and the resume digest leads with *your current project's* recent work — not whatever repo you touched last night.
202
202
 
@@ -159,10 +159,10 @@ Memory tools have a cold-start problem: install one halfway through a project an
159
159
 
160
160
  ```bash
161
161
  cd your-project
162
- memgit onboard # prints the bootstrap brief
162
+ memgit onboard # mines the repo, prints the bootstrap brief
163
163
  ```
164
164
 
165
- The brief tells your AI agent exactly what to do: read the README/docs/manifests and recent git history, extract 10–20 durable facts (purpose, architecture, conventions, current state, gotchas), save each as a typed memory, and checkpoint the seed set. Paste it into a session — or don't: if the AI searches memory in a project that has none, the MCP server itself replies with the bootstrap instructions instead of a bare "no results."
165
+ `onboard` first extracts a **repo digest** deterministically — git history (recent commit subjects, hot files/directories by churn, authors, branch, tags), detected stack from manifests, and the docs worth reading — using bounded, read-only probes that stay near-instant even on huge repositories. The brief then tells your AI agent exactly what to do with it: read only the listed files (no tree crawling), extract 10–20 durable facts (purpose, architecture, conventions, current state, gotchas), save each as a typed memory, and checkpoint the seed set. Paste it into a session — or don't: if the AI searches memory in a project that has none, the MCP server itself replies with the bootstrap instructions instead of a bare "no results."
166
166
 
167
167
  Memories are **project-scoped**: each carries the workspace it belongs to, searches boost the project you're standing in (global rules still surface), and the resume digest leads with *your current project's* recent work — not whatever repo you touched last night.
168
168
 
@@ -1,3 +1,3 @@
1
1
  """memgit — git for AI memory."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
@@ -369,18 +369,12 @@ ONBOARD_BRIEF = """\
369
369
  This project has {count} memories in memgit{count_note}. A memory store that
370
370
  starts empty mid-project is useless until it is seeded — do that now, once,
371
371
  and every future session (in any AI tool) starts oriented.
372
-
372
+ {digest_section}
373
373
  ## Instructions for the AI operator
374
374
 
375
375
  Extract 10–20 DURABLE facts about this project and save each one as a memory
376
376
  (via the memgit MCP `save_memory` tool, or `memgit add` in a shell).
377
- Read, in this order, whatever exists:
378
-
379
- 1. README / CLAUDE.md / CONTRIBUTING / docs/ — purpose, architecture, setup
380
- 2. Package manifests (package.json, pyproject.toml, go.mod, …) — stack, entry points, scripts
381
- 3. `git log --oneline -30` and recent PRs — what is being worked on RIGHT NOW
382
- 4. Config/env samples, CI files — deploy targets, environments, gates
383
- 5. The code layout itself — modules, boundaries, naming conventions
377
+ {reading_plan}
384
378
 
385
379
  ## What to save (one memory each, not a dump)
386
380
 
@@ -403,28 +397,71 @@ Checkpoint the seed set so it is versioned from day one:
403
397
  Then verify: `memgit search "<something about this project>"` should hit.
404
398
  """
405
399
 
400
+ _READING_PLAN_WITH_DIGEST = """\
401
+ The repo digest above was extracted deterministically from git and the
402
+ filesystem — treat it as ground truth and do NOT re-derive it. On a large
403
+ repo, do NOT crawl the tree. Work only from:
404
+
405
+ 1. The "Read these first" files listed in the digest — purpose, architecture, setup
406
+ 2. The manifests listed — stack, entry points, scripts, dependencies
407
+ 3. The recent commit subjects + hot areas — what is being worked on RIGHT NOW
408
+ (turn these into the "current state / active work" memory)
409
+ 4. Config/env samples and CI files if present — deploy targets, environments, gates"""
410
+
411
+ _READING_PLAN_GENERIC = """\
412
+ Read, in this order, whatever exists:
413
+
414
+ 1. README / CLAUDE.md / CONTRIBUTING / docs/ — purpose, architecture, setup
415
+ 2. Package manifests (package.json, pyproject.toml, go.mod, …) — stack, entry points, scripts
416
+ 3. `git log --oneline -30` and recent PRs — what is being worked on RIGHT NOW
417
+ 4. Config/env samples, CI files — deploy targets, environments, gates
418
+ 5. The code layout itself — modules, boundaries, naming conventions"""
419
+
406
420
 
407
421
  @cli.command()
408
422
  @click.option('--project', '-P', default=None,
409
423
  help='Project label (default: derived from the current directory)')
410
424
  @click.option('--path', 'proj_path', default='.', type=click.Path(exists=True),
411
425
  help='Project directory to onboard (default: cwd)')
412
- def onboard(project, proj_path):
426
+ @click.option('--json', 'fmt_json', is_flag=True, help='Emit the raw repo digest as JSON')
427
+ def onboard(project, proj_path, fmt_json):
413
428
  """Print the bootstrap brief for adopting memgit on an existing project.
414
429
 
415
430
  memgit only knows what has been saved — a project adopted midway starts
416
- with zero context. This prints a step-by-step brief for an AI agent (or
417
- you) to seed the store from the codebase: README, docs, git history,
418
- conventions. Paste it into your AI session, or run
431
+ with zero context. This mines the repo's git history and filesystem
432
+ (bounded and read-only, fast even on huge repos) into a factual digest,
433
+ then prints a step-by-step brief for an AI agent (or you) to seed the
434
+ store from it. Paste it into your AI session, or run
419
435
  `memgit onboard | pbcopy`.
420
436
  """
437
+ from .gitdigest import build_digest, format_digest
421
438
  from .importer import project_label_from_path
422
439
 
423
440
  repo = _require_repo()
424
- label = project or project_label_from_path(Path(proj_path)) or Path(proj_path).resolve().name
441
+ target = Path(proj_path)
442
+ label = project or project_label_from_path(target) or target.resolve().name
443
+
444
+ digest = build_digest(target)
445
+ if fmt_json:
446
+ import json as _j
447
+ digest['project'] = label
448
+ print(_j.dumps(digest, indent=2))
449
+ return
450
+
425
451
  count = sum(1 for m in repo.list() if m.project == label)
426
452
  count_note = '' if count else ' — it is a blank slate for this project'
427
- print(ONBOARD_BRIEF.format(project=label, count=count, count_note=count_note))
453
+
454
+ rendered = format_digest(digest)
455
+ if rendered:
456
+ digest_section = ('\n## Repo digest (auto-extracted — trust it, don\'t re-derive)\n\n'
457
+ f'{rendered}\n')
458
+ reading_plan = _READING_PLAN_WITH_DIGEST
459
+ else:
460
+ digest_section = ''
461
+ reading_plan = _READING_PLAN_GENERIC
462
+
463
+ print(ONBOARD_BRIEF.format(project=label, count=count, count_note=count_note,
464
+ digest_section=digest_section, reading_plan=reading_plan))
428
465
 
429
466
 
430
467
  # ── log ───────────────────────────────────────────────────────────────────────
@@ -0,0 +1,158 @@
1
+ """Deterministic repo digest for onboarding — mine git + the filesystem
2
+ so the AI operator starts from extracted facts, not exploration.
3
+
4
+ Every probe is bounded (commit caps, timeouts, output clips) so this stays
5
+ fast and safe on very large repositories, and every probe is read-only.
6
+ A probe that fails or times out is simply omitted — never an error.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ import subprocess
11
+ from collections import Counter
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ # Bounds — chosen so the digest is near-instant even on monorepos.
16
+ _GIT_TIMEOUT = 5 # seconds per git call
17
+ _CHURN_COMMITS = 300 # commits sampled for hot-file analysis
18
+ _RECENT_SUBJECTS = 15 # recent commit subjects shown
19
+ _AUTHOR_SAMPLE = 500 # commits sampled for contributor list
20
+
21
+ # Well-known files worth reading during onboarding, in reading order.
22
+ _DOC_FILES = [
23
+ 'CLAUDE.md', 'README.md', 'README.rst', 'CONTRIBUTING.md',
24
+ 'ARCHITECTURE.md', 'AGENTS.md', 'CHANGELOG.md',
25
+ ]
26
+ _MANIFEST_FILES = [
27
+ 'package.json', 'pyproject.toml', 'setup.py', 'requirements.txt',
28
+ 'go.mod', 'Cargo.toml', 'pom.xml', 'build.gradle', 'Gemfile',
29
+ 'composer.json', 'mix.exs', 'Makefile', 'docker-compose.yml',
30
+ 'Dockerfile',
31
+ ]
32
+
33
+ _STACK_HINTS = {
34
+ 'package.json': 'Node.js/TypeScript',
35
+ 'pyproject.toml': 'Python',
36
+ 'setup.py': 'Python',
37
+ 'requirements.txt': 'Python',
38
+ 'go.mod': 'Go',
39
+ 'Cargo.toml': 'Rust',
40
+ 'pom.xml': 'Java (Maven)',
41
+ 'build.gradle': 'Java/Kotlin (Gradle)',
42
+ 'Gemfile': 'Ruby',
43
+ 'composer.json': 'PHP',
44
+ 'mix.exs': 'Elixir',
45
+ }
46
+
47
+
48
+ def _git(args: list[str], cwd: Path) -> Optional[str]:
49
+ """Run one read-only git command; None on any failure or timeout."""
50
+ try:
51
+ out = subprocess.run(
52
+ ['git', '--no-pager'] + args,
53
+ cwd=str(cwd), capture_output=True, text=True, timeout=_GIT_TIMEOUT,
54
+ )
55
+ return out.stdout.strip() if out.returncode == 0 else None
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def build_digest(path: Path) -> dict:
61
+ """Extract a bounded, factual digest of the repo at `path`.
62
+
63
+ Returns a dict; `has_git` False means only the filesystem probes ran.
64
+ """
65
+ path = path.resolve()
66
+ digest: dict = {'path': str(path), 'has_git': False}
67
+
68
+ # Filesystem probes (work with or without git)
69
+ docs = [f for f in _DOC_FILES if (path / f).is_file()]
70
+ manifests = [f for f in _MANIFEST_FILES if (path / f).is_file()]
71
+ stacks = sorted({_STACK_HINTS[m] for m in manifests if m in _STACK_HINTS})
72
+ digest['docs'] = docs
73
+ digest['manifests'] = manifests
74
+ digest['stacks'] = stacks
75
+ digest['has_docs_dir'] = (path / 'docs').is_dir()
76
+ digest['has_ci'] = (path / '.github' / 'workflows').is_dir() or (path / '.gitlab-ci.yml').is_file()
77
+
78
+ if _git(['rev-parse', '--is-inside-work-tree'], path) != 'true':
79
+ return digest
80
+ digest['has_git'] = True
81
+
82
+ digest['branch'] = _git(['branch', '--show-current'], path) or None
83
+ digest['last_commit_date'] = _git(['log', '-1', '--format=%as'], path)
84
+ count = _git(['rev-list', '--count', 'HEAD'], path)
85
+ digest['commit_count'] = int(count) if count and count.isdigit() else None
86
+ digest['latest_tag'] = _git(['describe', '--tags', '--abbrev=0'], path)
87
+
88
+ subjects = _git(['log', f'-{_RECENT_SUBJECTS}', '--no-merges', '--format=%s'], path)
89
+ digest['recent_subjects'] = (
90
+ [s.strip()[:100] for s in subjects.splitlines() if s.strip()] if subjects else []
91
+ )
92
+
93
+ authors = _git(['log', f'-{_AUTHOR_SAMPLE}', '--format=%an'], path)
94
+ if authors:
95
+ top = Counter(a.strip() for a in authors.splitlines() if a.strip())
96
+ digest['top_authors'] = [name for name, _ in top.most_common(5)]
97
+ else:
98
+ digest['top_authors'] = []
99
+
100
+ # Hot files/dirs: what actually changes is where the project lives.
101
+ churn = _git(
102
+ ['log', f'-{_CHURN_COMMITS}', '--no-merges', '--name-only', '--format='],
103
+ path,
104
+ )
105
+ if churn:
106
+ files = [f.strip() for f in churn.splitlines() if f.strip()]
107
+ file_counts = Counter(files)
108
+ dir_counts: Counter = Counter()
109
+ for f, n in file_counts.items():
110
+ top_level = f.split('/', 1)[0] if '/' in f else '(root)'
111
+ dir_counts[top_level] += n
112
+ digest['hot_files'] = [f for f, _ in file_counts.most_common(10)]
113
+ digest['hot_dirs'] = [d for d, _ in dir_counts.most_common(6)]
114
+ else:
115
+ digest['hot_files'] = []
116
+ digest['hot_dirs'] = []
117
+
118
+ return digest
119
+
120
+
121
+ def format_digest(digest: dict) -> str:
122
+ """Render the digest as a markdown section for the onboarding brief."""
123
+ lines: list[str] = []
124
+ if digest.get('has_git'):
125
+ facts = []
126
+ if digest.get('commit_count'):
127
+ facts.append(f"{digest['commit_count']} commits")
128
+ if digest.get('branch'):
129
+ facts.append(f"branch `{digest['branch']}`")
130
+ if digest.get('last_commit_date'):
131
+ facts.append(f"last commit {digest['last_commit_date']}")
132
+ if digest.get('latest_tag'):
133
+ facts.append(f"latest tag {digest['latest_tag']}")
134
+ if facts:
135
+ lines.append(f"- Git: {' · '.join(facts)}")
136
+ if digest.get('top_authors'):
137
+ lines.append(f"- Recent authors: {', '.join(digest['top_authors'])}")
138
+ if digest.get('stacks'):
139
+ lines.append(f"- Stack: {', '.join(digest['stacks'])} "
140
+ f"(manifests: {', '.join(digest['manifests'])})")
141
+ elif digest.get('manifests'):
142
+ lines.append(f"- Manifests: {', '.join(digest['manifests'])}")
143
+ read_first = list(digest.get('docs', []))
144
+ if digest.get('has_docs_dir'):
145
+ read_first.append('docs/')
146
+ if read_first:
147
+ lines.append(f"- Read these first: {', '.join(read_first)}")
148
+ if digest.get('hot_dirs'):
149
+ lines.append(f"- Hot areas (most-changed recently): {', '.join(digest['hot_dirs'])}")
150
+ if digest.get('hot_files'):
151
+ lines.append(f"- Most-changed files: {', '.join(digest['hot_files'][:6])}")
152
+ if digest.get('recent_subjects'):
153
+ lines.append('- Recent commit subjects (newest first):')
154
+ for s in digest['recent_subjects']:
155
+ lines.append(f' · {s}')
156
+ if digest.get('has_ci'):
157
+ lines.append('- CI config present (.github/workflows or .gitlab-ci.yml)')
158
+ return '\n'.join(lines)
@@ -403,7 +403,9 @@ def run_server(store_path: Path | None = None) -> None:
403
403
  "docs and `git log --oneline -30`, extract 10-20 durable facts "
404
404
  "(purpose, architecture, conventions, current state, gotchas), and "
405
405
  "save_memory each with a one-line rule + full body. "
406
- "`memgit onboard` prints the complete brief."
406
+ "Run `memgit onboard` first — it mines the repo's git history and "
407
+ "manifests into a factual digest plus the complete seeding brief, "
408
+ "so you read only what matters instead of crawling the tree."
407
409
  )
408
410
  return [TextContent(type="text", text=text)]
409
411
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memgit
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Git for AI memory — version-controlled context persistence across Claude, GPT, Gemini, Cursor, Windsurf, and more
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://memgit.dev
@@ -193,10 +193,10 @@ Memory tools have a cold-start problem: install one halfway through a project an
193
193
 
194
194
  ```bash
195
195
  cd your-project
196
- memgit onboard # prints the bootstrap brief
196
+ memgit onboard # mines the repo, prints the bootstrap brief
197
197
  ```
198
198
 
199
- The brief tells your AI agent exactly what to do: read the README/docs/manifests and recent git history, extract 10–20 durable facts (purpose, architecture, conventions, current state, gotchas), save each as a typed memory, and checkpoint the seed set. Paste it into a session — or don't: if the AI searches memory in a project that has none, the MCP server itself replies with the bootstrap instructions instead of a bare "no results."
199
+ `onboard` first extracts a **repo digest** deterministically — git history (recent commit subjects, hot files/directories by churn, authors, branch, tags), detected stack from manifests, and the docs worth reading — using bounded, read-only probes that stay near-instant even on huge repositories. The brief then tells your AI agent exactly what to do with it: read only the listed files (no tree crawling), extract 10–20 durable facts (purpose, architecture, conventions, current state, gotchas), save each as a typed memory, and checkpoint the seed set. Paste it into a session — or don't: if the AI searches memory in a project that has none, the MCP server itself replies with the bootstrap instructions instead of a bare "no results."
200
200
 
201
201
  Memories are **project-scoped**: each carries the workspace it belongs to, searches boost the project you're standing in (global rules still surface), and the resume digest leads with *your current project's* recent work — not whatever repo you touched last night.
202
202
 
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  memgit/__init__.py
5
5
  memgit/cli.py
6
+ memgit/gitdigest.py
6
7
  memgit/graph.py
7
8
  memgit/http_server.py
8
9
  memgit/importer.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memgit"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Git for AI memory — version-controlled context persistence across Claude, GPT, Gemini, Cursor, Windsurf, and more"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -281,6 +281,85 @@ class TestCli:
281
281
  assert msg == "sync: +1 (brand-new)"
282
282
 
283
283
 
284
+ # ── git digest (onboarding intelligence) ────────────────────────────────────
285
+
286
+ def _make_git_repo(root: Path):
287
+ import subprocess
288
+ def g(*args):
289
+ subprocess.run(["git"] + list(args), cwd=root, capture_output=True,
290
+ env={"GIT_AUTHOR_NAME": "Tester", "GIT_AUTHOR_EMAIL": "t@t",
291
+ "GIT_COMMITTER_NAME": "Tester", "GIT_COMMITTER_EMAIL": "t@t",
292
+ "PATH": os.environ["PATH"], "HOME": os.environ.get("HOME", "")})
293
+ g("init", "-q", "-b", "main")
294
+ (root / "README.md").write_text("# proj")
295
+ (root / "pyproject.toml").write_text("[project]\nname='x'")
296
+ (root / "src").mkdir()
297
+ (root / "src" / "app.py").write_text("print(1)")
298
+ g("add", "-A")
299
+ g("commit", "-qm", "initial scaffold")
300
+ (root / "src" / "app.py").write_text("print(2)")
301
+ g("add", "-A")
302
+ g("commit", "-qm", "fix: app output")
303
+
304
+
305
+ class TestGitDigest:
306
+ def test_digest_extracts_facts(self, tmp_path):
307
+ from memgit.gitdigest import build_digest
308
+ _make_git_repo(tmp_path)
309
+ d = build_digest(tmp_path)
310
+ assert d["has_git"] is True
311
+ assert d["branch"] == "main"
312
+ assert d["commit_count"] == 2
313
+ assert "fix: app output" in d["recent_subjects"]
314
+ assert "README.md" in d["docs"]
315
+ assert "Python" in d["stacks"]
316
+ assert "src" in d["hot_dirs"]
317
+
318
+ def test_digest_without_git(self, tmp_path):
319
+ from memgit.gitdigest import build_digest
320
+ (tmp_path / "README.md").write_text("# hi")
321
+ d = build_digest(tmp_path)
322
+ assert d["has_git"] is False
323
+ assert d["docs"] == ["README.md"]
324
+
325
+ def test_format_digest_renders(self, tmp_path):
326
+ from memgit.gitdigest import build_digest, format_digest
327
+ _make_git_repo(tmp_path)
328
+ out = format_digest(build_digest(tmp_path))
329
+ assert "Read these first: README.md" in out
330
+ assert "Stack: Python" in out
331
+ assert "fix: app output" in out
332
+
333
+ def test_onboard_embeds_digest(self, tmp_path, monkeypatch):
334
+ _make_git_repo(tmp_path)
335
+ monkeypatch.chdir(tmp_path)
336
+ Repository.init(tmp_path)
337
+ r = CliRunner()
338
+ res = r.invoke(cli, ["onboard", "--project", "MyProj"])
339
+ assert res.exit_code == 0
340
+ assert "Repo digest" in res.output
341
+ assert "trust it, don't re-derive" in res.output
342
+ assert "do NOT crawl the tree" in res.output
343
+
344
+ def test_onboard_json_digest(self, tmp_path, monkeypatch):
345
+ import json
346
+ _make_git_repo(tmp_path)
347
+ monkeypatch.chdir(tmp_path)
348
+ Repository.init(tmp_path)
349
+ r = CliRunner()
350
+ res = r.invoke(cli, ["onboard", "--project", "MyProj", "--json"])
351
+ d = json.loads(res.output)
352
+ assert d["project"] == "MyProj" and d["has_git"] is True
353
+
354
+ def test_onboard_no_git_falls_back_generic(self, tmp_path, monkeypatch):
355
+ monkeypatch.chdir(tmp_path)
356
+ Repository.init(tmp_path)
357
+ r = CliRunner()
358
+ res = r.invoke(cli, ["onboard", "--project", "MyProj"])
359
+ assert res.exit_code == 0
360
+ assert "git log --oneline -30" in res.output # generic reading plan
361
+
362
+
284
363
  # ── MCP project detection ────────────────────────────────────────────────────
285
364
 
286
365
  class TestMcpDetect:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes