tier-ctx 1.2.1__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.
- tier/__init__.py +53 -0
- tier/__main__.py +5 -0
- tier/cache.py +325 -0
- tier/cli.py +467 -0
- tier/context.py +369 -0
- tier/converter.py +451 -0
- tier/doctor.py +215 -0
- tier/installer.py +1482 -0
- tier/lint.py +58 -0
- tier/mcp/__init__.py +9 -0
- tier/mcp/__main__.py +5 -0
- tier/mcp/sdk_server.py +183 -0
- tier/mcp/server.py +567 -0
- tier/memory.py +171 -0
- tier/models.py +202 -0
- tier/parser.py +301 -0
- tier/pipe.py +328 -0
- tier/plugin.py +167 -0
- tier/repo.py +1694 -0
- tier/retrieval.py +385 -0
- tier/tokens.py +294 -0
- tier/ui.py +129 -0
- tier_ctx-1.2.1.dist-info/METADATA +273 -0
- tier_ctx-1.2.1.dist-info/RECORD +28 -0
- tier_ctx-1.2.1.dist-info/WHEEL +5 -0
- tier_ctx-1.2.1.dist-info/entry_points.txt +4 -0
- tier_ctx-1.2.1.dist-info/licenses/LICENSE +21 -0
- tier_ctx-1.2.1.dist-info/top_level.txt +1 -0
tier/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""TIER: layered context, repository graphs, code slicing, and agent memory."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .models import (
|
|
5
|
+
VERSION, Doc, Section, SIGIL_CHARS, SEC_RE, HDR_RE, TABLE_RE, DATA_OPEN_RE,
|
|
6
|
+
RULE_START, STOP, SHELL_LANGS, TABLE_MODES, _W, _atomic_write, _clip, _inside,
|
|
7
|
+
_read_text, _table_count,
|
|
8
|
+
)
|
|
9
|
+
from .tokens import count_tokens
|
|
10
|
+
from .parser import (
|
|
11
|
+
_csv_cells, _data_closes, _data_open, _kv_split, _live_lines, _load, _parse_kv,
|
|
12
|
+
_render_table, _repo_file_meta, _table_rows, dumps, is_repo_doc, nodes, parse,
|
|
13
|
+
render_lines, render_section, to_obj, wrap_untrusted,
|
|
14
|
+
)
|
|
15
|
+
from .lint import lint
|
|
16
|
+
from .converter import (
|
|
17
|
+
_auto_abstract, _clip as _converter_clip, _csv_row, _esc, _json_body, _json_cell,
|
|
18
|
+
_json_kv_lines, _json_scalar, _make_id, _md_cells, _one_line_text, _records_table,
|
|
19
|
+
_ref_topics, _table_name, from_json, from_md, from_payload, to_md,
|
|
20
|
+
)
|
|
21
|
+
from .retrieval import (
|
|
22
|
+
QSTOP, SYNONYMS, Session, _find_legacy, _index_tokens, _is_navigation_section,
|
|
23
|
+
_is_test_module, _norm, _query_terms, _rule_count, _synonym_map, _trigram_sim,
|
|
24
|
+
_trigrams, find, get, index, resolve, split_identifier,
|
|
25
|
+
)
|
|
26
|
+
from .repo import (
|
|
27
|
+
JS_CONTROL, LANG_BY_EXT, REPO_ENTRY_NAMES, REPO_SKIP_DIRS, REPO_SOURCE_EXTS,
|
|
28
|
+
_brace_end, _build_graph, _changed_symbols, _extract_symbols, _git_changed_paths,
|
|
29
|
+
_go_module, _go_symbols, _js_arrow_name, _js_expr_end, _js_rhs_arrow, _js_symbols,
|
|
30
|
+
_leading_comment, _py_module_name, _py_symbols, _reachable, _repo_add_deps, _repo_id,
|
|
31
|
+
_repo_info, _repo_section, _repo_summary, _resolve_go_imports, _resolve_impact_target,
|
|
32
|
+
_resolve_js_imports, _resolve_module, _resolve_py_imports, _resolve_rust_mods,
|
|
33
|
+
_rust_symbols, _sha256, _sig_line, _slice_range, _strip_code_strings, _symbol_rows,
|
|
34
|
+
_symbol_table, _top_symbol_names, from_repo, impact, render_impact, repo_source_files,
|
|
35
|
+
slice, slice_symbols, sync_repo,
|
|
36
|
+
)
|
|
37
|
+
from .memory import _validate_section_body, remember
|
|
38
|
+
from .pipe import (
|
|
39
|
+
LOG_CMD_RE, LOG_ERR_RE, LOG_WARN_RE, _fit_text, _log_shape, _text_fields, _tokens,
|
|
40
|
+
detect_stream_format, from_stream, hook_process, log_doc, pipe_compress,
|
|
41
|
+
)
|
|
42
|
+
from .plugin import (
|
|
43
|
+
TIER_BEGIN, TIER_END, _hook_commands, _looks_like_tier_hook, _merge_hook_settings,
|
|
44
|
+
_plugin_instructions, _write_marked_instructions, init_plugin,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__version__ = VERSION
|
|
48
|
+
__all__ = [
|
|
49
|
+
"VERSION", "Doc", "Section", "parse", "dumps", "index", "find", "get", "slice",
|
|
50
|
+
"slice_symbols", "impact", "sync_repo", "from_repo", "remember", "Session",
|
|
51
|
+
"wrap_untrusted", "from_md", "from_json", "from_payload", "to_md", "lint", "nodes",
|
|
52
|
+
"count_tokens", "pipe_compress", "hook_process", "init_plugin",
|
|
53
|
+
]
|
tier/__main__.py
ADDED
tier/cache.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Repository-index cache resolution.
|
|
2
|
+
|
|
3
|
+
`repo.tier` is a derived artifact, so requiring users to track one in their tree
|
|
4
|
+
is friction. This module resolves where the map for a given root lives:
|
|
5
|
+
|
|
6
|
+
1. An explicit ``*.tier`` path always wins (caller-supplied).
|
|
7
|
+
2. ``<root>/repo.tier`` when it exists, which keeps checked-in maps working and
|
|
8
|
+
the CI freshness gate meaningful.
|
|
9
|
+
3. ``<root>/.git/info/tier/repo.tier`` when ``.git`` is a directory. Git ignores
|
|
10
|
+
everything under ``.git/info``, so this never dirties the working tree and
|
|
11
|
+
needs no ``.gitignore`` entry.
|
|
12
|
+
4. ``<cache_root>/<sha256(realpath(root))[:12]>/repo.tier`` otherwise, which is
|
|
13
|
+
outside the worktree entirely.
|
|
14
|
+
|
|
15
|
+
Nothing here writes unless ``ensure_doc`` is called.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from .models import _atomic_write, _inside
|
|
24
|
+
from .parser import _load, dumps, is_repo_doc
|
|
25
|
+
from .repo import from_repo, sync_repo
|
|
26
|
+
|
|
27
|
+
CACHE_DIR_ENV = "TIER_CACHE_DIR"
|
|
28
|
+
CACHE_FILENAME = "repo.tier"
|
|
29
|
+
INGEST_DIRNAME = "ingest"
|
|
30
|
+
INGEST_KEEP = 32
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cache_root() -> str:
|
|
34
|
+
"""Root directory for caches that cannot live inside the repository."""
|
|
35
|
+
override = os.environ.get(CACHE_DIR_ENV)
|
|
36
|
+
if override:
|
|
37
|
+
return os.path.abspath(os.path.expanduser(override))
|
|
38
|
+
if sys.platform.startswith("win"):
|
|
39
|
+
base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
|
|
40
|
+
if base:
|
|
41
|
+
return os.path.join(base, "tier", "cache")
|
|
42
|
+
xdg = os.environ.get("XDG_CACHE_HOME")
|
|
43
|
+
if xdg:
|
|
44
|
+
return os.path.join(os.path.abspath(os.path.expanduser(xdg)), "tier")
|
|
45
|
+
return os.path.join(os.path.expanduser("~"), ".cache", "tier")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _git_pointer(root: str) -> str:
|
|
49
|
+
"""Resolve a `.git` pointer file (git worktree, submodule) to its gitdir.
|
|
50
|
+
|
|
51
|
+
Returns "" when `.git` is absent, is a directory, or the pointer is
|
|
52
|
+
unreadable. A relative target is resolved against the directory containing
|
|
53
|
+
`.git`, which is what Git itself does.
|
|
54
|
+
"""
|
|
55
|
+
dot_git = os.path.join(root, ".git")
|
|
56
|
+
if not os.path.isfile(dot_git):
|
|
57
|
+
return ""
|
|
58
|
+
try:
|
|
59
|
+
with open(dot_git, encoding="utf-8", errors="replace") as f:
|
|
60
|
+
first = f.readline().strip()
|
|
61
|
+
except OSError:
|
|
62
|
+
return ""
|
|
63
|
+
if not first.lower().startswith("gitdir:"):
|
|
64
|
+
return ""
|
|
65
|
+
target = first.split(":", 1)[1].strip()
|
|
66
|
+
if not target:
|
|
67
|
+
return ""
|
|
68
|
+
if not os.path.isabs(target):
|
|
69
|
+
target = os.path.join(os.path.dirname(dot_git), target)
|
|
70
|
+
return os.path.abspath(target)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _common_git_dir(git_dir: str) -> str:
|
|
74
|
+
"""Return the shared git directory for a per-worktree admin directory.
|
|
75
|
+
|
|
76
|
+
A linked worktree's gitdir contains a `commondir` file pointing at the main
|
|
77
|
+
repository's `.git`. Using it means every worktree of one repository shares
|
|
78
|
+
a single map instead of one per checkout.
|
|
79
|
+
"""
|
|
80
|
+
if not git_dir:
|
|
81
|
+
return ""
|
|
82
|
+
marker = os.path.join(git_dir, "commondir")
|
|
83
|
+
if not os.path.isfile(marker):
|
|
84
|
+
return git_dir
|
|
85
|
+
try:
|
|
86
|
+
with open(marker, encoding="utf-8", errors="replace") as f:
|
|
87
|
+
target = f.read().strip()
|
|
88
|
+
except OSError:
|
|
89
|
+
return git_dir
|
|
90
|
+
if not target:
|
|
91
|
+
return git_dir
|
|
92
|
+
if not os.path.isabs(target):
|
|
93
|
+
target = os.path.join(git_dir, target)
|
|
94
|
+
return os.path.abspath(target)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _git_dir(root: str) -> str:
|
|
98
|
+
"""Return the git metadata directory for `root`, or "" when not a repository.
|
|
99
|
+
|
|
100
|
+
Handles both layouts: a plain `.git` directory, and a `.git` pointer file as
|
|
101
|
+
used by `git worktree` and submodules.
|
|
102
|
+
|
|
103
|
+
A linked worktree's pointer targets its own admin directory under
|
|
104
|
+
`<main>/.git/worktrees/<name>`, and a submodule's targets
|
|
105
|
+
`<super>/.git/modules/<name>`. Those per-checkout directories are used as
|
|
106
|
+
given rather than resolved to the shared common directory: worktrees exist
|
|
107
|
+
to hold different branches, so one shared map would be invalidated on every
|
|
108
|
+
checkout switch and could serve stale content.
|
|
109
|
+
"""
|
|
110
|
+
dot_git = os.path.join(root, ".git")
|
|
111
|
+
if os.path.isdir(dot_git):
|
|
112
|
+
return _common_git_dir(dot_git)
|
|
113
|
+
pointer = _git_pointer(root)
|
|
114
|
+
if pointer:
|
|
115
|
+
return pointer or _common_git_dir(pointer)
|
|
116
|
+
return ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _writable_dir(path: str) -> bool:
|
|
120
|
+
if os.path.isdir(path):
|
|
121
|
+
return os.access(path, os.W_OK)
|
|
122
|
+
parent = os.path.dirname(path)
|
|
123
|
+
while parent and not os.path.exists(parent):
|
|
124
|
+
nxt = os.path.dirname(parent)
|
|
125
|
+
if nxt == parent:
|
|
126
|
+
break
|
|
127
|
+
parent = nxt
|
|
128
|
+
return os.path.isdir(parent) and os.access(parent, os.W_OK)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cache_path(root: str) -> str:
|
|
132
|
+
"""The invisible cache location for a repository root."""
|
|
133
|
+
root = os.path.abspath(root)
|
|
134
|
+
tracked = os.path.join(root, CACHE_FILENAME)
|
|
135
|
+
if os.path.isfile(tracked):
|
|
136
|
+
return tracked
|
|
137
|
+
git = _git_dir(root)
|
|
138
|
+
if git:
|
|
139
|
+
info_dir = os.path.join(git, "info", "tier")
|
|
140
|
+
candidate = os.path.join(info_dir, CACHE_FILENAME)
|
|
141
|
+
if _writable_dir(info_dir):
|
|
142
|
+
return candidate
|
|
143
|
+
digest = hashlib.sha256(os.path.realpath(root).encode("utf-8", "replace")).hexdigest()[:12]
|
|
144
|
+
return os.path.join(cache_root(), digest, CACHE_FILENAME)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cache_kind(path: str, root: str) -> str:
|
|
148
|
+
"""Classify a resolved path: ``tracked``, ``git``, or ``cache``."""
|
|
149
|
+
path = os.path.abspath(path)
|
|
150
|
+
tracked = os.path.join(os.path.abspath(root), CACHE_FILENAME)
|
|
151
|
+
if path == tracked:
|
|
152
|
+
return "tracked"
|
|
153
|
+
# Match on the `info/tier` segment rather than a literal `.git`: a linked
|
|
154
|
+
# worktree's gitdir lives outside the checkout entirely.
|
|
155
|
+
norm = path.replace(os.sep, "/")
|
|
156
|
+
if "/info/tier/" in norm:
|
|
157
|
+
return "git"
|
|
158
|
+
return "cache"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_cache_dir(path: str, root: str = "") -> bool:
|
|
162
|
+
"""True when `path` is a directory this module owns (so listings skip it)."""
|
|
163
|
+
path = os.path.abspath(path)
|
|
164
|
+
parts = path.replace(os.sep, "/").split("/")
|
|
165
|
+
if ".git/info/tier" in "/".join(parts) or "/info/tier" in "/".join(parts):
|
|
166
|
+
return True
|
|
167
|
+
roots = {os.path.abspath(cache_root())}
|
|
168
|
+
digest_dir = os.path.dirname(cache_path(root)) if root else ""
|
|
169
|
+
if digest_dir:
|
|
170
|
+
roots.add(digest_dir)
|
|
171
|
+
return any(path == r or path.startswith(r + os.sep) for r in roots if r)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _invisible_cache_dir(root: str) -> str:
|
|
175
|
+
"""Directory for cache side artifacts, never inside the working tree.
|
|
176
|
+
|
|
177
|
+
Deliberately ignores a tracked `<root>/repo.tier`: the map may be checked in,
|
|
178
|
+
but scratch artifacts must not appear as untracked files in the checkout.
|
|
179
|
+
"""
|
|
180
|
+
root = os.path.abspath(root)
|
|
181
|
+
git = _git_dir(root)
|
|
182
|
+
if git:
|
|
183
|
+
info_dir = os.path.join(git, "info", "tier")
|
|
184
|
+
if _writable_dir(info_dir):
|
|
185
|
+
return info_dir
|
|
186
|
+
digest = hashlib.sha256(os.path.realpath(root).encode("utf-8", "replace")).hexdigest()[:12]
|
|
187
|
+
return os.path.join(cache_root(), digest)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def is_cache_path(path: str, root: str = "") -> bool:
|
|
191
|
+
"""True when `path` resolves inside the invisible cache directory for `root`.
|
|
192
|
+
|
|
193
|
+
Stricter than `is_cache_dir`, which matches any path containing an
|
|
194
|
+
`info/tier` segment because it only exists to prune directory listings.
|
|
195
|
+
Confinement decisions need the exact resolved directory.
|
|
196
|
+
"""
|
|
197
|
+
if not root:
|
|
198
|
+
return False
|
|
199
|
+
return _inside(os.path.realpath(_invisible_cache_dir(root)), os.path.realpath(path))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def ingest_dir(root: str = "") -> str:
|
|
203
|
+
"""Sibling of the map where over-budget ingest artifacts are stored.
|
|
204
|
+
|
|
205
|
+
Inherits the map's invisible location, so it lands in
|
|
206
|
+
`<gitdir>/info/tier/ingest/` for a repository and in the per-root user cache
|
|
207
|
+
otherwise, and stays readable by every tool that can read the map.
|
|
208
|
+
"""
|
|
209
|
+
return os.path.join(_invisible_cache_dir(root or os.getcwd()), INGEST_DIRNAME)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def prune_ingest(directory: str, keep: int = INGEST_KEEP) -> list:
|
|
213
|
+
"""Delete all but the newest `keep` ingest artifacts. Returns deleted paths."""
|
|
214
|
+
try:
|
|
215
|
+
names = [n for n in os.listdir(directory) if n.startswith("ingest-") and n.endswith(".tier")]
|
|
216
|
+
except OSError:
|
|
217
|
+
return []
|
|
218
|
+
|
|
219
|
+
def mtime(name):
|
|
220
|
+
try:
|
|
221
|
+
return os.stat(os.path.join(directory, name)).st_mtime
|
|
222
|
+
except OSError:
|
|
223
|
+
return 0.0
|
|
224
|
+
|
|
225
|
+
names.sort(key=mtime, reverse=True)
|
|
226
|
+
removed = []
|
|
227
|
+
for name in names[max(0, int(keep)):]:
|
|
228
|
+
path = os.path.join(directory, name)
|
|
229
|
+
try:
|
|
230
|
+
os.unlink(path)
|
|
231
|
+
removed.append(path)
|
|
232
|
+
except OSError:
|
|
233
|
+
pass
|
|
234
|
+
return removed
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _work_root_for(path: str, hint: str = "") -> str:
|
|
238
|
+
"""Infer the repository a map describes when it lives outside that repo.
|
|
239
|
+
|
|
240
|
+
An invisible cache sits under `<gitdir>/info/tier/` or
|
|
241
|
+
`<cache_root>/<digest>/`, neither of which contains the sources. The owning
|
|
242
|
+
root is recovered from the `.owner` manifest written beside the map; the
|
|
243
|
+
literal `/.git/info/tier/` path is still handled for older maps.
|
|
244
|
+
"""
|
|
245
|
+
path = os.path.abspath(path)
|
|
246
|
+
parent = os.path.dirname(path)
|
|
247
|
+
owner = os.path.join(parent, ".owner")
|
|
248
|
+
if os.path.isfile(owner):
|
|
249
|
+
try:
|
|
250
|
+
with open(owner, encoding="utf-8") as f:
|
|
251
|
+
value = f.read().strip()
|
|
252
|
+
if value:
|
|
253
|
+
return value
|
|
254
|
+
except OSError:
|
|
255
|
+
pass
|
|
256
|
+
norm = path.replace(os.sep, "/")
|
|
257
|
+
marker = "/.git/info/tier/"
|
|
258
|
+
if marker in norm:
|
|
259
|
+
return norm.split(marker, 1)[0].replace("/", os.sep) or os.sep
|
|
260
|
+
return hint or parent
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def resolve_doc(target=None, root=None):
|
|
264
|
+
"""Resolve a target to ``(doc_path, work_root, kind)``.
|
|
265
|
+
|
|
266
|
+
`target` may be an explicit ``.tier`` file, a directory, or None.
|
|
267
|
+
"""
|
|
268
|
+
base = os.path.abspath(root or os.getcwd())
|
|
269
|
+
if target is None or str(target).strip() in ("", "."):
|
|
270
|
+
target_path = base
|
|
271
|
+
elif os.path.isabs(str(target)):
|
|
272
|
+
target_path = os.path.abspath(str(target))
|
|
273
|
+
else:
|
|
274
|
+
target_path = os.path.abspath(os.path.join(base, str(target)))
|
|
275
|
+
|
|
276
|
+
if os.path.isfile(target_path):
|
|
277
|
+
if not target_path.endswith(".tier"):
|
|
278
|
+
raise ValueError(f"not a .tier document: {target}")
|
|
279
|
+
return target_path, _work_root_for(target_path, os.path.dirname(target_path)), \
|
|
280
|
+
cache_kind(target_path, base)
|
|
281
|
+
if os.path.isdir(target_path):
|
|
282
|
+
return cache_path(target_path), target_path, cache_kind(cache_path(target_path), target_path)
|
|
283
|
+
raise ValueError(f"no such file or directory: {target}")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def ensure_doc(target=None, root=None, session=None, session_key: str = "",
|
|
287
|
+
write: bool = True):
|
|
288
|
+
"""Return ``(path, doc, work_root)``, building or syncing the map as needed.
|
|
289
|
+
|
|
290
|
+
``work_root`` is the directory the map describes, which is not always the
|
|
291
|
+
directory holding the map: an invisible cache lives elsewhere, so slices and
|
|
292
|
+
impact need the original root to resolve source files.
|
|
293
|
+
"""
|
|
294
|
+
path, work_root, _kind = resolve_doc(target, root)
|
|
295
|
+
if os.path.isfile(path):
|
|
296
|
+
doc = _load(path)
|
|
297
|
+
if is_repo_doc(doc):
|
|
298
|
+
try:
|
|
299
|
+
res = sync_repo(path, root=work_root, session=session,
|
|
300
|
+
session_key=session_key or path, write=write)
|
|
301
|
+
return path, res["doc"], work_root
|
|
302
|
+
except (OSError, ValueError):
|
|
303
|
+
return path, doc, work_root
|
|
304
|
+
return path, doc, work_root
|
|
305
|
+
|
|
306
|
+
doc = from_repo(work_root)
|
|
307
|
+
if write:
|
|
308
|
+
_atomic_write(path, dumps(doc))
|
|
309
|
+
_write_owner(path, work_root)
|
|
310
|
+
return path, doc, work_root
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _write_owner(path: str, work_root: str) -> None:
|
|
314
|
+
"""Record which repository a cached map describes.
|
|
315
|
+
|
|
316
|
+
Needed whenever the map is not beside its sources, which includes the common
|
|
317
|
+
case of a map under `<gitdir>/info/tier/`: in a linked worktree that gitdir
|
|
318
|
+
lives outside the checkout, so the path alone cannot identify the root.
|
|
319
|
+
"""
|
|
320
|
+
if os.path.dirname(os.path.abspath(path)) == os.path.abspath(work_root):
|
|
321
|
+
return
|
|
322
|
+
try:
|
|
323
|
+
_atomic_write(os.path.join(os.path.dirname(path), ".owner"), work_root + "\n")
|
|
324
|
+
except OSError:
|
|
325
|
+
pass
|