context-loader 0.1.8__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.
@@ -0,0 +1,3 @@
1
+ """Codex Project Context Loader."""
2
+
3
+ __version__ = "0.1.8"
@@ -0,0 +1,197 @@
1
+ """Load one deterministic project-context result for all output formats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ from . import __version__
12
+ from .collect import (
13
+ AgentsSectionAuditEntry,
14
+ AgentsSelectionAudit,
15
+ AgentsSelectionInputError,
16
+ ProjectContext,
17
+ collect_project_context,
18
+ )
19
+ from .git import ContextLoaderError, collect_repository
20
+ from .render import render_markdown_with_details, rendered_source_contents
21
+
22
+ JSON_SCHEMA_VERSION = 1
23
+ TOOL_NAME = "context-loader"
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class ToolIdentity:
28
+ name: str
29
+ version: str
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class RepositoryIdentity:
34
+ requested_path: Path
35
+ canonical_root: Path
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class ProjectContextSource:
40
+ ordinal: int
41
+ kind: str
42
+ scope: str
43
+ path: Path
44
+ content_sha256: str
45
+ content: str
46
+ selection: AgentsSelectionAudit | None = None
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ProjectContextResult:
51
+ schema_version: int
52
+ tool: ToolIdentity
53
+ repository: RepositoryIdentity
54
+ sources: tuple[ProjectContextSource, ...]
55
+ context: str
56
+ context_sha256: str
57
+ warnings: tuple[str, ...]
58
+
59
+
60
+ def _text_sha256(content: str) -> str:
61
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
62
+
63
+
64
+ def source_scope_for_path(path: Path, canonical_root: Path) -> str:
65
+ """Classify a canonical source path without following or reading it."""
66
+ try:
67
+ path.relative_to(canonical_root)
68
+ except ValueError:
69
+ return "global"
70
+ return "repository"
71
+
72
+
73
+ def _source_kind(name: str) -> str:
74
+ if name == "AGENTS.md":
75
+ return "agents"
76
+ if name == "README.md":
77
+ return "readme"
78
+ return "entry_file"
79
+
80
+
81
+ def _sources(
82
+ canonical_root: Path,
83
+ project: ProjectContext,
84
+ included_sections: tuple[str, ...],
85
+ ) -> tuple[ProjectContextSource, ...]:
86
+ sources: list[ProjectContextSource] = []
87
+ for ordinal, (source, content) in enumerate(
88
+ rendered_source_contents(project, included_sections)
89
+ ):
90
+ path = canonical_root / source.name
91
+ sources.append(
92
+ ProjectContextSource(
93
+ ordinal=ordinal,
94
+ kind=_source_kind(source.name),
95
+ scope=source_scope_for_path(path, canonical_root),
96
+ path=path,
97
+ content_sha256=_text_sha256(content),
98
+ content=content,
99
+ selection=source.selection,
100
+ )
101
+ )
102
+ return tuple(sources)
103
+
104
+
105
+ def load_project_context(
106
+ repo: str | os.PathLike[str],
107
+ *,
108
+ require_repository_root: bool = False,
109
+ focus: str | None = None,
110
+ path: str | None = None,
111
+ ) -> ProjectContextResult:
112
+ """Collect one repository once and return its deterministic machine-readable result."""
113
+ location, state = collect_repository(
114
+ repo,
115
+ require_canonical_root=require_repository_root,
116
+ )
117
+ try:
118
+ project = collect_project_context(state.repository, focus=focus, path=path)
119
+ except AgentsSelectionInputError as exc:
120
+ raise ContextLoaderError(str(exc), exit_code=2) from None
121
+ rendered = render_markdown_with_details(state, project)
122
+ context = rendered.output.decode("utf-8")
123
+ return ProjectContextResult(
124
+ schema_version=JSON_SCHEMA_VERSION,
125
+ tool=ToolIdentity(name=TOOL_NAME, version=__version__),
126
+ repository=RepositoryIdentity(
127
+ requested_path=location.requested_path,
128
+ canonical_root=location.canonical_root,
129
+ ),
130
+ sources=_sources(state.repository, project, rendered.included_sections),
131
+ context=context,
132
+ context_sha256=hashlib.sha256(rendered.output).hexdigest(),
133
+ warnings=(),
134
+ )
135
+
136
+
137
+ def _selection_document(selection: AgentsSelectionAudit) -> dict[str, object]:
138
+ def entry_document(entry: AgentsSectionAuditEntry) -> dict[str, object]:
139
+ return {
140
+ "heading": entry.heading,
141
+ "heading_level": entry.heading_level,
142
+ "reasons": list(entry.reasons),
143
+ }
144
+
145
+ return {
146
+ "source": selection.source,
147
+ "selected_sections": [entry_document(entry) for entry in selection.selected_sections],
148
+ "indexed_only_sections": [
149
+ entry_document(entry) for entry in selection.indexed_only_sections
150
+ ],
151
+ "chars_selected": selection.chars_selected,
152
+ "chars_omitted": selection.chars_omitted,
153
+ "truncated": selection.truncated,
154
+ "parse_fallback": selection.parse_fallback,
155
+ "source_scan_truncated": selection.source_scan_truncated,
156
+ "index_truncated": selection.index_truncated,
157
+ }
158
+
159
+
160
+ def _source_document(source: ProjectContextSource) -> dict[str, object]:
161
+ document: dict[str, object] = {
162
+ "ordinal": source.ordinal,
163
+ "kind": source.kind,
164
+ "scope": source.scope,
165
+ "path": os.fspath(source.path),
166
+ "content_sha256": source.content_sha256,
167
+ "content": source.content,
168
+ }
169
+ if source.selection is not None:
170
+ document["selection"] = _selection_document(source.selection)
171
+ return document
172
+
173
+
174
+ def render_json(result: ProjectContextResult) -> bytes:
175
+ """Serialize one result as stable UTF-8 JSON followed by exactly one newline."""
176
+ document = {
177
+ "schema_version": result.schema_version,
178
+ "tool": {
179
+ "name": result.tool.name,
180
+ "version": result.tool.version,
181
+ },
182
+ "repository": {
183
+ "requested_path": os.fspath(result.repository.requested_path),
184
+ "canonical_root": os.fspath(result.repository.canonical_root),
185
+ },
186
+ "sources": [_source_document(source) for source in result.sources],
187
+ "context": result.context,
188
+ "context_sha256": result.context_sha256,
189
+ "warnings": list(result.warnings),
190
+ }
191
+ serialized = json.dumps(
192
+ document,
193
+ ensure_ascii=False,
194
+ sort_keys=True,
195
+ separators=(",", ":"),
196
+ )
197
+ return f"{serialized}\n".encode()
context_loader/cli.py ADDED
@@ -0,0 +1,75 @@
1
+ """Command-line interface for deterministic Codex project context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from collections.abc import Sequence
8
+
9
+ from . import __version__
10
+ from .application import load_project_context, render_json
11
+ from .git import ContextLoaderError
12
+
13
+
14
+ class SafeArgumentParser(argparse.ArgumentParser):
15
+ def error(self, message: str) -> None:
16
+ del message
17
+ raise ContextLoaderError("invalid command-line arguments", exit_code=2)
18
+
19
+
20
+ def _parser() -> argparse.ArgumentParser:
21
+ parser = SafeArgumentParser(
22
+ prog="codex-project-context",
23
+ description="Render deterministic local Git context as Markdown or JSON.",
24
+ allow_abbrev=False,
25
+ )
26
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
27
+ parser.add_argument("--repo", required=True, help="absolute Git worktree path")
28
+ parser.add_argument(
29
+ "--focus",
30
+ help="optional bounded task focus used for deterministic AGENTS section selection",
31
+ )
32
+ parser.add_argument(
33
+ "--path",
34
+ dest="target_path",
35
+ help="optional repository-relative target path used for AGENTS section selection",
36
+ )
37
+ parser.add_argument(
38
+ "--format",
39
+ choices=("markdown", "json"),
40
+ default="markdown",
41
+ help="output format (default: markdown)",
42
+ )
43
+ return parser
44
+
45
+
46
+ def main(argv: Sequence[str] | None = None) -> int:
47
+ try:
48
+ arguments = _parser().parse_args(argv)
49
+ result = load_project_context(
50
+ arguments.repo,
51
+ require_repository_root=arguments.format == "markdown",
52
+ focus=arguments.focus,
53
+ path=arguments.target_path,
54
+ )
55
+ output = (
56
+ result.context.encode("utf-8")
57
+ if arguments.format == "markdown"
58
+ else render_json(result)
59
+ )
60
+ except ContextLoaderError as exc:
61
+ print(f"error: {exc}", file=sys.stderr)
62
+ return exc.exit_code
63
+ except Exception:
64
+ print("error: context collection failed", file=sys.stderr)
65
+ return 1
66
+ try:
67
+ sys.stdout.buffer.write(output)
68
+ except Exception:
69
+ print("error: unable to write context output", file=sys.stderr)
70
+ return 1
71
+ return 0
72
+
73
+
74
+ if __name__ == "__main__":
75
+ raise SystemExit(main())