sessionmemory 0.4.1__tar.gz → 0.5.0__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 (37) hide show
  1. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/PKG-INFO +8 -6
  2. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/README.md +7 -5
  3. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/pyproject.toml +1 -1
  4. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/pyproject.toml.orig +1 -1
  5. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/cli.py +1 -1
  6. sessionmemory-0.5.0/src/sessionmemory/commands/reindex.py +67 -0
  7. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/doctor.py +5 -14
  8. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/inject.py +21 -12
  9. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/paths.py +10 -0
  10. sessionmemory-0.4.1/src/sessionmemory/commands/reindex.py +0 -46
  11. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/__init__.py +0 -0
  12. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/__init__.py +0 -0
  13. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/_common.py +0 -0
  14. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/delete.py +0 -0
  15. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/doctor.py +0 -0
  16. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/export.py +0 -0
  17. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/init.py +0 -0
  18. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/inject.py +0 -0
  19. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/log.py +0 -0
  20. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/new.py +0 -0
  21. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/project.py +0 -0
  22. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/commands/search.py +0 -0
  23. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/__init__.py +0 -0
  24. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/atomic.py +0 -0
  25. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/backlog.py +0 -0
  26. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/bootstrap.py +0 -0
  27. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/config.py +0 -0
  28. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/embed.py +0 -0
  29. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/export.py +0 -0
  30. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/field.py +0 -0
  31. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/fieldindex.py +0 -0
  32. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/frontmatter.py +0 -0
  33. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/gitinfo.py +0 -0
  34. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/ids.py +0 -0
  35. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/log.py +0 -0
  36. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/registry.py +0 -0
  37. {sessionmemory-0.4.1 → sessionmemory-0.5.0}/src/sessionmemory/lib/resolve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sessionmemory
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Durable memory for coding agents, one folder of searchable pages per project.
5
5
  Author: Nathaniel Landau
6
6
  Author-email: Nathaniel Landau <github@natelandau.com>
@@ -205,7 +205,7 @@ project holding four learnings, one spec, one plan, and two open backlog items:
205
205
  Durable memory for this project lives in a vault of markdown pages. Nothing below is
206
206
  loaded for you: the titles are what the vault holds, and each is one `sessionmemory search`
207
207
  away. The project's folder has `learnings/` and `logs/`, searched by meaning, beside
208
- `specs/`, `plans/`, and `backlog.md`, which are ordinary files you Read and Edit.
208
+ `specs/` and `backlog.md`, which are ordinary files you Read and Edit.
209
209
  `sessionmemory project --json` prints every path.
210
210
 
211
211
  - Before assuming nothing was written down, search: `sessionmemory search "<words>"`
@@ -220,8 +220,11 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
220
220
  which creates the file or heading when missing. Delete a finished line, and one
221
221
  that will never be done, directly; never tick or annotate it. Git history is the
222
222
  record of what was finished.
223
- - Specs and plans: `sessionmemory new spec|plan --title "..." --cwd .` creates the file
224
- and prints its path. Edit it directly after that.
223
+ - Specs: `specs/` holds one design record per feature, named `<date>-<topic>.md`
224
+ and kept after the work ships. Before designing or changing a feature, list the
225
+ directory and read any spec whose name matches, so a decision already made is not
226
+ made again. `sessionmemory new spec --title "..." --cwd .` creates one and prints its
227
+ path. Edit it directly after that.
225
228
  - Learnings are captured at session end, not by you mid-session. When the user asks
226
229
  to keep one now: `sessionmemory new learning --title "..." --summary "..." --cwd .`
227
230
  creates the page and prints the path to write prose into. Title and summary state
@@ -237,8 +240,7 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
237
240
  ## Open work
238
241
 
239
242
  2 open backlog items
