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.
- context_loader/__init__.py +3 -0
- context_loader/application.py +197 -0
- context_loader/cli.py +75 -0
- context_loader/collect.py +993 -0
- context_loader/git.py +348 -0
- context_loader/render.py +377 -0
- context_loader-0.1.8.dist-info/METADATA +555 -0
- context_loader-0.1.8.dist-info/RECORD +10 -0
- context_loader-0.1.8.dist-info/WHEEL +4 -0
- context_loader-0.1.8.dist-info/entry_points.txt +3 -0
|
@@ -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())
|