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 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
@@ -0,0 +1,5 @@
1
+ """Enable `python -m tier`."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
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