240
- spec: Export invoices as UBL 2.1 XML
241
- plan: Move PDF rendering to a worker queue
243
+ 1 spec in specs/
242
244
  ```
243
245
 
244
246
  A page body never enters that block, so its cost grows with the number of pages and not
@@ -190,7 +190,7 @@ project holding four learnings, one spec, one plan, and two open backlog items:
190
190
  Durable memory for this project lives in a vault of markdown pages. Nothing below is
191
191
  loaded for you: the titles are what the vault holds, and each is one `sessionmemory search`
192
192
  away. The project's folder has `learnings/` and `logs/`, searched by meaning, beside
193
- `specs/`, `plans/`, and `backlog.md`, which are ordinary files you Read and Edit.
193
+ `specs/` and `backlog.md`, which are ordinary files you Read and Edit.
194
194
  `sessionmemory project --json` prints every path.
195
195
 
196
196
  - Before assuming nothing was written down, search: `sessionmemory search "<words>"`
@@ -205,8 +205,11 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
205
205
  which creates the file or heading when missing. Delete a finished line, and one
206
206
  that will never be done, directly; never tick or annotate it. Git history is the
207
207
  record of what was finished.
208
- - Specs and plans: `sessionmemory new spec|plan --title "..." --cwd .` creates the file
209
- and prints its path. Edit it directly after that.
208
+ - Specs: `specs/` holds one design record per feature, named `<date>-<topic>.md`
209
+ and kept after the work ships. Before designing or changing a feature, list the
210
+ directory and read any spec whose name matches, so a decision already made is not
211
+ made again. `sessionmemory new spec --title "..." --cwd .` creates one and prints its
212
+ path. Edit it directly after that.
210
213
  - Learnings are captured at session end, not by you mid-session. When the user asks
211
214
  to keep one now: `sessionmemory new learning --title "..." --summary "..." --cwd .`
212
215
  creates the page and prints the path to write prose into. Title and summary state
@@ -222,8 +225,7 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
222
225
  ## Open work
223
226
 
224
227
  2 open backlog items
225
- spec: Export invoices as UBL 2.1 XML
226
- plan: Move PDF rendering to a worker queue
228
+ 1 spec in specs/
227
229
  ```
228
230
 
229
231
  A page body never enters that block, so its cost grows with the number of pages and not
@@ -11,7 +11,7 @@ description = "Durable memory for coding agents, one folder of searchable pages
11
11
  name = "sessionmemory"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.13,<3.15"
14
- version = "0.4.1"
14
+ version = "0.5.0"
15
15
 
16
16
  [[project.authors]]
17
17
  name = "Nathaniel Landau"
@@ -12,7 +12,7 @@
12
12
  name = "sessionmemory"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.13,<3.15"
15
- version = "0.4.1"
15
+ version = "0.5.0"
16
16
 
17
17
  [project.scripts]
18
18
  sessionmemory = "sessionmemory.cli:main"
