sessionmemory 0.3.1__tar.gz → 0.4.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.
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/PKG-INFO +6 -3
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/README.md +5 -2
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/pyproject.toml +1 -1
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/pyproject.toml.orig +1 -1
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/new.py +45 -5
- sessionmemory-0.4.1/src/sessionmemory/lib/backlog.py +134 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/bootstrap.py +2 -1
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/config.py +5 -2
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/doctor.py +30 -2
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/field.py +6 -9
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/inject.py +7 -9
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/__init__.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/cli.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/__init__.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/_common.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/delete.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/doctor.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/export.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/init.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/inject.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/log.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/project.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/reindex.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/commands/search.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/__init__.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/atomic.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/embed.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/export.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/fieldindex.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/frontmatter.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/gitinfo.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/ids.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/log.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/paths.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/registry.py +0 -0
- {sessionmemory-0.3.1 → sessionmemory-0.4.1}/src/sessionmemory/lib/resolve.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sessionmemory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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>
|
|
@@ -215,8 +215,11 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
|
|
|
215
215
|
- Past sessions, one page each: `sessionmemory search "<words>" --logs`.
|
|
216
216
|
- Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
|
|
217
217
|
(feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
|
|
218
|
-
`- [
|
|
219
|
-
|
|
218
|
+
`- [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add one with
|
|
219
|
+
`sessionmemory new backlog --kind <kind> --size <S|M|L> --title "..." --topic <topic> --cwd .`,
|
|
220
|
+
which creates the file or heading when missing. Delete a finished line, and one
|
|
221
|
+
that will never be done, directly; never tick or annotate it. Git history is the
|
|
222
|
+
record of what was finished.
|
|
220
223
|
- Specs and plans: `sessionmemory new spec|plan --title "..." --cwd .` creates the file
|
|
221
224
|
and prints its path. Edit it directly after that.
|
|
222
225
|
- Learnings are captured at session end, not by you mid-session. When the user asks
|
|
@@ -200,8 +200,11 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
|
|
|
200
200
|
- Past sessions, one page each: `sessionmemory search "<words>" --logs`.
|
|
201
201
|
- Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
|
|
202
202
|
(feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
|
|
203
|
-
`- [
|
|
204
|
-
|
|
203
|
+
`- [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add one with
|
|
204
|
+
`sessionmemory new backlog --kind <kind> --size <S|M|L> --title "..." --topic <topic> --cwd .`,
|
|
205
|
+
which creates the file or heading when missing. Delete a finished line, and one
|
|
206
|
+
that will never be done, directly; never tick or annotate it. Git history is the
|
|
207
|
+
record of what was finished.
|
|
205
208
|
- Specs and plans: `sessionmemory new spec|plan --title "..." --cwd .` creates the file
|
|
206
209
|
and prints its path. Edit it directly after that.
|
|
207
210
|
- Learnings are captured at session end, not by you mid-session. When the user asks
|
|
@@ -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.
|
|
14
|
+
version = "0.4.1"
|
|
15
15
|
|
|
16
16
|
[[project.authors]]
|
|
17
17
|
name = "Nathaniel Landau"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
"""Commands that create a page or a
|
|
1
|
+
"""Commands that create a page, a document, or a backlog item and print what was written."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import enum
|
|
5
6
|
from pathlib import Path # noqa: TC003
|
|
6
7
|
|
|
7
8
|
import typer
|
|
@@ -14,10 +15,15 @@ from sessionmemory.commands._common import (
|
|
|
14
15
|
require_vault,
|
|
15
16
|
resolve_body,
|
|
16
17
|
)
|
|
17
|
-
from sessionmemory.lib import field, paths
|
|
18
|
-
from sessionmemory.lib.config import now
|
|
18
|
+
from sessionmemory.lib import backlog, field, paths
|
|
19
|
+
from sessionmemory.lib.config import now, today
|
|
19
20
|
|
|
20
|
-
app = typer.Typer(no_args_is_help=True, help="Create a learning, spec, or
|
|
21
|
+
app = typer.Typer(no_args_is_help=True, help="Create a learning, spec, plan, or backlog item.")
|
|
22
|
+
|
|
23
|
+
# Typer lists an Enum's members in --help and rejects anything else at parse time, so the
|
|
24
|
+
# allowed sets are spelled once, in lib/backlog, and mirrored here as choices.
|
|
25
|
+
Kind = enum.Enum("Kind", {kind: kind for kind in backlog.KINDS}, type=str)
|
|
26
|
+
Size = enum.Enum("Size", {size: size for size in backlog.SIZES}, type=str)
|
|
21
27
|
|
|
22
28
|
TITLE = typer.Option(..., "--title", help="The title.")
|
|
23
29
|
SUMMARY = typer.Option(..., "--summary", help="One sentence a search result shows.")
|
|
@@ -25,6 +31,10 @@ BODY = typer.Option("", "--body", help="Markdown body.")
|
|
|
25
31
|
BODY_FILE = typer.Option(None, "--body-file", help="Read the body from a file, or stdin for '-'.")
|
|
26
32
|
CWD = typer.Option(None, "--cwd", help="Directory to resolve the project from.")
|
|
27
33
|
JSON = typer.Option(False, "--json", help="Emit JSON instead of prose.") # noqa: FBT003
|
|
34
|
+
KIND = typer.Option(..., "--kind", help="The commit type heading the item goes under.")
|
|
35
|
+
SIZE = typer.Option(..., "--size", help="Effort: S, M, or L.")
|
|
36
|
+
DESCRIPTION = typer.Option(..., "--title", help="The imperative description of the work.")
|
|
37
|
+
TOPIC = typer.Option(None, "--topic", help="A topic tag, without the leading hash.")
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
def _report(path: Path, *, as_json: bool, uuid: str | None = None) -> None:
|
|
@@ -62,7 +72,7 @@ def new_learning(
|
|
|
62
72
|
|
|
63
73
|
def _new_document(folder: Path, title: str, body: str, *, as_json: bool) -> None:
|
|
64
74
|
try:
|
|
65
|
-
path = field.new_document(folder, title=title, body=body, now=now())
|
|
75
|
+
path = field.new_document(folder, title=title, body=body, now=now(), day=today())
|
|
66
76
|
except field.PageError as error:
|
|
67
77
|
fail(str(error))
|
|
68
78
|
_report(path, as_json=as_json)
|
|
@@ -100,3 +110,33 @@ def new_plan(
|
|
|
100
110
|
_new_document(
|
|
101
111
|
paths.plans_dir(vault, slug), title, resolve_body(body, body_file), as_json=as_json
|
|
102
112
|
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command("backlog", help="Add one open item to this project's backlog.md.")
|
|
116
|
+
def new_backlog(
|
|
117
|
+
kind: Kind = KIND,
|
|
118
|
+
size: Size = SIZE,
|
|
119
|
+
title: str = DESCRIPTION,
|
|
120
|
+
topic: str | None = TOPIC,
|
|
121
|
+
cwd: Path | None = CWD,
|
|
122
|
+
*,
|
|
123
|
+
as_json: bool = JSON,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Append an open item under its kind heading, creating the file or heading as needed.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
typer.Exit: The title or topic cannot be written as a well-formed line.
|
|
129
|
+
"""
|
|
130
|
+
vault = require_vault()
|
|
131
|
+
slug = require_project(vault, cwd)
|
|
132
|
+
path = paths.backlog_path(vault, slug)
|
|
133
|
+
try:
|
|
134
|
+
line = backlog.add_item(
|
|
135
|
+
path, kind=kind.value, size=size.value, description=title, topic=topic, today=today()
|
|
136
|
+
)
|
|
137
|
+
except backlog.BacklogError as error:
|
|
138
|
+
fail(str(error))
|
|
139
|
+
if as_json:
|
|
140
|
+
emit_json({"path": str(path), "line": line})
|
|
141
|
+
return
|
|
142
|
+
pp.success(f"added to {path.name}", details=[line, str(path)])
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Append an item to a project's backlog and recognize one.
|
|
2
|
+
|
|
3
|
+
This is the one place the item line's shape is known. A backlog line carries a kind that
|
|
4
|
+
must match a `## <kind>` heading, a size from a fixed set, a date, and a topic tag, and
|
|
5
|
+
the inject count, the doctor check, and the backlog skill all depend on that shape being
|
|
6
|
+
exact. The line has no checkbox: finished work is deleted, and git history is the
|
|
7
|
+
record of it, so there is nothing to tick. Deleting a line needs no validation and
|
|
8
|
+
stays a direct edit.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from sessionmemory.lib import atomic
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
KINDS = ("feat", "fix", "refactor", "perf", "docs", "test", "build", "ci")
|
|
22
|
+
SIZES = ("S", "M", "L")
|
|
23
|
+
HEADING = "# Backlog"
|
|
24
|
+
ITEM_PATTERN = re.compile(
|
|
25
|
+
rf"^- \[(?:{'|'.join(SIZES)})\] \S.* - \d{{4}}-\d{{2}}-\d{{2}}(?: \[#[^\s\]]+\])?$"
|
|
26
|
+
)
|
|
27
|
+
_SECTION_PREFIX = "## "
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BacklogError(ValueError):
|
|
31
|
+
"""An item field that cannot be written as a well-formed line."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def format_item(*, kind: str, size: str, description: str, topic: str | None, today: str) -> str:
|
|
35
|
+
"""Return the checklist line for one open item, validating every field.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
kind (str): One of `KINDS`; the heading the line belongs under.
|
|
39
|
+
size (str): One of `SIZES`.
|
|
40
|
+
description (str): The imperative description, a single line.
|
|
41
|
+
topic (str | None): The tag written as `[#topic]`, or None for no tag. A leading
|
|
42
|
+
`#` is accepted and dropped.
|
|
43
|
+
today (str): The date as `YYYY-MM-DD`.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: The line, without a trailing newline.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
BacklogError: A field is empty, spans lines, contains whitespace where a tag
|
|
50
|
+
cannot, or names an unknown kind or size.
|
|
51
|
+
"""
|
|
52
|
+
if kind not in KINDS:
|
|
53
|
+
msg = f"kind must be one of {', '.join(KINDS)}, not {kind!r}"
|
|
54
|
+
raise BacklogError(msg)
|
|
55
|
+
if size not in SIZES:
|
|
56
|
+
msg = f"size must be one of {', '.join(SIZES)}, not {size!r}"
|
|
57
|
+
raise BacklogError(msg)
|
|
58
|
+
description = description.strip()
|
|
59
|
+
if not description:
|
|
60
|
+
msg = "the description is empty"
|
|
61
|
+
raise BacklogError(msg)
|
|
62
|
+
if "\n" in description:
|
|
63
|
+
msg = "the description must be a single line"
|
|
64
|
+
raise BacklogError(msg)
|
|
65
|
+
line = f"- [{size}] {description} - {today}"
|
|
66
|
+
if topic is None:
|
|
67
|
+
return line
|
|
68
|
+
topic = topic.strip().removeprefix("#")
|
|
69
|
+
if not topic:
|
|
70
|
+
msg = "the topic is empty"
|
|
71
|
+
raise BacklogError(msg)
|
|
72
|
+
if any(character.isspace() for character in topic):
|
|
73
|
+
msg = "the topic cannot contain whitespace"
|
|
74
|
+
raise BacklogError(msg)
|
|
75
|
+
return f"{line} [#{topic}]"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_item(line: str) -> bool:
|
|
79
|
+
"""Return whether `line` is an open item, the only shape the session start counts.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
line (str): One line of `backlog.md`; leading whitespace is ignored.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
bool: True for `- [S|M|L] <description> - <YYYY-MM-DD>` with an optional
|
|
86
|
+
`[#topic]`, and False for anything else, a checkbox included.
|
|
87
|
+
"""
|
|
88
|
+
return ITEM_PATTERN.match(line.lstrip()) is not None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _insert(lines: list[str], kind: str, item: str) -> list[str]:
|
|
92
|
+
"""Place `item` at the end of the `## kind` section, or open that section last."""
|
|
93
|
+
heading = f"{_SECTION_PREFIX}{kind}"
|
|
94
|
+
start = next((i for i, line in enumerate(lines) if line.rstrip() == heading), None)
|
|
95
|
+
if start is None:
|
|
96
|
+
return [*lines, "", heading, "", item]
|
|
97
|
+
end = next(
|
|
98
|
+
(i for i in range(start + 1, len(lines)) if lines[i].startswith(_SECTION_PREFIX)),
|
|
99
|
+
len(lines),
|
|
100
|
+
)
|
|
101
|
+
last = end
|
|
102
|
+
while last > start + 1 and not lines[last - 1].strip():
|
|
103
|
+
last -= 1
|
|
104
|
+
if last == start + 1:
|
|
105
|
+
return [*lines[:last], "", item, *lines[last:]]
|
|
106
|
+
return [*lines[:last], item, *lines[last:]]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def add_item(
|
|
110
|
+
path: Path, *, kind: str, size: str, description: str, topic: str | None, today: str
|
|
111
|
+
) -> str:
|
|
112
|
+
"""Append one open item under its kind heading and return the line written.
|
|
113
|
+
|
|
114
|
+
A missing file starts with `# Backlog`. An existing file keeps whatever heads it,
|
|
115
|
+
since the `# Backlog` heading is never inserted into a file someone else structured.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
path (Path): The project's `backlog.md`.
|
|
119
|
+
kind (str): One of `KINDS`.
|
|
120
|
+
size (str): One of `SIZES`.
|
|
121
|
+
description (str): The imperative description.
|
|
122
|
+
topic (str | None): The tag, or None for none.
|
|
123
|
+
today (str): The date as `YYYY-MM-DD`.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
str: The line written, without a trailing newline.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
BacklogError: A field cannot be written as a well-formed line.
|
|
130
|
+
"""
|
|
131
|
+
item = format_item(kind=kind, size=size, description=description, topic=topic, today=today)
|
|
132
|
+
lines = path.read_text(encoding="utf-8").rstrip().splitlines() if path.is_file() else [HEADING]
|
|
133
|
+
atomic.write_text(path, "\n".join(_insert(lines, kind=kind, item=item)) + "\n")
|
|
134
|
+
return item
|
|
@@ -57,7 +57,8 @@ derived from the pages beside it, gitignored, and rebuilt by `sessionmemory rein
|
|
|
57
57
|
`_system/vault.toml` is the marker that tells the CLI this directory is a vault.
|
|
58
58
|
|
|
59
59
|
Pages are created with `sessionmemory new learning` and searched with
|
|
60
|
-
`sessionmemory search
|
|
60
|
+
`sessionmemory search`, and a backlog item is added with `sessionmemory new backlog`.
|
|
61
|
+
Everything else is an ordinary file you read and edit directly.
|
|
61
62
|
The format follows the memoryfield spec: https://github.com/calpaterson/memoryfield-spec
|
|
62
63
|
"""
|
|
63
64
|
|
|
@@ -83,12 +83,15 @@ def _configured_root() -> str | None:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
def today() -> str:
|
|
86
|
-
"""Return today's date as an ISO string.
|
|
86
|
+
"""Return today's local date as an ISO string.
|
|
87
|
+
|
|
88
|
+
A date a person reads, in a filename or a checklist line, follows their clock; only
|
|
89
|
+
the frontmatter timestamps from `now` are UTC, which the memoryfield spec asks for.
|
|
87
90
|
|
|
88
91
|
Returns:
|
|
89
92
|
str: Today's date.
|
|
90
93
|
"""
|
|
91
|
-
return datetime.datetime.now(
|
|
94
|
+
return datetime.datetime.now().astimezone().date().isoformat()
|
|
92
95
|
|
|
93
96
|
|
|
94
97
|
def now() -> str:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""The
|
|
1
|
+
"""The seven things that can be wrong with a vault, stated without severity.
|
|
2
2
|
|
|
3
3
|
Every check is a suggestion. The article's argument holds: an irrelevant or imperfect
|
|
4
4
|
page is never surfaced by semantic search, so nothing here fails a build. There is no
|
|
@@ -13,7 +13,7 @@ from dataclasses import dataclass
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
|
-
from sessionmemory.lib import field, fieldindex, paths, registry
|
|
16
|
+
from sessionmemory.lib import backlog, field, fieldindex, paths, registry
|
|
17
17
|
from sessionmemory.lib.frontmatter import (
|
|
18
18
|
FrontmatterError,
|
|
19
19
|
MissingFrontmatterError,
|
|
@@ -139,6 +139,33 @@ def dead_projects(vault: Path, _embedder: Embedder) -> list[Finding]:
|
|
|
139
139
|
]
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
_TICKED = "- [x]"
|
|
143
|
+
_SHAPE = "- [S|M|L] <description> - <YYYY-MM-DD> [#topic]"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def uncounted_backlog_lines(vault: Path, _embedder: Embedder) -> list[Finding]:
|
|
147
|
+
"""Report a top-level bullet in a backlog that the session start does not count.
|
|
148
|
+
|
|
149
|
+
A ticked line is finished work that should have been deleted; anything else is a
|
|
150
|
+
line that does not match the item shape. An indented bullet is a note under an
|
|
151
|
+
item and is left alone.
|
|
152
|
+
"""
|
|
153
|
+
findings = []
|
|
154
|
+
for slug in paths.iter_project_slugs(vault):
|
|
155
|
+
path = paths.backlog_path(vault, slug)
|
|
156
|
+
if not path.is_file():
|
|
157
|
+
continue
|
|
158
|
+
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
159
|
+
if not line.startswith("- ") or backlog.is_item(line):
|
|
160
|
+
continue
|
|
161
|
+
if line.startswith(_TICKED):
|
|
162
|
+
message = f"line {number}: ticked; finished work is deleted, not ticked"
|
|
163
|
+
else:
|
|
164
|
+
message = f"line {number}: not counted as open; the shape is `{_SHAPE}`"
|
|
165
|
+
findings.append(Finding("backlog", str(path), message))
|
|
166
|
+
return findings
|
|
167
|
+
|
|
168
|
+
|
|
142
169
|
def _is_stale(directory: Path, embedder: Embedder) -> str | None:
|
|
143
170
|
index = fieldindex.index_path(directory, embedder)
|
|
144
171
|
if not index.is_file():
|
|
@@ -177,6 +204,7 @@ CHECKS: tuple[Callable[[Path, Embedder], list[Finding]], ...] = (
|
|
|
177
204
|
malformed_frontmatter,
|
|
178
205
|
unquoted_datetimes,
|
|
179
206
|
dead_projects,
|
|
207
|
+
uncounted_backlog_lines,
|
|
180
208
|
stale_indexes,
|
|
181
209
|
)
|
|
182
210
|
|
|
@@ -179,21 +179,18 @@ def new_page(directory: Path, *, title: str, summary: str, body: str, now: str)
|
|
|
179
179
|
return _create(directory, meta, body, stem=None)
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def new_document(
|
|
183
|
-
|
|
184
|
-
) -> Path:
|
|
185
|
-
"""Create a spec, plan, or log: a titled, dated file that is not a memory page.
|
|
182
|
+
def new_document(directory: Path, *, title: str, body: str, now: str, day: str) -> Path:
|
|
183
|
+
"""Create a spec or plan: a titled, dated file that is not a memory page.
|
|
186
184
|
|
|
187
|
-
The filename leads with
|
|
188
|
-
|
|
185
|
+
The filename leads with `day`, so a directory listing reads in the order the
|
|
186
|
+
documents were written. `day` is passed rather than sliced from `now` because the
|
|
187
|
+
timestamp is UTC and the date a person reads in a filename follows their own clock.
|
|
189
188
|
|
|
190
189
|
Raises:
|
|
191
190
|
PageError: If the title yields no slug or no name is free.
|
|
192
191
|
"""
|
|
193
|
-
if stem is None:
|
|
194
|
-
stem = dated_stem(title, now[: len("2026-01-01")])
|
|
195
192
|
meta = {"title": title, "created": now, "updated": now}
|
|
196
|
-
return _create(directory, meta, body, stem=
|
|
193
|
+
return _create(directory, meta, body, stem=dated_stem(title, day))
|
|
197
194
|
|
|
198
195
|
|
|
199
196
|
def dated_stem(title: str, day: str) -> str:
|
|
@@ -10,12 +10,11 @@ from __future__ import annotations
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
from sessionmemory.lib import field, paths
|
|
13
|
+
from sessionmemory.lib import backlog, field, paths
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
-
OPEN_ITEM = "- [ ]"
|
|
19
18
|
_LEADING_PUNCTUATION = "`'\"([{<*_~"
|
|
20
19
|
|
|
21
20
|
|
|
@@ -43,11 +42,7 @@ def _titles(directory: Path) -> tuple[str, ...]:
|
|
|
43
42
|
def _open_backlog(path: Path) -> int:
|
|
44
43
|
if not path.is_file():
|
|
45
44
|
return 0
|
|
46
|
-
return sum(
|
|
47
|
-
1
|
|
48
|
-
for line in path.read_text(encoding="utf-8").splitlines()
|
|
49
|
-
if line.lstrip().startswith(OPEN_ITEM)
|
|
50
|
-
)
|
|
45
|
+
return sum(1 for line in path.read_text(encoding="utf-8").splitlines() if backlog.is_item(line))
|
|
51
46
|
|
|
52
47
|
|
|
53
48
|
def build(vault: Path, slug: str) -> Injection:
|
|
@@ -76,8 +71,11 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
|
|
|
76
71
|
- Past sessions, one page each: `{command} search "<words>" --logs`.
|
|
77
72
|
- Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
|
|
78
73
|
(feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
|
|
79
|
-
`- [
|
|
80
|
-
|
|
74
|
+
`- [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add one with
|
|
75
|
+
`{command} new backlog --kind <kind> --size <S|M|L> --title "..." --topic <topic> --cwd .`,
|
|
76
|
+
which creates the file or heading when missing. Delete a finished line, and one
|
|
77
|
+
that will never be done, directly; never tick or annotate it. Git history is the
|
|
78
|
+
record of what was finished.
|
|
81
79
|
- Specs and plans: `{command} new spec|plan --title "..." --cwd .` creates the file
|
|
82
80
|
and prints its path. Edit it directly after that.
|
|
83
81
|
- Learnings are captured at session end, not by you mid-session. When the user asks
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|