exegete 0.14.1a0.dev1__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.
- exegete/__init__.py +16 -0
- exegete/coder_comparison.py +208 -0
- exegete/cursors.py +218 -0
- exegete/database.py +12621 -0
- exegete/env_settings.py +174 -0
- exegete/memo_privacy.py +215 -0
- exegete/names.py +68 -0
- exegete/new_project.py +1027 -0
- exegete/preview_tokens.py +613 -0
- exegete/project_settings.py +1082 -0
- exegete/pseudonymise.py +2796 -0
- exegete/refi_export.py +645 -0
- exegete/server.py +18899 -0
- exegete/sessions.py +1153 -0
- exegete/state_folder.py +360 -0
- exegete/transition.py +1049 -0
- exegete-0.14.1a0.dev1.dist-info/METADATA +515 -0
- exegete-0.14.1a0.dev1.dist-info/RECORD +24 -0
- exegete-0.14.1a0.dev1.dist-info/WHEEL +5 -0
- exegete-0.14.1a0.dev1.dist-info/entry_points.txt +2 -0
- exegete-0.14.1a0.dev1.dist-info/licenses/COPYING.LESSER +165 -0
- exegete-0.14.1a0.dev1.dist-info/licenses/NOTICE +773 -0
- exegete-0.14.1a0.dev1.dist-info/licenses/legal/GPL-3.0.txt +674 -0
- exegete-0.14.1a0.dev1.dist-info/top_level.txt +1 -0
exegete/env_settings.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
"""The server's settings, read under both spellings (v0.14.1).
|
|
3
|
+
|
|
4
|
+
Every setting this server reads from its environment is read here and
|
|
5
|
+
nowhere else (tests/test_v0141_compat.py forbids any other environment
|
|
6
|
+
read in the package). Each has a new spelling, starting EXEGETE_, and
|
|
7
|
+
the earlier one, which is still read until v1.0 (names.SETTINGS). The
|
|
8
|
+
one other read here is not a setting: Windows' own folder, SystemRoot,
|
|
9
|
+
from which the transition check starts PowerShell by its full path.
|
|
10
|
+
|
|
11
|
+
The rule when both spellings are set:
|
|
12
|
+
|
|
13
|
+
- only the new one: used, silently;
|
|
14
|
+
- only the earlier one: used, and the server's start-up log says once
|
|
15
|
+
which spelling to use instead, and until when the earlier one works;
|
|
16
|
+
- both, saying the same thing: used, silently;
|
|
17
|
+
- both, saying different things: the server does not start, and says
|
|
18
|
+
which two spellings disagree (never their values);
|
|
19
|
+
- a workspace is required if either spelling of
|
|
20
|
+
EXEGETE_WORKSPACE_REQUIRED says 1.
|
|
21
|
+
|
|
22
|
+
"The same thing" is judged after each value is tidied exactly as its
|
|
23
|
+
read tidies it (spaces stripped where the read strips them, the tool set
|
|
24
|
+
in lower case, a workspace path with `~` expanded, the two switches
|
|
25
|
+
compared as "is it 1", the AI coder name after validate_coder_name), and
|
|
26
|
+
a blank value counts as unset. The value then used is the raw text of
|
|
27
|
+
the spelling chosen, so it behaves exactly as it did before the rename.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional
|
|
33
|
+
|
|
34
|
+
from . import names
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Reading(NamedTuple):
|
|
38
|
+
"""One setting as read: the raw value (None when neither spelling is
|
|
39
|
+
set), the spelling it came from (the new one when neither is set, so
|
|
40
|
+
a message has a name to give), whether only the earlier spelling was
|
|
41
|
+
set, and whether the two spellings disagree."""
|
|
42
|
+
value: Optional[str]
|
|
43
|
+
name: str
|
|
44
|
+
old_only: bool
|
|
45
|
+
conflict: bool
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _blank(value: Optional[str]) -> bool:
|
|
49
|
+
return value is None or not value.strip()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _toolset(value: str) -> Any:
|
|
53
|
+
return value.strip().lower()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _coder_name(value: str) -> Any:
|
|
57
|
+
from .database import validate_coder_name # no import cycle
|
|
58
|
+
try:
|
|
59
|
+
return validate_coder_name(value, "name")
|
|
60
|
+
except ValueError:
|
|
61
|
+
return ("not a valid coder name", value.strip())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _path(value: str) -> Any:
|
|
65
|
+
try:
|
|
66
|
+
return str(Path(value).expanduser())
|
|
67
|
+
except (RuntimeError, ValueError):
|
|
68
|
+
return ("not a path", value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _required_switch(value: str) -> Any:
|
|
72
|
+
return value.strip() == "1"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _exact_switch(value: str) -> Any:
|
|
76
|
+
return value == "1"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _as_given(value: str) -> Any:
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# How each read tidies its value today, which is how the two spellings
|
|
84
|
+
# are compared.
|
|
85
|
+
NORMALISE: Dict[str, Callable[[str], Any]] = {
|
|
86
|
+
"toolset": _toolset,
|
|
87
|
+
"ai_coder_name": _coder_name,
|
|
88
|
+
"workspace": _path,
|
|
89
|
+
"workspace_required": _required_switch,
|
|
90
|
+
"allow_unknown_schema": _exact_switch,
|
|
91
|
+
"project_path": _as_given,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def new_name(key: str) -> str:
|
|
96
|
+
return names.SETTINGS[key][0]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def old_name(key: str) -> str:
|
|
100
|
+
return names.SETTINGS[key][1]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def read(key: str, environ: Optional[Mapping[str, str]] = None) -> Reading:
|
|
104
|
+
"""Setting `key` (a key of names.SETTINGS) from `environ`, by default
|
|
105
|
+
this process's environment."""
|
|
106
|
+
env = os.environ if environ is None else environ
|
|
107
|
+
new, old = names.SETTINGS[key]
|
|
108
|
+
new_raw, old_raw = env.get(new), env.get(old)
|
|
109
|
+
if not _blank(new_raw) and not _blank(old_raw):
|
|
110
|
+
normalise = NORMALISE[key]
|
|
111
|
+
new_value, old_value = normalise(new_raw), normalise(old_raw)
|
|
112
|
+
if key == "workspace_required":
|
|
113
|
+
# Required if either says so: the safer reading
|
|
114
|
+
if not new_value and old_value:
|
|
115
|
+
return Reading(old_raw, old, False, False)
|
|
116
|
+
return Reading(new_raw, new, False, False)
|
|
117
|
+
return Reading(new_raw, new, False, new_value != old_value)
|
|
118
|
+
if not _blank(old_raw):
|
|
119
|
+
return Reading(old_raw, old, True, False)
|
|
120
|
+
if not _blank(new_raw):
|
|
121
|
+
return Reading(new_raw, new, False, False)
|
|
122
|
+
# Neither says anything: a blank one is still handed on as it is, so
|
|
123
|
+
# a read that treats a blank value in its own way (a blank coder name
|
|
124
|
+
# stops the server) goes on doing so.
|
|
125
|
+
if new_raw is not None:
|
|
126
|
+
return Reading(new_raw, new, False, False)
|
|
127
|
+
if old_raw is not None:
|
|
128
|
+
return Reading(old_raw, old, False, False)
|
|
129
|
+
return Reading(None, new, False, False)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def value(key: str, environ: Optional[Mapping[str, str]] = None
|
|
133
|
+
) -> Optional[str]:
|
|
134
|
+
"""Only the value of `read(key)`."""
|
|
135
|
+
return read(key, environ).value
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def conflicts(environ: Optional[Mapping[str, str]] = None) -> List[str]:
|
|
139
|
+
"""The start-up refusal for every setting whose two spellings
|
|
140
|
+
disagree, in words that name the spellings and never the values."""
|
|
141
|
+
found = []
|
|
142
|
+
for key in names.SETTINGS:
|
|
143
|
+
if read(key, environ).conflict:
|
|
144
|
+
new, old = names.SETTINGS[key]
|
|
145
|
+
found.append(
|
|
146
|
+
f"{new} and {old} are both set and say different things. "
|
|
147
|
+
f"They are two spellings of one setting ({old} is the "
|
|
148
|
+
f"earlier one, read until {names.OLD_SPELLINGS_UNTIL}): "
|
|
149
|
+
f"remove one of them from the host's configuration, or "
|
|
150
|
+
f"make them the same.")
|
|
151
|
+
return found
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def old_spellings_in_use(environ: Optional[Mapping[str, str]] = None
|
|
155
|
+
) -> List[str]:
|
|
156
|
+
"""One log line for each setting given only under its earlier
|
|
157
|
+
spelling."""
|
|
158
|
+
lines = []
|
|
159
|
+
for key in names.SETTINGS:
|
|
160
|
+
if read(key, environ).old_only:
|
|
161
|
+
new, old = names.SETTINGS[key]
|
|
162
|
+
lines.append(
|
|
163
|
+
f"{old} is the earlier spelling of {new}; it is read "
|
|
164
|
+
f"until {names.OLD_SPELLINGS_UNTIL}. Use {new} in the "
|
|
165
|
+
f"host's configuration instead.")
|
|
166
|
+
return lines
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def windows_system_root() -> str:
|
|
170
|
+
"""Windows' own folder: SystemRoot, or C:\\Windows when it is unset
|
|
171
|
+
or not a full path (a relative one would be looked up from the
|
|
172
|
+
current folder)."""
|
|
173
|
+
root = os.environ.get("SystemRoot", "")
|
|
174
|
+
return root if os.path.isabs(root) else "C:\\Windows"
|
exegete/memo_privacy.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
"""Memo privacy: QualCoder's '#####' personal-note convention.
|
|
3
|
+
|
|
4
|
+
QualCoder 4.0 lets researchers keep the tail of any memo private from
|
|
5
|
+
the AI: everything from the first '#####' marker onward is never shown
|
|
6
|
+
to the model, and AI memo updates preserve that private suffix
|
|
7
|
+
verbatim (upstream src/qualcoder/ai_memo.py:28-59 at pin 9bddf17,
|
|
8
|
+
applied to tool results in ai_mcp_server.py:210-223). This server
|
|
9
|
+
honours the same convention on every memo it returns to the client and
|
|
10
|
+
on every memo it writes, so a project touched by both tools keeps the
|
|
11
|
+
same promise to the researcher.
|
|
12
|
+
|
|
13
|
+
PERSONAL_NOTE_MARK, _SEPARATOR_CHARS, split_public_private_memo,
|
|
14
|
+
extract_ai_memo and merge_public_memo are QualCoder's own code, copied
|
|
15
|
+
from ai_memo.py:28-59 at 9bddf17 (author Kai Dröge) statement for
|
|
16
|
+
statement, with local names changed, so that the behaviour is upstream's
|
|
17
|
+
exactly; NOTICE lists them.
|
|
18
|
+
neutralize_marker, rewrite_public_memo and strip_private_memos are this
|
|
19
|
+
project's own. The behaviour the copied functions give:
|
|
20
|
+
|
|
21
|
+
- Split: the FIRST marker wins; the private suffix starts AT the
|
|
22
|
+
marker, marker included. Text with no marker is entirely public.
|
|
23
|
+
A marker at position 0 makes the whole memo private. A marker with
|
|
24
|
+
nothing after it still creates a (marker-only) private suffix.
|
|
25
|
+
- Extract: the public part is returned byte-for-byte, including any
|
|
26
|
+
whitespace that preceded the marker. No annotation is added: the
|
|
27
|
+
strip is silent by owner ruling.
|
|
28
|
+
- Merge (AI writes): the AI-provided text is itself reduced to its
|
|
29
|
+
public part first, so an AI write can never create a private zone,
|
|
30
|
+
and never reads, replaces, or deletes an existing one. An existing
|
|
31
|
+
private suffix is preserved verbatim, re-joined with the whitespace
|
|
32
|
+
run that separated the old public text from the marker. Writing an
|
|
33
|
+
empty public text against a memo with a private suffix leaves just
|
|
34
|
+
the suffix (the row keeps the researcher's private note).
|
|
35
|
+
|
|
36
|
+
The one deliberate exception, ruled by the owner for parity with
|
|
37
|
+
QualCoder's own exports: file exports (REFI-QDA, codebook files,
|
|
38
|
+
report and CSV files) carry FULL memos, marker and suffix included.
|
|
39
|
+
Export tools disclose this in their descriptions; tools that return
|
|
40
|
+
memo content into the AI conversation always strip.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import re
|
|
44
|
+
from typing import Any, Callable, Optional, Tuple
|
|
45
|
+
|
|
46
|
+
# The marker QualCoder 4.0 documents for private memo tails
|
|
47
|
+
# (upstream ai_memo.py:28).
|
|
48
|
+
PERSONAL_NOTE_MARK = "#####"
|
|
49
|
+
|
|
50
|
+
# Any run of five or more hashes contains the marker. neutralize_marker
|
|
51
|
+
# collapses the whole run: a plain .replace("#####", "####") would turn
|
|
52
|
+
# "######" into "#####" and re-form the marker it meant to remove.
|
|
53
|
+
_MARKER_RUN_RE = re.compile(r"#{5,}")
|
|
54
|
+
|
|
55
|
+
# Payload keys treated as memo text by strip_private_memos. Journal
|
|
56
|
+
# entries are exposed under 'content' by this server and are stripped
|
|
57
|
+
# at their build site instead ('content' also names file fulltext, so
|
|
58
|
+
# it cannot be a blanket key here).
|
|
59
|
+
_MEMO_KEYS = frozenset({"memo"})
|
|
60
|
+
|
|
61
|
+
# The whitespace characters upstream treats as the public/private
|
|
62
|
+
# separator when re-joining a preserved suffix (ai_memo.py:57).
|
|
63
|
+
_SEPARATOR_CHARS = " \t\r\n"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def split_public_private_memo(memo: Any) -> Tuple[str, str]:
|
|
67
|
+
"""Split a memo into (public_text, private_suffix).
|
|
68
|
+
|
|
69
|
+
The private suffix starts at the FIRST marker and includes it;
|
|
70
|
+
it is empty when no marker is present. None is treated as ''.
|
|
71
|
+
"""
|
|
72
|
+
text = "" if memo is None else str(memo)
|
|
73
|
+
mark = text.find(PERSONAL_NOTE_MARK)
|
|
74
|
+
if mark < 0:
|
|
75
|
+
return text, ""
|
|
76
|
+
return text[:mark], text[mark:]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def extract_ai_memo(memo: Any) -> str:
|
|
80
|
+
"""The part of a memo that may be shown to the AI (public text)."""
|
|
81
|
+
public, _private = split_public_private_memo(memo)
|
|
82
|
+
return public
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def neutralize_marker(text: Any) -> str:
|
|
86
|
+
"""Make a non-memo string safe to embed in a memo's public zone.
|
|
87
|
+
|
|
88
|
+
Code and category names and owner strings are written verbatim into
|
|
89
|
+
merge provenance blocks. They carry no privacy semantics of their
|
|
90
|
+
own, so a '#####' inside one would otherwise plant a private zone in
|
|
91
|
+
the target memo and hide everything after it (including the merged
|
|
92
|
+
source memo) from every AI read. Every run of five or more hashes is
|
|
93
|
+
collapsed to four; text without such a run is returned unchanged.
|
|
94
|
+
None is treated as ''.
|
|
95
|
+
"""
|
|
96
|
+
return _MARKER_RUN_RE.sub("####", "" if text is None else str(text))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def merge_public_memo(existing_memo: Any, new_public_memo: Any) -> str:
|
|
100
|
+
"""Replace a memo's public text, preserving any private suffix.
|
|
101
|
+
|
|
102
|
+
The new text is reduced to its own public part first (an AI write
|
|
103
|
+
cannot smuggle a marker in). With no existing suffix this is a
|
|
104
|
+
plain replace; with one, the suffix survives verbatim, joined by
|
|
105
|
+
the whitespace run that preceded the old marker.
|
|
106
|
+
"""
|
|
107
|
+
existing_public, private_suffix = split_public_private_memo(existing_memo)
|
|
108
|
+
public = extract_ai_memo(new_public_memo)
|
|
109
|
+
if private_suffix == "":
|
|
110
|
+
return public
|
|
111
|
+
if public == "":
|
|
112
|
+
return private_suffix
|
|
113
|
+
trimmed = existing_public.rstrip(_SEPARATOR_CHARS)
|
|
114
|
+
separator = existing_public[len(trimmed):]
|
|
115
|
+
return public + separator + private_suffix
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def rewrite_public_memo(stored: Any,
|
|
119
|
+
rewrite: Callable[[str], str]) -> Optional[str]:
|
|
120
|
+
"""Apply `rewrite` to the public part of a note; the private part is
|
|
121
|
+
carried across verbatim and never read.
|
|
122
|
+
|
|
123
|
+
The public part is a PREFIX of the stored text
|
|
124
|
+
(`split_public_private_memo` returns `text[:mark]`), so an offset in
|
|
125
|
+
the public part is an offset in the stored note and the two halves
|
|
126
|
+
concatenate back without a separator. Returns the new stored text,
|
|
127
|
+
or None when the note would not read back with the rewritten public
|
|
128
|
+
part: the rewrite made a marker inside the public part, or across its
|
|
129
|
+
boundary with the private part (a pseudonym ending in a hash, written
|
|
130
|
+
just before the marker, moves the marker earlier; Brief 2 fix round
|
|
131
|
+
1, QA-B2-3). Writing it would hide part of the researcher's note
|
|
132
|
+
from every later AI read, or move a pseudonym behind the marker, so
|
|
133
|
+
the caller leaves that note as it is and counts it.
|
|
134
|
+
|
|
135
|
+
Not `merge_public_memo`, on purpose. That function re-adds the
|
|
136
|
+
whitespace run that preceded the old marker, and a rewrite that
|
|
137
|
+
substitutes names never touches that run (a surface form cannot
|
|
138
|
+
begin or end with whitespace), so the rewritten public part already
|
|
139
|
+
ends in it: merging would double it. Concatenation gives, byte for
|
|
140
|
+
byte, what `merge_public_memo(stored, new_public.rstrip(" \\t\\r\\n"))`
|
|
141
|
+
gives, and `merge_public_memo` itself stays matched to upstream
|
|
142
|
+
(the pseudonymisation tool's note rewrite, v0.13 Brief 2, 4.3).
|
|
143
|
+
The old public part can never contain a marker (it ends where the
|
|
144
|
+
first one begins), so a marker in the new one was MADE by the
|
|
145
|
+
rewrite: hashes already in the note meeting a pseudonym, or two
|
|
146
|
+
pseudonyms meeting across a character that is not a word character.
|
|
147
|
+
One test covers both places: the new stored text, split again, must
|
|
148
|
+
give back exactly the new public part.
|
|
149
|
+
"""
|
|
150
|
+
public, private = split_public_private_memo(stored)
|
|
151
|
+
new_public = rewrite(public)
|
|
152
|
+
new_stored = new_public + private
|
|
153
|
+
if split_public_private_memo(new_stored)[0] != new_public:
|
|
154
|
+
return None
|
|
155
|
+
return new_stored
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def strip_private_memos(value: Any) -> Any:
|
|
159
|
+
"""Recursively strip private suffixes from a result payload.
|
|
160
|
+
|
|
161
|
+
Walks dicts, lists and tuples; every string under a 'memo' key is
|
|
162
|
+
reduced to its public part (upstream's payload sanitiser contract,
|
|
163
|
+
ai_mcp_server.py:210-223). Everything else passes through
|
|
164
|
+
unchanged. Returns a new structure; the input is not mutated.
|
|
165
|
+
"""
|
|
166
|
+
if isinstance(value, dict):
|
|
167
|
+
cleaned = {}
|
|
168
|
+
for key, item in value.items():
|
|
169
|
+
if key in _MEMO_KEYS and isinstance(item, str):
|
|
170
|
+
cleaned[key] = extract_ai_memo(item)
|
|
171
|
+
else:
|
|
172
|
+
cleaned[key] = strip_private_memos(item)
|
|
173
|
+
return cleaned
|
|
174
|
+
if isinstance(value, list):
|
|
175
|
+
return [strip_private_memos(item) for item in value]
|
|
176
|
+
if isinstance(value, tuple):
|
|
177
|
+
return tuple(strip_private_memos(item) for item in value)
|
|
178
|
+
return value
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# What every tool that writes the assistant's text into a memo, a note or
|
|
182
|
+
# a journal entry says of that text, in its description (v0.14, the
|
|
183
|
+
# claims audit's item 4).
|
|
184
|
+
MARKER_REFUSED_DESCRIPTION = (
|
|
185
|
+
"Text containing '#####', QualCoder's private-note marker, is refused "
|
|
186
|
+
"before anything is written or backed up: what follows the marker is "
|
|
187
|
+
"the researcher's own, written in QualCoder.")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def private_marker_refusal(text: Any, field: str) -> Optional[str]:
|
|
191
|
+
"""The refusal for assistant-supplied text that holds the private-note
|
|
192
|
+
marker, or None when it holds none (v0.14, the claims audit's item 4).
|
|
193
|
+
|
|
194
|
+
QualCoder's own AI server reduces such text to its public part and
|
|
195
|
+
writes that, silently (`merge_public_memo`, from ai_memo.py). Here that
|
|
196
|
+
silence destroyed notes: "##### Researcher note" has an empty public
|
|
197
|
+
part, so update_annotation deleted the annotation, set_memo emptied
|
|
198
|
+
the memo, and a journal entry lost everything after the marker with
|
|
199
|
+
no word said. A researcher who knows QualCoder 4.0's convention and
|
|
200
|
+
asks the assistant to "make this note private" meets exactly that.
|
|
201
|
+
So this server refuses instead, and every tool that writes the
|
|
202
|
+
assistant's text into a memo, a note or an entry checks before its
|
|
203
|
+
backup. The departure from QualCoder is deliberate, for that reason.
|
|
204
|
+
|
|
205
|
+
The refusal does not spell the marker out: this server's answers stay
|
|
206
|
+
free of it, so that a marker in any answer always means a leak.
|
|
207
|
+
"""
|
|
208
|
+
if not isinstance(text, str) or PERSONAL_NOTE_MARK not in text:
|
|
209
|
+
return None
|
|
210
|
+
return (f"{field} contains QualCoder's private-note marker (five hash "
|
|
211
|
+
f"signs in a row); nothing was written. What follows that "
|
|
212
|
+
f"marker is the researcher's private note, written in QualCoder "
|
|
213
|
+
f"and never shown to the assistant, so this server does not "
|
|
214
|
+
f"write it: send the text without the marker. A note the "
|
|
215
|
+
f"researcher wants to keep private is written in QualCoder.")
|
exegete/names.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
"""Every name this program goes by, typed once.
|
|
3
|
+
|
|
4
|
+
The project was called qualcoder-mcp until 0.14.0-alpha and is called
|
|
5
|
+
Exegete from 0.14.1-alpha. The code reads its runtime names from here,
|
|
6
|
+
messages included, so that each is typed in one place (a name that is
|
|
7
|
+
also part of a path or an import appears there too: the package folder
|
|
8
|
+
`src/exegete`, and `exegete.server:main` in pyproject.toml).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# The PyPI distribution, the command it installs and the Python package.
|
|
12
|
+
DISTRIBUTION = "exegete"
|
|
13
|
+
COMMAND = "exegete"
|
|
14
|
+
PACKAGE = "exegete"
|
|
15
|
+
|
|
16
|
+
# The old names, still answered: the old name's package on PyPI carries
|
|
17
|
+
# the command `qualcoder-mcp`, and a two-file stand-in package
|
|
18
|
+
# `qualcoder_mcp` (src/qualcoder_mcp, and its copy under
|
|
19
|
+
# packaging/pypi-old-name) hands over to exegete.server:main, so that
|
|
20
|
+
# `qualcoder-mcp` and `python -m qualcoder_mcp.server` start Exegete.
|
|
21
|
+
OLD_DISTRIBUTION = "qualcoder-mcp"
|
|
22
|
+
OLD_COMMAND = "qualcoder-mcp"
|
|
23
|
+
OLD_PACKAGE = "qualcoder_mcp"
|
|
24
|
+
|
|
25
|
+
# The server's settings (environment variables), each under its new
|
|
26
|
+
# spelling and the earlier one, which is still read until v1.0: one
|
|
27
|
+
# table, read only through env_settings.read (a test forbids any other
|
|
28
|
+
# environment read in the package). When both spellings are set they
|
|
29
|
+
# must agree, or the server does not start.
|
|
30
|
+
ENV_PREFIX = "EXEGETE_"
|
|
31
|
+
SETTINGS = {
|
|
32
|
+
"toolset": ("EXEGETE_TOOLSET", "QUALCODER_MCP_TOOLSET"),
|
|
33
|
+
"ai_coder_name": ("EXEGETE_AI_CODER_NAME",
|
|
34
|
+
"QUALCODER_MCP_AI_CODER_NAME"),
|
|
35
|
+
"workspace": ("EXEGETE_WORKSPACE", "QUALCODER_MCP_WORKSPACE"),
|
|
36
|
+
"workspace_required": ("EXEGETE_WORKSPACE_REQUIRED",
|
|
37
|
+
"QUALCODER_MCP_WORKSPACE_REQUIRED"),
|
|
38
|
+
"allow_unknown_schema": ("EXEGETE_ALLOW_UNKNOWN_SCHEMA",
|
|
39
|
+
"QUALCODER_MCP_ALLOW_UNKNOWN_SCHEMA"),
|
|
40
|
+
"project_path": ("EXEGETE_PROJECT_PATH", "QUALCODER_PROJECT_PATH"),
|
|
41
|
+
}
|
|
42
|
+
# Until when the earlier spellings are read, as messages say it.
|
|
43
|
+
OLD_SPELLINGS_UNTIL = "v1.0"
|
|
44
|
+
|
|
45
|
+
# The state folder in the home folder, and the one it is moved from once,
|
|
46
|
+
# whole, at the first real start, with a link left under the old name (a
|
|
47
|
+
# junction on Windows).
|
|
48
|
+
STATE_FOLDER = ".exegete"
|
|
49
|
+
OLD_STATE_FOLDER = ".qualcoder_mcp"
|
|
50
|
+
|
|
51
|
+
# The workspace inside the Documents folder: where copies and new projects
|
|
52
|
+
# go when no workspace is set (installs from PyPI or from the source; the
|
|
53
|
+
# desktop extension always sets one, `~/QualCoder projects`). The earlier
|
|
54
|
+
# one is never moved or emptied, and the project listing still finds its
|
|
55
|
+
# projects (it walks ~/Documents).
|
|
56
|
+
WORKSPACE_FOLDER = "Exegete projects"
|
|
57
|
+
OLD_WORKSPACE_FOLDER = "Qualcoder MCP Projects"
|
|
58
|
+
|
|
59
|
+
# The resources' address scheme; the earlier one is still answered, and
|
|
60
|
+
# no longer listed, until v1.0. (QualCoder's own
|
|
61
|
+
# MCP server uses `qualcoder://`, hence the departure.)
|
|
62
|
+
RESOURCE_SCHEME = "exegete"
|
|
63
|
+
OLD_RESOURCE_SCHEME = "qualcoder"
|
|
64
|
+
|
|
65
|
+
# The server's own name: in the MCP handshake, in what it says, in the
|
|
66
|
+
# `about` of the projects it creates and as the producer of its exports.
|
|
67
|
+
# Fixed by the owner's ruling (29 September 2026).
|
|
68
|
+
SERVER_NAME = "Exegete"
|