qhaway 0.1.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.
- qhaway-0.1.0/PKG-INFO +167 -0
- qhaway-0.1.0/README.md +151 -0
- qhaway-0.1.0/pyproject.toml +34 -0
- qhaway-0.1.0/src/qhaway/__init__.py +6 -0
- qhaway-0.1.0/src/qhaway/cli.py +238 -0
- qhaway-0.1.0/src/qhaway/model.py +241 -0
- qhaway-0.1.0/src/qhaway/parse.py +147 -0
- qhaway-0.1.0/src/qhaway/project.py +302 -0
- qhaway-0.1.0/src/qhaway/reconcile.py +240 -0
- qhaway-0.1.0/src/qhaway/server.py +123 -0
qhaway-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qhaway
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A truncation-proof projection of a Markdown memory index: regenerate MEMORY.md to fit the loader's budget and declare what it sets aside, instead of being silently cut.
|
|
5
|
+
Keywords: memory,index,markdown,llm,agent,sqlite,mcp
|
|
6
|
+
Author: Tony
|
|
7
|
+
Author-email: Tony <fsgeek@cs.ubc.ca>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Utilities
|
|
12
|
+
Requires-Dist: mcp[cli]>=1.28.0
|
|
13
|
+
Requires-Dist: pyyaml
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# qhaway
|
|
18
|
+
|
|
19
|
+
*Quechua: "to see / to watch over."* The name states the cure — make the whole
|
|
20
|
+
memory record **visible** instead of silently truncated.
|
|
21
|
+
|
|
22
|
+
`qhaway` keeps a Markdown memory index from being silently cut off when it grows
|
|
23
|
+
past the size limit of the system that loads it.
|
|
24
|
+
|
|
25
|
+
## The problem
|
|
26
|
+
|
|
27
|
+
Some agents and tools maintain memory as a directory of small Markdown files plus
|
|
28
|
+
a single curated index (`MEMORY.md`) that points at them. The index is loaded into
|
|
29
|
+
context on startup so the agent boots with a map of what it knows.
|
|
30
|
+
|
|
31
|
+
That index grows. When it grows past the loader's size limit, it is **silently
|
|
32
|
+
truncated** — cut off with no error raised. The agent boots a *partial self* and
|
|
33
|
+
doesn't know it: everything past the cut is invisible, and a pointer to a file
|
|
34
|
+
that no longer exists rides along just as silently. The honest record is there on
|
|
35
|
+
disk; the loaded view of it is a lie of omission.
|
|
36
|
+
|
|
37
|
+
This was observed live: a 36.8KB / 137-entry index against a ~24.4KB load limit,
|
|
38
|
+
with the entire latest section — including the pointer to the most recent state —
|
|
39
|
+
falling past the cut.
|
|
40
|
+
|
|
41
|
+
## The fix
|
|
42
|
+
|
|
43
|
+
qhaway regenerates `MEMORY.md` itself as a **truncation-proof projection** of the
|
|
44
|
+
memory files:
|
|
45
|
+
|
|
46
|
+
- **Files stay the write surface.** You keep writing topic `.md` files exactly as
|
|
47
|
+
you do today. There is no schema to learn and no "save" API to call. qhaway only
|
|
48
|
+
changes *who writes the index* — a machine, not a hand.
|
|
49
|
+
- **It fits the budget.** The regenerated index is guaranteed to come in under the
|
|
50
|
+
loader's limit, so it is never silently cut.
|
|
51
|
+
- **No silent loss — ever.** When the index can't fit everything, it doesn't drop
|
|
52
|
+
entries quietly. It **declares the omission**:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
+47 project memories not shown — run: qhaway index --type project
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Truncation becomes *visible selection*. You always know what was set aside and
|
|
59
|
+
how to see it.
|
|
60
|
+
|
|
61
|
+
The loader keeps reading `MEMORY.md` exactly as before — now complete-for-what-it-
|
|
62
|
+
claims and guaranteed under budget. Nothing downstream changes.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
uv tool install qhaway
|
|
68
|
+
# or
|
|
69
|
+
pipx install qhaway
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Embedded and zero-infra: it uses stdlib SQLite (WAL mode) as a single local
|
|
73
|
+
file. No server, no database to provision, no credentials.
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
# Regenerate MEMORY.md from the memory directory (the main command)
|
|
79
|
+
qhaway index
|
|
80
|
+
|
|
81
|
+
# See a specific slice — including entries the default index declared as omitted
|
|
82
|
+
qhaway index --type project
|
|
83
|
+
qhaway index --role <role>
|
|
84
|
+
qhaway index --status superseded
|
|
85
|
+
|
|
86
|
+
# Set a custom budget
|
|
87
|
+
qhaway index --budget <bytes>
|
|
88
|
+
|
|
89
|
+
# Inspect without writing: would it overflow? any broken links? any leftover files?
|
|
90
|
+
qhaway index --check
|
|
91
|
+
|
|
92
|
+
# Print the projection without writing the file
|
|
93
|
+
qhaway index --dry-run
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
To record a memory: **write a topic `.md` file, then run `qhaway index`.** Don't
|
|
97
|
+
hand-edit `MEMORY.md` — it is fully derived, and any hand edit is preserved (see
|
|
98
|
+
below) but won't survive into the index unless it lives in a topic file.
|
|
99
|
+
|
|
100
|
+
## MCP spine (remember / recall)
|
|
101
|
+
|
|
102
|
+
The spine lets a Claude Code instance reach its memory through MCP tools instead
|
|
103
|
+
of hand-writing files. `MEMORY.md` becomes a managed, read-only **redirect** into
|
|
104
|
+
the SQLite-derived index; the topic files stay the source of truth.
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
# Run the MCP server over a memory directory (reconciles once at startup)
|
|
108
|
+
qhaway serve --dir <memory_dir>
|
|
109
|
+
|
|
110
|
+
# Sync the index from the files (alias: qhaway index)
|
|
111
|
+
qhaway reconcile --dir <memory_dir>
|
|
112
|
+
|
|
113
|
+
# Inspect: broken wikilinks, orphan backups, low topic count, would-overflow
|
|
114
|
+
qhaway check --dir <memory_dir>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Two verbs are exposed to the model:
|
|
118
|
+
|
|
119
|
+
- `recall(type?, role?, status?)` — pure read; returns the budgeted projection
|
|
120
|
+
(omit args for the working set).
|
|
121
|
+
- `remember(type, title, body, description?, links?)` — writes a topic file then
|
|
122
|
+
reconciles. Files stay truth; the DB is a derived, rebuildable view.
|
|
123
|
+
|
|
124
|
+
`MEMORY.md` is written born-read-only (`0o444`) as a friction signal — not a hard
|
|
125
|
+
barrier — so the reflexive hand-edit is deflected toward the tools. qhaway's own
|
|
126
|
+
writer updates it via atomic temp-file + replace.
|
|
127
|
+
|
|
128
|
+
## How it works
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
qhaway index
|
|
132
|
+
→ scan the memory directory
|
|
133
|
+
→ parse each file into a node (frontmatter type, filename role, links, body)
|
|
134
|
+
→ build an index of nodes + links in SQLite
|
|
135
|
+
→ project the working set under the byte budget,
|
|
136
|
+
appending a declared-omissions footer for anything set aside
|
|
137
|
+
→ write MEMORY.md
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The memory files are the single source of truth. The index is rebuilt from scratch
|
|
141
|
+
on every run, so it can never drift from the files. The same files always produce
|
|
142
|
+
a byte-identical index.
|
|
143
|
+
|
|
144
|
+
### What's preserved
|
|
145
|
+
|
|
146
|
+
`MEMORY.md` is fully machine-derived — there are no hand-maintained regions. If
|
|
147
|
+
qhaway ever finds that the index was edited by hand since it last wrote it, it does
|
|
148
|
+
**not** overwrite the edit: it renames the existing file to a timestamped
|
|
149
|
+
`MEMORY-<timestamp>.md` and writes a fresh index. Your edit is preserved verbatim;
|
|
150
|
+
the index rebuilds from the files. Nothing is interpreted, merged, or lost.
|
|
151
|
+
|
|
152
|
+
## Design philosophy
|
|
153
|
+
|
|
154
|
+
One pain, fixed completely: **truncation**. Full-text search, deep audit, write
|
|
155
|
+
tooling, and ranking sophistication are deliberately *not* in this version — each
|
|
156
|
+
is a real later idea, none is this version's job.
|
|
157
|
+
|
|
158
|
+
The wager is simple: a structured index built *over* an existing pile of files —
|
|
159
|
+
without replacing the pile — makes the whole thing measurably work better. The
|
|
160
|
+
proof is use. If it removes felt pain for skeptical users who'll drop it the moment
|
|
161
|
+
it's more friction than value, it ships; if it removes the same pain for strangers
|
|
162
|
+
feeling the same sprawl, it spreads. Propagation is the measurement.
|
|
163
|
+
|
|
164
|
+
## Status
|
|
165
|
+
|
|
166
|
+
Early (`v0.1.0`). The design is specified in
|
|
167
|
+
[`docs/superpowers/specs/2026-06-20-qhaway-mvp-design.md`](docs/superpowers/specs/2026-06-20-qhaway-mvp-design.md).
|
qhaway-0.1.0/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# qhaway
|
|
2
|
+
|
|
3
|
+
*Quechua: "to see / to watch over."* The name states the cure — make the whole
|
|
4
|
+
memory record **visible** instead of silently truncated.
|
|
5
|
+
|
|
6
|
+
`qhaway` keeps a Markdown memory index from being silently cut off when it grows
|
|
7
|
+
past the size limit of the system that loads it.
|
|
8
|
+
|
|
9
|
+
## The problem
|
|
10
|
+
|
|
11
|
+
Some agents and tools maintain memory as a directory of small Markdown files plus
|
|
12
|
+
a single curated index (`MEMORY.md`) that points at them. The index is loaded into
|
|
13
|
+
context on startup so the agent boots with a map of what it knows.
|
|
14
|
+
|
|
15
|
+
That index grows. When it grows past the loader's size limit, it is **silently
|
|
16
|
+
truncated** — cut off with no error raised. The agent boots a *partial self* and
|
|
17
|
+
doesn't know it: everything past the cut is invisible, and a pointer to a file
|
|
18
|
+
that no longer exists rides along just as silently. The honest record is there on
|
|
19
|
+
disk; the loaded view of it is a lie of omission.
|
|
20
|
+
|
|
21
|
+
This was observed live: a 36.8KB / 137-entry index against a ~24.4KB load limit,
|
|
22
|
+
with the entire latest section — including the pointer to the most recent state —
|
|
23
|
+
falling past the cut.
|
|
24
|
+
|
|
25
|
+
## The fix
|
|
26
|
+
|
|
27
|
+
qhaway regenerates `MEMORY.md` itself as a **truncation-proof projection** of the
|
|
28
|
+
memory files:
|
|
29
|
+
|
|
30
|
+
- **Files stay the write surface.** You keep writing topic `.md` files exactly as
|
|
31
|
+
you do today. There is no schema to learn and no "save" API to call. qhaway only
|
|
32
|
+
changes *who writes the index* — a machine, not a hand.
|
|
33
|
+
- **It fits the budget.** The regenerated index is guaranteed to come in under the
|
|
34
|
+
loader's limit, so it is never silently cut.
|
|
35
|
+
- **No silent loss — ever.** When the index can't fit everything, it doesn't drop
|
|
36
|
+
entries quietly. It **declares the omission**:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
+47 project memories not shown — run: qhaway index --type project
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Truncation becomes *visible selection*. You always know what was set aside and
|
|
43
|
+
how to see it.
|
|
44
|
+
|
|
45
|
+
The loader keeps reading `MEMORY.md` exactly as before — now complete-for-what-it-
|
|
46
|
+
claims and guaranteed under budget. Nothing downstream changes.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
uv tool install qhaway
|
|
52
|
+
# or
|
|
53
|
+
pipx install qhaway
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Embedded and zero-infra: it uses stdlib SQLite (WAL mode) as a single local
|
|
57
|
+
file. No server, no database to provision, no credentials.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
# Regenerate MEMORY.md from the memory directory (the main command)
|
|
63
|
+
qhaway index
|
|
64
|
+
|
|
65
|
+
# See a specific slice — including entries the default index declared as omitted
|
|
66
|
+
qhaway index --type project
|
|
67
|
+
qhaway index --role <role>
|
|
68
|
+
qhaway index --status superseded
|
|
69
|
+
|
|
70
|
+
# Set a custom budget
|
|
71
|
+
qhaway index --budget <bytes>
|
|
72
|
+
|
|
73
|
+
# Inspect without writing: would it overflow? any broken links? any leftover files?
|
|
74
|
+
qhaway index --check
|
|
75
|
+
|
|
76
|
+
# Print the projection without writing the file
|
|
77
|
+
qhaway index --dry-run
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
To record a memory: **write a topic `.md` file, then run `qhaway index`.** Don't
|
|
81
|
+
hand-edit `MEMORY.md` — it is fully derived, and any hand edit is preserved (see
|
|
82
|
+
below) but won't survive into the index unless it lives in a topic file.
|
|
83
|
+
|
|
84
|
+
## MCP spine (remember / recall)
|
|
85
|
+
|
|
86
|
+
The spine lets a Claude Code instance reach its memory through MCP tools instead
|
|
87
|
+
of hand-writing files. `MEMORY.md` becomes a managed, read-only **redirect** into
|
|
88
|
+
the SQLite-derived index; the topic files stay the source of truth.
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
# Run the MCP server over a memory directory (reconciles once at startup)
|
|
92
|
+
qhaway serve --dir <memory_dir>
|
|
93
|
+
|
|
94
|
+
# Sync the index from the files (alias: qhaway index)
|
|
95
|
+
qhaway reconcile --dir <memory_dir>
|
|
96
|
+
|
|
97
|
+
# Inspect: broken wikilinks, orphan backups, low topic count, would-overflow
|
|
98
|
+
qhaway check --dir <memory_dir>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Two verbs are exposed to the model:
|
|
102
|
+
|
|
103
|
+
- `recall(type?, role?, status?)` — pure read; returns the budgeted projection
|
|
104
|
+
(omit args for the working set).
|
|
105
|
+
- `remember(type, title, body, description?, links?)` — writes a topic file then
|
|
106
|
+
reconciles. Files stay truth; the DB is a derived, rebuildable view.
|
|
107
|
+
|
|
108
|
+
`MEMORY.md` is written born-read-only (`0o444`) as a friction signal — not a hard
|
|
109
|
+
barrier — so the reflexive hand-edit is deflected toward the tools. qhaway's own
|
|
110
|
+
writer updates it via atomic temp-file + replace.
|
|
111
|
+
|
|
112
|
+
## How it works
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
qhaway index
|
|
116
|
+
→ scan the memory directory
|
|
117
|
+
→ parse each file into a node (frontmatter type, filename role, links, body)
|
|
118
|
+
→ build an index of nodes + links in SQLite
|
|
119
|
+
→ project the working set under the byte budget,
|
|
120
|
+
appending a declared-omissions footer for anything set aside
|
|
121
|
+
→ write MEMORY.md
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The memory files are the single source of truth. The index is rebuilt from scratch
|
|
125
|
+
on every run, so it can never drift from the files. The same files always produce
|
|
126
|
+
a byte-identical index.
|
|
127
|
+
|
|
128
|
+
### What's preserved
|
|
129
|
+
|
|
130
|
+
`MEMORY.md` is fully machine-derived — there are no hand-maintained regions. If
|
|
131
|
+
qhaway ever finds that the index was edited by hand since it last wrote it, it does
|
|
132
|
+
**not** overwrite the edit: it renames the existing file to a timestamped
|
|
133
|
+
`MEMORY-<timestamp>.md` and writes a fresh index. Your edit is preserved verbatim;
|
|
134
|
+
the index rebuilds from the files. Nothing is interpreted, merged, or lost.
|
|
135
|
+
|
|
136
|
+
## Design philosophy
|
|
137
|
+
|
|
138
|
+
One pain, fixed completely: **truncation**. Full-text search, deep audit, write
|
|
139
|
+
tooling, and ranking sophistication are deliberately *not* in this version — each
|
|
140
|
+
is a real later idea, none is this version's job.
|
|
141
|
+
|
|
142
|
+
The wager is simple: a structured index built *over* an existing pile of files —
|
|
143
|
+
without replacing the pile — makes the whole thing measurably work better. The
|
|
144
|
+
proof is use. If it removes felt pain for skeptical users who'll drop it the moment
|
|
145
|
+
it's more friction than value, it ships; if it removes the same pain for strangers
|
|
146
|
+
feeling the same sprawl, it spreads. Propagation is the measurement.
|
|
147
|
+
|
|
148
|
+
## Status
|
|
149
|
+
|
|
150
|
+
Early (`v0.1.0`). The design is specified in
|
|
151
|
+
[`docs/superpowers/specs/2026-06-20-qhaway-mvp-design.md`](docs/superpowers/specs/2026-06-20-qhaway-mvp-design.md).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qhaway"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A truncation-proof projection of a Markdown memory index: regenerate MEMORY.md to fit the loader's budget and declare what it sets aside, instead of being silently cut."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Tony", email = "fsgeek@cs.ubc.ca" }]
|
|
9
|
+
keywords = ["memory", "index", "markdown", "llm", "agent", "sqlite", "mcp"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Topic :: Utilities",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"mcp[cli]>=1.28.0",
|
|
17
|
+
"pyyaml",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
qhaway = "qhaway.cli:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.10,<0.11"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[tool.uv.build-backend]
|
|
28
|
+
module-root = "src"
|
|
29
|
+
|
|
30
|
+
# Publishing (deferred — no release until the implementation exists and its tests
|
|
31
|
+
# pass). When ready, tokens are read from the environment at publish time and are
|
|
32
|
+
# NEVER stored in this file or committed:
|
|
33
|
+
# TestPyPI: uv publish --index testpypi (token: PYPI_DEPLOY_KEY_TEST)
|
|
34
|
+
# PyPI: uv publish (token: PYPI_DEPLOY_KEY_REAL)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Entry point for the `qhaway` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from qhaway import model, parse, project, server
|
|
11
|
+
from qhaway import reconcile as reconcile_mod
|
|
12
|
+
from qhaway.reconcile import reconcile
|
|
13
|
+
|
|
14
|
+
MEMORY_NAME = "MEMORY.md"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(args: list[str] | None = None) -> int:
|
|
18
|
+
parser = argparse.ArgumentParser(prog="qhaway")
|
|
19
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
20
|
+
for name in ("reconcile", "check", "serve", "index", "exit"):
|
|
21
|
+
p = sub.add_parser(name)
|
|
22
|
+
p.add_argument("--dir")
|
|
23
|
+
p.add_argument("--budget", type=int, default=project.DEFAULT_BUDGET)
|
|
24
|
+
p.add_argument("--type", dest="content_type")
|
|
25
|
+
p.add_argument("--role")
|
|
26
|
+
p.add_argument("--status", default="live")
|
|
27
|
+
p.add_argument("--dry-run", action="store_true")
|
|
28
|
+
p.add_argument("--check", action="store_true") # deprecated alias on index
|
|
29
|
+
p.add_argument("--emit", action="store_true")
|
|
30
|
+
|
|
31
|
+
ns = parser.parse_args(args)
|
|
32
|
+
directory = _resolve_dir(ns)
|
|
33
|
+
|
|
34
|
+
if ns.command == "serve":
|
|
35
|
+
return _serve(directory)
|
|
36
|
+
if ns.command == "exit":
|
|
37
|
+
return _exit(directory, ns.budget)
|
|
38
|
+
if ns.command == "check" or (ns.command == "index" and ns.check):
|
|
39
|
+
return _check(directory, ns.budget)
|
|
40
|
+
if ns.command == "index" and ns.dry_run:
|
|
41
|
+
return _dry_run(directory, ns)
|
|
42
|
+
|
|
43
|
+
# reconcile, and index-as-reconcile-alias
|
|
44
|
+
try:
|
|
45
|
+
reconcile(directory)
|
|
46
|
+
except FileNotFoundError as exc:
|
|
47
|
+
sys.stderr.write(f"{exc}\n")
|
|
48
|
+
return 1
|
|
49
|
+
if getattr(ns, "emit", False):
|
|
50
|
+
conn = model.get_connection(directory)
|
|
51
|
+
try:
|
|
52
|
+
sys.stdout.write(project.project_slice(conn, budget=ns.budget))
|
|
53
|
+
finally:
|
|
54
|
+
conn.close()
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_dir(ns) -> str:
|
|
59
|
+
return ns.dir or os.environ.get("QHAWAY_MEMORY_DIR") or "."
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _serve(directory: str) -> int:
|
|
63
|
+
if not os.path.isdir(directory):
|
|
64
|
+
sys.stderr.write(f"memory directory is not readable: {directory}\n")
|
|
65
|
+
return 1
|
|
66
|
+
server.run(directory)
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _dry_run(directory: str, ns) -> int:
|
|
71
|
+
if not os.path.isdir(directory):
|
|
72
|
+
sys.stderr.write(f"memory directory is not readable: {directory}\n")
|
|
73
|
+
return 1
|
|
74
|
+
conn = model.get_connection(directory)
|
|
75
|
+
try:
|
|
76
|
+
output = project.project_slice(
|
|
77
|
+
conn,
|
|
78
|
+
budget=ns.budget,
|
|
79
|
+
content_type=ns.content_type,
|
|
80
|
+
role=ns.role,
|
|
81
|
+
status=ns.status,
|
|
82
|
+
)
|
|
83
|
+
finally:
|
|
84
|
+
conn.close()
|
|
85
|
+
sys.stdout.write(output)
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _exit(directory: str, budget: int) -> int:
|
|
90
|
+
"""SessionEnd: leave a current, self-sufficient, truncation-proof index in
|
|
91
|
+
place — NOT the pre-install original. qhaway borrows MEMORY.md while enabled
|
|
92
|
+
and returns a current honest index when it leaves; the original is preserved
|
|
93
|
+
under its distinguished name (MEMORY.preinstall.md) for an explicit uninstall,
|
|
94
|
+
never handed back here. Worst case (a future loader truncates the file), the
|
|
95
|
+
index degrades gracefully and declares what it set aside; the raw original
|
|
96
|
+
would truncate into silent staleness — the exact failure qhaway prevents.
|
|
97
|
+
|
|
98
|
+
The index is budgeted (the footer's bytes are reserved within the budget, not
|
|
99
|
+
appended past it) and carries no recall()/remember() instructions, since the
|
|
100
|
+
hooks are not guaranteed to run once the plugin is disabled.
|
|
101
|
+
"""
|
|
102
|
+
memory_dir = Path(directory)
|
|
103
|
+
if not memory_dir.is_dir():
|
|
104
|
+
sys.stderr.write(f"memory directory is not readable: {memory_dir}\n")
|
|
105
|
+
return 1
|
|
106
|
+
|
|
107
|
+
reconcile(directory)
|
|
108
|
+
conn = model.get_connection(directory)
|
|
109
|
+
try:
|
|
110
|
+
total = len(model.topic_files(memory_dir))
|
|
111
|
+
|
|
112
|
+
def compose_footer(omitted: int) -> str:
|
|
113
|
+
return (
|
|
114
|
+
f"\n\n---\n_qhaway exit index — {total} memories under {budget} "
|
|
115
|
+
f"bytes; {omitted} set aside. Self-sufficient static index "
|
|
116
|
+
"(qhaway disabled)._\n"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Probe at full budget only to SIZE the reserve (footer + signature bytes);
|
|
120
|
+
# the displayed "set aside" count comes from the FINAL reduced-budget
|
|
121
|
+
# projection, so the footer reports what the shipped file actually omits —
|
|
122
|
+
# honest declaration is the whole point. The count's digit width is bounded
|
|
123
|
+
# (a few hundred memories at most), so any drift between probe and final
|
|
124
|
+
# count is sub-byte against the reserve and never pushes over budget.
|
|
125
|
+
probe = project.project_slice_with_overflow(conn, budget=budget)
|
|
126
|
+
reserve = (
|
|
127
|
+
len(compose_footer(sum(probe.overflow.omitted_counts.values())).encode("utf-8"))
|
|
128
|
+
+ len(reconcile_mod.signature_line(""))
|
|
129
|
+
+ 2
|
|
130
|
+
)
|
|
131
|
+
result = project.project_slice_with_overflow(conn, budget=max(0, budget - reserve))
|
|
132
|
+
footer = compose_footer(sum(result.overflow.omitted_counts.values()))
|
|
133
|
+
finally:
|
|
134
|
+
conn.close()
|
|
135
|
+
reconcile_mod.write_readonly(
|
|
136
|
+
memory_dir / MEMORY_NAME, reconcile_mod.embed_signature(result.markdown + footer)
|
|
137
|
+
)
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _check(directory: str, budget: int) -> int:
|
|
142
|
+
memory_dir = Path(directory)
|
|
143
|
+
if not memory_dir.is_dir():
|
|
144
|
+
sys.stderr.write(f"memory directory is not readable: {memory_dir}\n")
|
|
145
|
+
return 1
|
|
146
|
+
|
|
147
|
+
exit_code = 0
|
|
148
|
+
topic_count = len(model.topic_files(memory_dir))
|
|
149
|
+
if topic_count <= 2:
|
|
150
|
+
sys.stderr.write(f"warning: low topic file count ({topic_count}) in {memory_dir}\n")
|
|
151
|
+
|
|
152
|
+
orphans = _orphan_files(memory_dir)
|
|
153
|
+
if orphans:
|
|
154
|
+
sys.stdout.write(f"{len(orphans)} orphan MEMORY backups found:\n")
|
|
155
|
+
for orphan in orphans:
|
|
156
|
+
sys.stdout.write(f"- {orphan.name}\n")
|
|
157
|
+
|
|
158
|
+
conn = model.get_connection(directory)
|
|
159
|
+
try:
|
|
160
|
+
dangling = _dangling_links(conn)
|
|
161
|
+
stale_drift = _stale_drift(conn)
|
|
162
|
+
full_projection = project.project_slice(conn, budget=10**12)
|
|
163
|
+
finally:
|
|
164
|
+
conn.close()
|
|
165
|
+
|
|
166
|
+
if dangling:
|
|
167
|
+
exit_code = 1
|
|
168
|
+
sys.stdout.write("dangling topic wikilinks found:\n")
|
|
169
|
+
for src_file, dst_slug in dangling:
|
|
170
|
+
sys.stdout.write(f"- {src_file} -> [[{dst_slug}]]\n")
|
|
171
|
+
|
|
172
|
+
if stale_drift:
|
|
173
|
+
exit_code = 1
|
|
174
|
+
sys.stdout.write(
|
|
175
|
+
"live memories whose body announces supersession but whose name: was "
|
|
176
|
+
"never redirected (they leak into the working set):\n"
|
|
177
|
+
)
|
|
178
|
+
for file_name, marker in stale_drift:
|
|
179
|
+
sys.stdout.write(f"- {file_name} (body says {marker}; retire it: set name: 'SUPERSEDED — see ...')\n")
|
|
180
|
+
|
|
181
|
+
if len(full_projection.encode("utf-8")) > budget:
|
|
182
|
+
overflow = len(full_projection.encode("utf-8")) - budget
|
|
183
|
+
exit_code = 1
|
|
184
|
+
sys.stderr.write(f"corpus exceeds budget by {overflow} bytes before projection\n")
|
|
185
|
+
|
|
186
|
+
if exit_code == 0 and not orphans and topic_count > 2:
|
|
187
|
+
sys.stdout.write("qhaway check passed\n")
|
|
188
|
+
return exit_code
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _stale_drift(conn) -> list[tuple[str, str]]:
|
|
192
|
+
"""Find live nodes whose body announces supersession but whose name: was
|
|
193
|
+
never rewritten to the redirect form — so parse left status=live and the
|
|
194
|
+
projector serves them as current. This is the silent-staleness leak: the
|
|
195
|
+
conscientious in-body 'SUPERSEDED' annotation never reaches the one field
|
|
196
|
+
the retire path keys on. Conservative by design (a tombstone word as a
|
|
197
|
+
leading/emphasized token on its own line, not a passing mention) so a
|
|
198
|
+
correctly-live memory that merely discusses supersession is not nagged.
|
|
199
|
+
"""
|
|
200
|
+
drift: list[tuple[str, str]] = []
|
|
201
|
+
for file_name, status, body in conn.execute(
|
|
202
|
+
"SELECT file, status, body FROM nodes ORDER BY file"
|
|
203
|
+
).fetchall():
|
|
204
|
+
if status != "live":
|
|
205
|
+
continue
|
|
206
|
+
marker = _body_supersession_marker(body or "")
|
|
207
|
+
if marker:
|
|
208
|
+
drift.append((file_name, marker))
|
|
209
|
+
return drift
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _body_supersession_marker(body: str) -> str | None:
|
|
213
|
+
for raw in body.splitlines():
|
|
214
|
+
line = raw.strip().lstrip("*_# ").strip()
|
|
215
|
+
upper = line.upper()
|
|
216
|
+
for word in parse.TOMBSTONE_NAMES:
|
|
217
|
+
if upper.startswith(word):
|
|
218
|
+
return word
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _dangling_links(conn) -> list[tuple[str, str]]:
|
|
223
|
+
stems = {row[0].removesuffix(".md") for row in conn.execute("SELECT file FROM nodes").fetchall()}
|
|
224
|
+
dangling: list[tuple[str, str]] = []
|
|
225
|
+
for src_file, dst_slug in conn.execute(
|
|
226
|
+
"SELECT src_file, dst_slug FROM edges ORDER BY src_file, dst_slug"
|
|
227
|
+
).fetchall():
|
|
228
|
+
if dst_slug not in stems:
|
|
229
|
+
dangling.append((src_file, dst_slug))
|
|
230
|
+
return dangling
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _orphan_files(memory_dir: Path) -> list[Path]:
|
|
234
|
+
return sorted(memory_dir.glob("MEMORY-*.md"), key=lambda path: path.name)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
raise SystemExit(main())
|