meminqu 1.0.0__py3-none-any.whl
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.
- meminqu/__init__.py +40 -0
- meminqu/entry.py +63 -0
- meminqu/registers.py +177 -0
- meminqu/session.py +162 -0
- meminqu-1.0.0.dist-info/METADATA +143 -0
- meminqu-1.0.0.dist-info/RECORD +9 -0
- meminqu-1.0.0.dist-info/WHEEL +5 -0
- meminqu-1.0.0.dist-info/licenses/LICENSE +21 -0
- meminqu-1.0.0.dist-info/top_level.txt +1 -0
meminqu/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""meminqu — guided capture interviews for a memory store.
|
|
2
|
+
|
|
3
|
+
Walks each memory domain, asks questions in deliberately different
|
|
4
|
+
registers (direct, reflective, structural, irreverent, sparse,
|
|
5
|
+
temporal, contrastive), and captures the answers verbatim into the
|
|
6
|
+
domain's raw.md through memdate's pure CAPTURE rules. No
|
|
7
|
+
synthesis, no interpretation, no analysis.
|
|
8
|
+
|
|
9
|
+
Front-facing conversion of the ``meminqu-memory-interrogation`` skill
|
|
10
|
+
(Ryan's skill-suite, MIT). The domain list is inherited from the
|
|
11
|
+
memdate config — this package declares none of its own.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .entry import ENTRY_MARKER, Response, answers_preserved_verbatim, format_entry
|
|
15
|
+
from .registers import (
|
|
16
|
+
REGISTERS,
|
|
17
|
+
Register,
|
|
18
|
+
RegisterError,
|
|
19
|
+
get_register,
|
|
20
|
+
plan_cycle,
|
|
21
|
+
validate_register_set,
|
|
22
|
+
)
|
|
23
|
+
from .session import InterviewSession, SessionError
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"InterviewSession",
|
|
27
|
+
"SessionError",
|
|
28
|
+
"Response",
|
|
29
|
+
"Register",
|
|
30
|
+
"RegisterError",
|
|
31
|
+
"REGISTERS",
|
|
32
|
+
"get_register",
|
|
33
|
+
"plan_cycle",
|
|
34
|
+
"validate_register_set",
|
|
35
|
+
"format_entry",
|
|
36
|
+
"answers_preserved_verbatim",
|
|
37
|
+
"ENTRY_MARKER",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
__version__ = "1.0.0"
|
meminqu/entry.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Entry formatting: the inquiry-capture record.
|
|
2
|
+
|
|
3
|
+
The contract: answers are preserved verbatim — byte-identical. The
|
|
4
|
+
function adds structure around them (the inquiry marker, the register
|
|
5
|
+
list, question text) but never touches the answer text itself. If an
|
|
6
|
+
answer is empty (skipped question), the skip is recorded as a skip —
|
|
7
|
+
still capture, not silence.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from .registers import get_register
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Response:
|
|
19
|
+
"""One question asked, and its answer (possibly skipped)."""
|
|
20
|
+
|
|
21
|
+
register: str
|
|
22
|
+
question: str
|
|
23
|
+
answer: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ENTRY_MARKER = "### Inquiry capture"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_entry(domain: str, responses: list[Response]) -> str:
|
|
30
|
+
"""Build the capture entry body for one domain.
|
|
31
|
+
|
|
32
|
+
Raises ValueError on an empty response list or unknown register
|
|
33
|
+
names. Answers are embedded verbatim — this function performs no
|
|
34
|
+
normalization, trimming, or interpretation on them.
|
|
35
|
+
"""
|
|
36
|
+
if not responses:
|
|
37
|
+
raise ValueError("Cannot format an entry with no responses.")
|
|
38
|
+
registers_used: list[str] = []
|
|
39
|
+
for resp in responses:
|
|
40
|
+
reg = get_register(resp.register)
|
|
41
|
+
if reg.name not in registers_used:
|
|
42
|
+
registers_used.append(reg.name)
|
|
43
|
+
|
|
44
|
+
lines = [f"{ENTRY_MARKER} — {domain} (registers: {', '.join(registers_used)})", ""]
|
|
45
|
+
for resp in responses:
|
|
46
|
+
reg = get_register(resp.register)
|
|
47
|
+
lines.append(f"**[{reg.label}]** {resp.question.strip()}")
|
|
48
|
+
if resp.answer is None or not resp.answer.strip():
|
|
49
|
+
lines.append("_[no answer]_")
|
|
50
|
+
else:
|
|
51
|
+
# Verbatim: the answer text is appended exactly as given.
|
|
52
|
+
lines.append(resp.answer)
|
|
53
|
+
lines.append("")
|
|
54
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def answers_preserved_verbatim(body: str, responses: list[Response]) -> bool:
|
|
58
|
+
"""Check that every non-empty answer appears byte-identical in the body."""
|
|
59
|
+
for resp in responses:
|
|
60
|
+
if resp.answer and resp.answer.strip():
|
|
61
|
+
if resp.answer not in body:
|
|
62
|
+
return False
|
|
63
|
+
return True
|
meminqu/registers.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""The seven registers: deliberately different question stances.
|
|
2
|
+
|
|
3
|
+
This is the actual mechanism of the interview. Each register is a
|
|
4
|
+
distinct way of approaching the same domain — direct, reflective,
|
|
5
|
+
structural, irreverent, sparse, temporal, contrastive — so the answers
|
|
6
|
+
don't all come from the same angle. The registers are data: a caller
|
|
7
|
+
can supply their own set, but the shipped seven are the skill's
|
|
8
|
+
complete, unmodified set.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RegisterError(ValueError):
|
|
17
|
+
"""Raised for unknown register names or bad register sets."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Register:
|
|
22
|
+
"""One question stance."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
label: str
|
|
26
|
+
description: str
|
|
27
|
+
example_stems: tuple[str, ...]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
REGISTERS: tuple[Register, ...] = (
|
|
31
|
+
Register(
|
|
32
|
+
name="direct",
|
|
33
|
+
label="Direct / forensic",
|
|
34
|
+
description=(
|
|
35
|
+
"No framing, no softening. Ask for the facts as they are, "
|
|
36
|
+
"the way a deposition would."
|
|
37
|
+
),
|
|
38
|
+
example_stems=(
|
|
39
|
+
"State the single most important fact in this domain right now, with no qualifiers.",
|
|
40
|
+
"What happened here that you would swear to under oath?",
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
Register(
|
|
44
|
+
name="reflective",
|
|
45
|
+
label="Reflective / interior",
|
|
46
|
+
description=(
|
|
47
|
+
"How it feels from the inside. Subjective material the "
|
|
48
|
+
"forensic register can't reach."
|
|
49
|
+
),
|
|
50
|
+
example_stems=(
|
|
51
|
+
"What does this domain feel like from the inside this week?",
|
|
52
|
+
"What has been sitting in the back of your mind about it?",
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
Register(
|
|
56
|
+
name="structural",
|
|
57
|
+
label="Structural / systems",
|
|
58
|
+
description=(
|
|
59
|
+
"The machinery underneath: routines, dependencies, "
|
|
60
|
+
"load-bearing parts, failure points."
|
|
61
|
+
),
|
|
62
|
+
example_stems=(
|
|
63
|
+
"What system or routine governs this domain? Which parts are load-bearing?",
|
|
64
|
+
"If this domain were a machine, which part is under the most strain?",
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
Register(
|
|
68
|
+
name="irreverent",
|
|
69
|
+
label="Irreverent / cutting",
|
|
70
|
+
description=(
|
|
71
|
+
"Deliberately disrespectful of the domain's official story. "
|
|
72
|
+
"Politeness is a filter; this register removes it."
|
|
73
|
+
),
|
|
74
|
+
example_stems=(
|
|
75
|
+
"Cut the diplomacy: what is actually going on here?",
|
|
76
|
+
"What is the most embarrassing truth about this domain you would rather not admit?",
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
Register(
|
|
80
|
+
name="sparse",
|
|
81
|
+
label="Sparse / minimal",
|
|
82
|
+
description=(
|
|
83
|
+
"Compression as a forcing function. What survives when "
|
|
84
|
+
"there is almost no room says what matters."
|
|
85
|
+
),
|
|
86
|
+
example_stems=(
|
|
87
|
+
"This domain in one sentence.",
|
|
88
|
+
"Three words. No more.",
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
Register(
|
|
92
|
+
name="temporal",
|
|
93
|
+
label="Temporal / what's alive now",
|
|
94
|
+
description=(
|
|
95
|
+
"Time-indexed. What is true *now*, what changed, what is "
|
|
96
|
+
"moving — not the domain's permanent self-image."
|
|
97
|
+
),
|
|
98
|
+
example_stems=(
|
|
99
|
+
"What is alive in this domain right now, today?",
|
|
100
|
+
"What changed here since the last time you looked at it?",
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
Register(
|
|
104
|
+
name="contrastive",
|
|
105
|
+
label="Contrastive / what is not true",
|
|
106
|
+
description=(
|
|
107
|
+
"Approach by negation. False appearances and near-misses "
|
|
108
|
+
"define the edges of what is real here."
|
|
109
|
+
),
|
|
110
|
+
example_stems=(
|
|
111
|
+
"What is *not* true about this domain, though it might look that way?",
|
|
112
|
+
"What would surprise someone who only saw the surface?",
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
_REGISTER_INDEX = {r.name: r for r in REGISTERS}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_register(name: str) -> Register:
|
|
121
|
+
"""Return the register with this name, or raise RegisterError."""
|
|
122
|
+
key = name.strip().lower()
|
|
123
|
+
if key not in _REGISTER_INDEX:
|
|
124
|
+
raise RegisterError(
|
|
125
|
+
f"Unknown register {name!r}. Known registers: "
|
|
126
|
+
f"{', '.join(r.name for r in REGISTERS)}."
|
|
127
|
+
)
|
|
128
|
+
return _REGISTER_INDEX[key]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def validate_register_set(registers: tuple[Register, ...]) -> tuple[Register, ...]:
|
|
132
|
+
"""Check a caller-supplied register set: unique names, usable stems."""
|
|
133
|
+
if len(registers) < 2:
|
|
134
|
+
raise RegisterError("A register set needs at least two registers.")
|
|
135
|
+
seen: set[str] = set()
|
|
136
|
+
for r in registers:
|
|
137
|
+
key = r.name.strip().lower()
|
|
138
|
+
if not key:
|
|
139
|
+
raise RegisterError("Register names must not be blank.")
|
|
140
|
+
if key in seen:
|
|
141
|
+
raise RegisterError(f"Duplicate register: {r.name!r}.")
|
|
142
|
+
seen.add(key)
|
|
143
|
+
if not r.example_stems:
|
|
144
|
+
raise RegisterError(f"Register {r.name!r} has no example stems.")
|
|
145
|
+
return registers
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def plan_cycle(
|
|
149
|
+
domains: tuple[str, ...] | list[str],
|
|
150
|
+
per_domain: int = 3,
|
|
151
|
+
registers: tuple[Register, ...] = REGISTERS,
|
|
152
|
+
) -> dict[str, list[str]]:
|
|
153
|
+
"""Assign registers to each domain for one interview cycle.
|
|
154
|
+
|
|
155
|
+
Rotation rule: consecutive domains never share the same register
|
|
156
|
+
set, and the offset advances each domain so every register gets
|
|
157
|
+
used across a full cycle. Deterministic — the same inputs always
|
|
158
|
+
produce the same plan.
|
|
159
|
+
"""
|
|
160
|
+
registers = validate_register_set(tuple(registers))
|
|
161
|
+
domains = tuple(domains)
|
|
162
|
+
if not domains:
|
|
163
|
+
raise RegisterError("Need at least one domain to plan a cycle.")
|
|
164
|
+
if per_domain < 1:
|
|
165
|
+
raise RegisterError("per_domain must be at least 1.")
|
|
166
|
+
if per_domain > len(registers):
|
|
167
|
+
raise RegisterError(
|
|
168
|
+
f"per_domain ({per_domain}) exceeds the register count "
|
|
169
|
+
f"({len(registers)})."
|
|
170
|
+
)
|
|
171
|
+
names = [r.name for r in registers]
|
|
172
|
+
n = len(names)
|
|
173
|
+
plan: dict[str, list[str]] = {}
|
|
174
|
+
for i, domain in enumerate(domains):
|
|
175
|
+
start = (i * per_domain) % n
|
|
176
|
+
plan[domain] = [names[(start + j) % n] for j in range(per_domain)]
|
|
177
|
+
return plan
|
meminqu/session.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""The interview session: domain walk, capture calls, completion report.
|
|
2
|
+
|
|
3
|
+
The session is the record-keeping side of the interview. The actual
|
|
4
|
+
questions and answers happen outside it (an operator or a model runs
|
|
5
|
+
the conversation); this class tracks state — which domain is active,
|
|
6
|
+
which registers were suggested, what got captured, what got skipped —
|
|
7
|
+
formats entries, and makes the capture calls into memdate.
|
|
8
|
+
|
|
9
|
+
The domain list is inherited from the memdate config. This
|
|
10
|
+
package declares no domains of its own: single source of truth.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from memdate import (
|
|
17
|
+
CaptureResult,
|
|
18
|
+
InterpretationError,
|
|
19
|
+
MemoryConfig,
|
|
20
|
+
StorageAdapter,
|
|
21
|
+
capture,
|
|
22
|
+
)
|
|
23
|
+
except ImportError as exc: # pragma: no cover - dependency wiring
|
|
24
|
+
raise ImportError(
|
|
25
|
+
"meminqu requires the memdate package "
|
|
26
|
+
"(the memdate module). Install it first, e.g.: "
|
|
27
|
+
"pip install -e ../memdate # from this staging tree"
|
|
28
|
+
) from exc
|
|
29
|
+
|
|
30
|
+
from .entry import Response, format_entry
|
|
31
|
+
from .registers import REGISTERS, Register, plan_cycle
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SessionError(ValueError):
|
|
35
|
+
"""Raised for bad session operations (unknown domain, bad order, ...)."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InterviewSession:
|
|
39
|
+
"""One guided-capture interview cycle over a memory store's domains."""
|
|
40
|
+
|
|
41
|
+
SOURCE_TAG = "meminqu"
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
config: MemoryConfig,
|
|
46
|
+
*,
|
|
47
|
+
adapter: StorageAdapter | None = None,
|
|
48
|
+
per_domain: int = 3,
|
|
49
|
+
registers: tuple[Register, ...] = REGISTERS,
|
|
50
|
+
domains: tuple[str, ...] | list[str] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self.config = config
|
|
53
|
+
self.adapter = adapter
|
|
54
|
+
# Domains come from the config unless the caller narrows the
|
|
55
|
+
# cycle to a subset (which must still be config-known domains).
|
|
56
|
+
chosen = tuple(domains) if domains is not None else config.domains
|
|
57
|
+
for d in chosen:
|
|
58
|
+
config.require_domain(d)
|
|
59
|
+
self.domains: tuple[str, ...] = chosen
|
|
60
|
+
self.register_plan = plan_cycle(self.domains, per_domain, registers)
|
|
61
|
+
self._captured: dict[str, CaptureResult] = {}
|
|
62
|
+
self._skipped: list[str] = []
|
|
63
|
+
self._started: list[str] = []
|
|
64
|
+
|
|
65
|
+
# -- interview flow -------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def start_domain(self, domain: str) -> list[str]:
|
|
68
|
+
"""Announce a domain; return its suggested registers."""
|
|
69
|
+
name = self.config.require_domain(domain)
|
|
70
|
+
if name not in self.domains:
|
|
71
|
+
raise SessionError(f"Domain {domain!r} is not in this cycle.")
|
|
72
|
+
if name not in self._started:
|
|
73
|
+
self._started.append(name)
|
|
74
|
+
return list(self.register_plan[name])
|
|
75
|
+
|
|
76
|
+
def submit_answers(
|
|
77
|
+
self,
|
|
78
|
+
domain: str,
|
|
79
|
+
responses: list[Response],
|
|
80
|
+
*,
|
|
81
|
+
capture_domain: str | None = None,
|
|
82
|
+
) -> CaptureResult:
|
|
83
|
+
"""Format the answers and capture them via memdate.
|
|
84
|
+
|
|
85
|
+
Captures under the announced domain unless ``capture_domain``
|
|
86
|
+
is given — the explicit reassignment the skill requires before
|
|
87
|
+
material may land in a different domain's raw.md.
|
|
88
|
+
|
|
89
|
+
Answers are captured under memdate's pure CAPTURE rules:
|
|
90
|
+
if an answer trips an interpretation marker, capture() raises
|
|
91
|
+
InterpretationError rather than silently synthesizing. That is
|
|
92
|
+
enforcement, not a bug — rewrite the answer as raw record, or
|
|
93
|
+
pass force through the lower-level capture() yourself.
|
|
94
|
+
"""
|
|
95
|
+
name = self.config.require_domain(domain)
|
|
96
|
+
target = (
|
|
97
|
+
self.config.require_domain(capture_domain)
|
|
98
|
+
if capture_domain is not None
|
|
99
|
+
else name
|
|
100
|
+
)
|
|
101
|
+
body = format_entry(name, responses)
|
|
102
|
+
result = capture(
|
|
103
|
+
self.config,
|
|
104
|
+
target,
|
|
105
|
+
body,
|
|
106
|
+
source=self.SOURCE_TAG,
|
|
107
|
+
adapter=self.adapter,
|
|
108
|
+
)
|
|
109
|
+
self._captured[name] = result
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
def skip_domain(self, domain: str) -> None:
|
|
113
|
+
"""Record a skipped domain. No capture is written."""
|
|
114
|
+
name = self.config.require_domain(domain)
|
|
115
|
+
if name not in self.domains:
|
|
116
|
+
raise SessionError(f"Domain {domain!r} is not in this cycle.")
|
|
117
|
+
if name not in self._skipped:
|
|
118
|
+
self._skipped.append(name)
|
|
119
|
+
|
|
120
|
+
# -- completion -----------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def completion_report(self) -> dict:
|
|
123
|
+
"""Structured report: what was captured, where, what was skipped."""
|
|
124
|
+
return {
|
|
125
|
+
"domains_captured": sorted(self._captured),
|
|
126
|
+
"paths": {d: r.path for d, r in sorted(self._captured.items())},
|
|
127
|
+
"domains_skipped": list(self._skipped),
|
|
128
|
+
"domains_untouched": [
|
|
129
|
+
d for d in self.domains
|
|
130
|
+
if d not in self._captured and d not in self._skipped
|
|
131
|
+
],
|
|
132
|
+
"next_actions": ["continue", "deepen a domain", "distill", "exit"],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def render_report(self) -> str:
|
|
136
|
+
"""Human-readable completion report."""
|
|
137
|
+
report = self.completion_report()
|
|
138
|
+
lines = ["Interview complete.", ""]
|
|
139
|
+
if report["domains_captured"]:
|
|
140
|
+
lines.append("Captured:")
|
|
141
|
+
for d in report["domains_captured"]:
|
|
142
|
+
lines.append(f" - {d}: {report['paths'][d]}")
|
|
143
|
+
else:
|
|
144
|
+
lines.append("Nothing captured.")
|
|
145
|
+
if report["domains_skipped"]:
|
|
146
|
+
lines.append("Skipped: " + ", ".join(report["domains_skipped"]))
|
|
147
|
+
if report["domains_untouched"]:
|
|
148
|
+
lines.append("Untouched: " + ", ".join(report["domains_untouched"]))
|
|
149
|
+
lines.append("")
|
|
150
|
+
lines.append(
|
|
151
|
+
"Next: " + " / ".join(report["next_actions"]) + "."
|
|
152
|
+
)
|
|
153
|
+
return "\n".join(lines) + "\n"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"InterviewSession",
|
|
158
|
+
"SessionError",
|
|
159
|
+
"InterpretationError",
|
|
160
|
+
"CaptureResult",
|
|
161
|
+
"MemoryConfig",
|
|
162
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meminqu
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Guided capture interviews for a memory store: seven question registers, rotating across domains, answers captured verbatim through memdate's pure CAPTURE rules
|
|
5
|
+
Author: Ryan Wesley Hitchcock
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ohpersonperson/frontface
|
|
8
|
+
Keywords: memory,interview,capture,knowledge-management,journaling
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: memdate
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# meminqu — guided capture interviews for a memory store
|
|
19
|
+
|
|
20
|
+
Seven questions, asked seven different ways, written down exactly as
|
|
21
|
+
answered. That is the whole product.
|
|
22
|
+
|
|
23
|
+
An interview cycle walks each memory domain in order. For each domain,
|
|
24
|
+
the interviewer poses 2–4 questions in deliberately different
|
|
25
|
+
*registers* — direct, reflective, structural, irreverent, sparse,
|
|
26
|
+
temporal, contrastive — receives the answers, and captures them
|
|
27
|
+
**verbatim** into that domain's `raw.md`. No synthesis, no
|
|
28
|
+
interpretation, no analysis. The registers rotate across the cycle so
|
|
29
|
+
no two domains get interrogated from the same angle twice in a row.
|
|
30
|
+
|
|
31
|
+
No prerequisites. You don't need to know anything about the project's
|
|
32
|
+
history. The mechanism is the whole product.
|
|
33
|
+
|
|
34
|
+
*(Lineage: front-facing conversion of the `meminqu-memory-interrogation`
|
|
35
|
+
skill from Ryan's skill-suite, MIT. What changed is documented in
|
|
36
|
+
CHANGELOG.md — the decoupling record.)*
|
|
37
|
+
|
|
38
|
+
## What it does
|
|
39
|
+
|
|
40
|
+
- **Seven registers** (`registers.py`) — the question stances, kept
|
|
41
|
+
exactly as the skill defined them, each with a description and
|
|
42
|
+
example stems. They are data, so a caller can supply their own set;
|
|
43
|
+
the shipped seven are the complete original set.
|
|
44
|
+
- **Cycle planning** (`plan_cycle`) — assigns registers per domain
|
|
45
|
+
deterministically: consecutive domains never share a register set,
|
|
46
|
+
and a full cycle uses every register. No fixed order, no repetition
|
|
47
|
+
rut.
|
|
48
|
+
- **Verbatim entry format** (`entry.py`) — one `### Inquiry capture —
|
|
49
|
+
<domain> (registers: …)` record per domain, questions paired with
|
|
50
|
+
answers, answers embedded byte-identical. Skipped questions are
|
|
51
|
+
recorded as skips, not silently dropped.
|
|
52
|
+
- **Session runner** (`session.py`) — the record-keeping side of the
|
|
53
|
+
interview: domain walk, skip, explicit reassignment (material that
|
|
54
|
+
belongs elsewhere is captured under the announced domain *unless*
|
|
55
|
+
the operator explicitly reassigns it), and a completion report
|
|
56
|
+
listing which domains got entries and where they landed.
|
|
57
|
+
|
|
58
|
+
## Why it works
|
|
59
|
+
|
|
60
|
+
Most "tell me about your week" systems fail in one of two places: they
|
|
61
|
+
ask every question the same way (so every answer comes from the same
|
|
62
|
+
angle), or they quietly rewrite what you said on the way in (so the
|
|
63
|
+
record is the interviewer's summary, not your words). This package
|
|
64
|
+
fixes both in code:
|
|
65
|
+
|
|
66
|
+
- **Register rotation** — `plan_cycle` guarantees angular variety. A
|
|
67
|
+
test asserts consecutive domains differ and a full cycle covers all
|
|
68
|
+
seven registers.
|
|
69
|
+
- **Verbatim preservation** — `format_entry` adds structure *around*
|
|
70
|
+
answers but never touches them. The load-bearing test feeds answers
|
|
71
|
+
with contradictions, typos, and markdown through formatting and
|
|
72
|
+
asserts byte-identical survival.
|
|
73
|
+
- **Capture purity** — answers go through memdate's `capture()`,
|
|
74
|
+
which raises `InterpretationError` if the text looks like synthesis
|
|
75
|
+
("in summary…") instead of a raw record. The refusal is the feature:
|
|
76
|
+
it keeps capture and interpretation from mixing in the same motion.
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
`meminqu` is a thin runner over `memdate` — it does
|
|
81
|
+
not reimplement capture, config, or storage. Install the dependency
|
|
82
|
+
first:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# from the staging tree
|
|
86
|
+
pip install -e ../memdate
|
|
87
|
+
pip install -e .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
or for a quick offline run, put both `src` trees on the path:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
PYTHONPATH=../memdate/src:src python3 -m unittest discover -s tests
|
|
94
|
+
PYTHONPATH=../memdate/src:src python3 examples/demo_interview.py
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`pyproject.toml` declares `memdate` as a dependency honestly —
|
|
98
|
+
this package cannot function without it.
|
|
99
|
+
|
|
100
|
+
## Quickstart
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from meminqu import InterviewSession, Response
|
|
104
|
+
from memdate import MemoryConfig
|
|
105
|
+
|
|
106
|
+
config = MemoryConfig(root="./my-memory") # domains from config, not from here
|
|
107
|
+
session = InterviewSession(config)
|
|
108
|
+
|
|
109
|
+
for domain in session.domains:
|
|
110
|
+
registers = session.start_domain(domain) # announce; get suggested registers
|
|
111
|
+
# ... the operator/model asks questions, collects answers ...
|
|
112
|
+
session.submit_answers(domain, [
|
|
113
|
+
Response("direct", "What is the key fact?", "The server migration finished Tuesday."),
|
|
114
|
+
Response("sparse", "This domain in one sentence.", "Quiet, for once."),
|
|
115
|
+
])
|
|
116
|
+
# session.skip_domain(domain) # or skip it — nothing is written
|
|
117
|
+
|
|
118
|
+
print(session.render_report())
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Where the formats meet
|
|
122
|
+
|
|
123
|
+
The original skill specified captures under a `## YYYY-MM-DD`
|
|
124
|
+
day-heading. The converted memdate writes a `## YYYY-MM-DD
|
|
125
|
+
HH:MM TZ` heading per entry instead. This package defers to the
|
|
126
|
+
library's heading — one source of truth for the capture format — and
|
|
127
|
+
the `### Inquiry capture — <domain> (registers: …)` marker rides
|
|
128
|
+
inside the entry body, where the register list stays attached to the
|
|
129
|
+
answers it describes.
|
|
130
|
+
|
|
131
|
+
## Evidence taxonomy
|
|
132
|
+
|
|
133
|
+
This package tags no evidence itself; its artifacts are `raw.md`
|
|
134
|
+
entries. memdate's README maps those onto the shared ten-tier
|
|
135
|
+
taxonomy (`raw.md` entries sit at OBSERVATION / CLAIM). No third
|
|
136
|
+
taxonomy was invented.
|
|
137
|
+
|
|
138
|
+
## Worked example
|
|
139
|
+
|
|
140
|
+
- `examples/example-1-full-cycle.md` — one full annotated cycle on
|
|
141
|
+
neutral domains, register rotation shown.
|
|
142
|
+
- `examples/demo_interview.py` — scripted end-to-end run against a
|
|
143
|
+
temp store; no model, no network.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
meminqu/__init__.py,sha256=Ti6ND01wzcKXhUlPOBLOKOentScKLbBi-rDT_-hzcYQ,1123
|
|
2
|
+
meminqu/entry.py,sha256=Td_OHMJqnv6MhV5D7Z_ZW74K4QEB8jE7JRPkchzKZKI,2127
|
|
3
|
+
meminqu/registers.py,sha256=gVC7xavcHz7cK7zU8asPqrEoMESgQjWwhOn9Yum6b6A,6046
|
|
4
|
+
meminqu/session.py,sha256=mvuJyB1712q4lP7vd0gEDLgnFYoAOHy7x2qSuxJPpG4,5894
|
|
5
|
+
meminqu-1.0.0.dist-info/licenses/LICENSE,sha256=Zx_XafyZDq7qBexUb7tER9kYlBZ0rCVaAwKuqMCfxLE,1078
|
|
6
|
+
meminqu-1.0.0.dist-info/METADATA,sha256=zRHnJKoI_M7eSGIlG6khNc9Z3TqXsB4OPzP_ctZdMdM,5882
|
|
7
|
+
meminqu-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
meminqu-1.0.0.dist-info/top_level.txt,sha256=neG-q5IDmgCRjizwVApWa3wY669pJO_Hz-MColjVyZ4,8
|
|
9
|
+
meminqu-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Wesley Hitchcock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
meminqu
|