@@ -65,7 +65,7 @@ app.command("log", help="Record this session's work in one upserted page.")(
65
65
  app.command("project", help="Report this directory's project, or register it.")(
66
66
  project.project_command
67
67
  )
68
- app.command("reindex", help="Rebuild this project's search indexes.")(
68
+ app.command("reindex", help="Rebuild this project's search indexes, or every project's.")(
69
69
  reindex_commands.reindex_command
70
70
  )
71
71
  app.command("search", help="Search this project's learnings by meaning, or its logs with --logs.")(
@@ -0,0 +1,67 @@
1
+ """The `reindex` command: rebuild a project's field indexes from its pages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict
6
+ from pathlib import Path # noqa: TC003
7
+
8
+ import typer
9
+ from nclutils import pp
10
+
11
+ from sessionmemory.commands._common import (
12
+ build_embedder,
13
+ emit_json,
14
+ fail,
15
+ require_project,
16
+ require_vault,
17
+ )
18
+ from sessionmemory.lib import fieldindex, paths
19
+
20
+ CWD = typer.Option(None, "--cwd", help="Directory to resolve the project from.")
21
+ ALL = typer.Option(False, "--all", help="Reindex every project in the vault.") # noqa: FBT003
22
+ JSON = typer.Option(False, "--json", help="Emit JSON instead of prose.") # noqa: FBT003
23
+
24
+
25
+ def reindex_command(
26
+ cwd: Path | None = CWD, *, all_projects: bool = ALL, as_json: bool = JSON
27
+ ) -> None:
28
+ """Bring this project's learnings and logs indexes up to date, or every project's.
29
+
30
+ Raises:
31
+ Exit: When `--all` is combined with `--cwd`.
32
+ """
33
+ vault = require_vault()
34
+ if all_projects:
35
+ if cwd is not None:
36
+ fail("--all and --cwd are mutually exclusive", ["pass one or the other"])
37
+ fields = [
38
+ (directory.parent.name, directory.name, directory)
39
+ for directory in paths.iter_field_dirs(vault)
40
+ ]
41
+ else:
42
+ slug = require_project(vault, cwd)
43
+ fields = [
44
+ (slug, paths.LEARNINGS_DIR, paths.learnings_dir(vault, slug)),
45
+ (slug, paths.LOGS_DIR, paths.logs_dir(vault, slug)),
46
+ ]
47
+ embedder = build_embedder()
48
+
49
+ if as_json:
50
+ payload: dict[str, dict] = {}
51
+ for slug, name, directory in fields:
52
+ counts = asdict(fieldindex.refresh(directory, embedder))
53
+ if all_projects:
54
+ payload.setdefault(slug, {})[name] = counts
55
+ else:
56
+ payload[name] = counts
57
+ emit_json(payload)
58
+ return
59
+
60
+ with pp.step("reindexing") as step:
61
+ for slug, name, directory in fields:
62
+ result = fieldindex.refresh(directory, embedder)
63
+ label = f"{slug}/{name}" if all_projects else name
64
+ step.sub(
65
+ f"{label}: {result.added} added, {result.updated} updated, "
66
+ f"{result.removed} removed, {result.unchanged} unchanged"
67
+ )
@@ -36,20 +36,11 @@ class Finding:
36
36
  message: str
37
37
 
38
38
 
39
- def _fields(vault: Path) -> list[Path]:
40
- return [
41
- paths.project_dir(vault, slug) / name
42
- for slug in paths.iter_project_slugs(vault)
43
- for name in paths.FIELD_DIRS
44
- if (paths.project_dir(vault, slug) / name).is_dir()
45
- ]
46
-
47
-
48
39
  def nonconformant_names(vault: Path, _embedder: Embedder) -> list[Finding]:
49
40
  """Report a markdown file in a field whose name breaks the spec's filename rule."""
50
41
  return [
51
42
  Finding("filename", str(path), "not lowercase ascii letters, digits, and hyphens")
52
- for directory in _fields(vault)
43
+ for directory in paths.iter_field_dirs(vault)
53
44
  for path in sorted(directory.glob("*.md"))
54
45
  if path.is_file() and not field.is_debris(path.name) and not field.is_page_name(path.name)
55
46
  ]
@@ -63,7 +54,7 @@ def oversized_pages(vault: Path, _embedder: Embedder) -> list[Finding]:
63
54
  str(path),
64
55
  f"{path.stat().st_size} bytes; split it, the limit is {field.PAGE_LIMIT}",
65
56
  )
66
- for directory in _fields(vault)
57
+ for directory in paths.iter_field_dirs(vault)
67
58
  for path in field.iter_pages(directory)
68
59
  if path.stat().st_size > field.PAGE_LIMIT
69
60
  ]
@@ -82,7 +73,7 @@ def malformed_frontmatter(vault: Path, _embedder: Embedder) -> list[Finding]:
82
73
  when it has no block at all.
83
74
  """
84
75
  findings = []
85
- for directory in _fields(vault):
76
+ for directory in paths.iter_field_dirs(vault):
86
77
  for path in field.iter_pages(directory):
87
78
  raw = path.read_bytes()
88
79
  try:
@@ -114,7 +105,7 @@ def unquoted_datetimes(vault: Path, _embedder: Embedder) -> list[Finding]:
114
105
  block that cannot be parsed at all is left to the frontmatter check.
115
106
  """
116
107
  findings = []
117
- for directory in _fields(vault):
108
+ for directory in paths.iter_field_dirs(vault):
118
109
  for path in field.iter_pages(directory):
119
110
  try:
120
111
  keys = unquoted_datetime_keys(path.read_text(encoding="utf-8"))
@@ -191,7 +182,7 @@ def _is_stale(directory: Path, embedder: Embedder) -> str | None:
191
182
  def stale_indexes(vault: Path, embedder: Embedder) -> list[Finding]:
192
183
  """Report a field whose index file is unreadable or behind its pages."""
193
184
  findings = []
194
- for directory in _fields(vault):
185
+ for directory in paths.iter_field_dirs(vault):
195
186
  message = _is_stale(directory, embedder)
196
187
  if message:
197
188
  findings.append(Finding("index", str(directory), message))
@@ -25,8 +25,7 @@ class Injection:
25
25
  project: str
26
26
  titles: tuple[str, ...]
27
27
  open_backlog: int
28
- specs: tuple[str, ...]
29
- plans: tuple[str, ...]
28
+ specs: int
30
29
 
31
30
 
32
31
  def _sort_key(title: str) -> str:
@@ -45,14 +44,22 @@ def _open_backlog(path: Path) -> int:
45
44
  return sum(1 for line in path.read_text(encoding="utf-8").splitlines() if backlog.is_item(line))
46
45
 
47
46
 
47
+ def _spec_count(directory: Path) -> int:
48
+ return sum(1 for _ in field.iter_pages(directory))
49
+
50
+
48
51
  def build(vault: Path, slug: str) -> Injection:
49
- """Read the project's pages and files; the index is never consulted here."""
52
+ """Read the project's pages and files; the index is never consulted here.
53
+
54
+ Specs are counted rather than listed. A spec outlives the work it describes, so a
55
+ list of them is a changelog rather than open work, and it grows without bound. The
56
+ count says whether listing `specs/` is worth a call, which is all a session needs.
57
+ """
50
58
  return Injection(
51
59
  project=slug,
52
60
  titles=_titles(paths.learnings_dir(vault, slug)),
53
61
  open_backlog=_open_backlog(paths.backlog_path(vault, slug)),
54
- specs=_titles(paths.specs_dir(vault, slug)),
55
- plans=_titles(paths.plans_dir(vault, slug)),
62
+ specs=_spec_count(paths.specs_dir(vault, slug)),
56
63
  )
57
64
 
58
65
 
@@ -61,7 +68,7 @@ GUIDANCE = """## Using this vault
61
68
  Durable memory for this project lives in a vault of markdown pages. Nothing below is
62
69
  loaded for you: the titles are what the vault holds, and each is one `{command} search`
63
70
  away. The project's folder has `learnings/` and `logs/`, searched by meaning, beside
64
- `specs/`, `plans/`, and `backlog.md`, which are ordinary files you Read and Edit.
71
+ `specs/` and `backlog.md`, which are ordinary files you Read and Edit.
65
72
  `{command} project --json` prints every path.
66
73
 
67
74
  - Before assuming nothing was written down, search: `{command} search "<words>"`
@@ -76,8 +83,11 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
76
83
  which creates the file or heading when missing. Delete a finished line, and one
77
84
  that will never be done, directly; never tick or annotate it. Git history is the
78
85
  record of what was finished.
79
- - Specs and plans: `{command} new spec|plan --title "..." --cwd .` creates the file
80
- and prints its path. Edit it directly after that.
86
+ - Specs: `specs/` holds one design record per feature, named `<date>-<topic>.md`
87
+ and kept after the work ships. Before designing or changing a feature, list the
88
+ directory and read any spec whose name matches, so a decision already made is not
89
+ made again. `{command} new spec --title "..." --cwd .` creates one and prints its
90
+ path. Edit it directly after that.
81
91
  - Learnings are captured at session end, not by you mid-session. When the user asks
82
92
  to keep one now: `{command} new learning --title "..." --summary "..." --cwd .`
83
93
  creates the page and prints the path to write prose into. Title and summary state
@@ -93,8 +103,8 @@ def render(injection: Injection, *, command: str = "sessionmemory") -> str:
93
103
  lines.extend(["", "## Open work", ""])
94
104
  item = "item" if injection.open_backlog == 1 else "items"
95
105
  lines.append(f" {injection.open_backlog} open backlog {item}")
96
- lines.extend(f" spec: {title}" for title in injection.specs)
97
- lines.extend(f" plan: {title}" for title in injection.plans)
106
+ spec = "spec" if injection.specs == 1 else "specs"
107
+ lines.append(f" {injection.specs} {spec} in specs/")
98
108
  return "\n".join(lines)
99
109
 
100
110
 
@@ -105,6 +115,5 @@ def payload(injection: Injection, *, command: str) -> dict[str, object]:
105
115
  "project": injection.project,
106
116
  "titles": list(injection.titles),
107
117
  "open_backlog": injection.open_backlog,
108
- "specs": list(injection.specs),
109
- "plans": list(injection.plans),
118
+ "specs": injection.specs,
110
119
  }
@@ -75,3 +75,13 @@ def iter_project_slugs(vault: Path) -> list[str]:
75
75
  if not root.is_dir():
76
76
  return []
77
77
  return sorted(entry.name for entry in root.iterdir() if entry.is_dir())
78
+
79
+
80
+ def iter_field_dirs(vault: Path) -> list[Path]:
81
+ """Return every existing field directory in the vault, sorted by project then field."""
82
+ return [
83
+ project_dir(vault, slug) / name
84
+ for slug in iter_project_slugs(vault)
85
+ for name in FIELD_DIRS
86
+ if (project_dir(vault, slug) / name).is_dir()
87
+ ]
@@ -1,46 +0,0 @@
1
- """The `reindex` command: rebuild a project's field indexes from its pages."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import asdict
6
- from pathlib import Path # noqa: TC003
7
-
8
- import typer
9
- from nclutils import pp
10
-
11
- from sessionmemory.commands._common import (
12
- build_embedder,
13
- emit_json,
14
- require_project,
15
- require_vault,
16
- )
17
- from sessionmemory.lib import fieldindex, paths
18
-
19
- CWD = typer.Option(None, "--cwd", help="Directory to resolve the project from.")
20
- JSON = typer.Option(False, "--json", help="Emit JSON instead of prose.") # noqa: FBT003
21
-
22
-
23
- def reindex_command(cwd: Path | None = CWD, *, as_json: bool = JSON) -> None:
24
- """Bring this project's learnings and logs indexes up to date."""
25
- vault = require_vault()
26
- slug = require_project(vault, cwd)
27
- embedder = build_embedder()
28
- fields = {
29
- "learnings": paths.learnings_dir(vault, slug),
30
- "logs": paths.logs_dir(vault, slug),
31
- }
32
- if as_json:
33
- emit_json(
34
- {
35
- name: asdict(fieldindex.refresh(directory, embedder))
36
- for name, directory in fields.items()
37
- }
38
- )
39
- return
40
- with pp.step("reindexing") as step:
41
- for name, directory in fields.items():
42
- result = fieldindex.refresh(directory, embedder)
43
- step.sub(
44
- f"{name}: {result.added} added, {result.updated} updated, "
45
- f"{result.removed} removed, {result.unchanged} unchanged"
46
- )