context-loader 1.0.0__tar.gz → 1.2.0__tar.gz
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-1.0.0 → context_loader-1.2.0}/PKG-INFO +52 -24
- {context_loader-1.0.0 → context_loader-1.2.0}/README.md +51 -23
- {context_loader-1.0.0 → context_loader-1.2.0}/context_loader/__init__.py +1 -1
- {context_loader-1.0.0 → context_loader-1.2.0}/context_loader/application.py +132 -7
- {context_loader-1.0.0 → context_loader-1.2.0}/context_loader/cli.py +11 -7
- {context_loader-1.0.0 → context_loader-1.2.0}/context_loader/render.py +25 -14
- {context_loader-1.0.0 → context_loader-1.2.0}/pyproject.toml +1 -1
- {context_loader-1.0.0 → context_loader-1.2.0}/pyproject.toml.orig +1 -1
- {context_loader-1.0.0 → context_loader-1.2.0}/tool_cli_contract.json +5 -3
- {context_loader-1.0.0 → context_loader-1.2.0}/LICENSE +0 -0
- {context_loader-1.0.0 → context_loader-1.2.0}/context_loader/collect.py +0 -0
- {context_loader-1.0.0 → context_loader-1.2.0}/context_loader/git.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: context-loader
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Render deterministic local Git context for any executor
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -219,15 +219,14 @@ Python standard library.
|
|
|
219
219
|
|
|
220
220
|
## Open-source quick start
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
rendering stable Markdown or JSON output.
|
|
222
|
+
Current stable release: **1.2.0**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
|
|
224
223
|
|
|
225
224
|
```bash
|
|
226
|
-
uv tool install context-loader
|
|
225
|
+
uv tool install 'context-loader==1.2.0'
|
|
227
226
|
```
|
|
228
227
|
|
|
229
228
|
```bash
|
|
230
|
-
pip install context-loader
|
|
229
|
+
pip install 'context-loader==1.2.0'
|
|
231
230
|
```
|
|
232
231
|
|
|
233
232
|
```bash
|
|
@@ -240,21 +239,21 @@ project-context --repo /path/to/repo --format json
|
|
|
240
239
|
|
|
241
240
|
## Install
|
|
242
241
|
|
|
243
|
-
Source version: `1.
|
|
242
|
+
Source version: `1.2.0`. Install its matching published release or an exact source commit.
|
|
244
243
|
|
|
245
244
|
Install via `uv`:
|
|
246
245
|
|
|
247
246
|
```bash
|
|
248
|
-
uv tool install context-loader
|
|
247
|
+
uv tool install 'context-loader==1.2.0'
|
|
249
248
|
```
|
|
250
249
|
|
|
251
250
|
Install via `pip`:
|
|
252
251
|
|
|
253
252
|
```bash
|
|
254
|
-
pip install context-loader
|
|
253
|
+
pip install 'context-loader==1.2.0'
|
|
255
254
|
```
|
|
256
255
|
|
|
257
|
-
For development or source-based installs tracking current repository (`1.
|
|
256
|
+
For development or source-based installs tracking current repository (`1.2.0`):
|
|
258
257
|
|
|
259
258
|
```bash
|
|
260
259
|
uv tool install git+https://github.com/xuanheng-tech/context-loader.git
|
|
@@ -305,16 +304,17 @@ project-context --version
|
|
|
305
304
|
project-context --repo /home/user/projects/example
|
|
306
305
|
project-context --repo /home/user/projects/example --format markdown
|
|
307
306
|
project-context --repo /home/user/projects/example --format json
|
|
307
|
+
project-context --repo /home/user/projects/example --format json-compact
|
|
308
308
|
project-context --repo /home/user/projects/example \
|
|
309
309
|
--focus "Authentication and session management" \
|
|
310
310
|
--path auth/session.py
|
|
311
311
|
```
|
|
312
312
|
|
|
313
313
|
`--format` defaults to `markdown`. In Markdown mode, `--repo` retains the 0.1.1 contract: it must be
|
|
314
|
-
the absolute, canonical root of a non-bare Git working tree. In
|
|
315
|
-
directory inside the working tree is accepted; symlinks are normalized and the
|
|
316
|
-
reported as `canonical_root`. Relative paths, non-Git directories, regular files,
|
|
317
|
-
repositories are rejected in
|
|
314
|
+
the absolute, canonical root of a non-bare Git working tree. In the `json` and `json-compact` modes,
|
|
315
|
+
an absolute existing directory inside the working tree is accepted; symlinks are normalized and the
|
|
316
|
+
discovered root is reported as `canonical_root`. Relative paths, non-Git directories, regular files,
|
|
317
|
+
and bare repositories are rejected in every mode.
|
|
318
318
|
|
|
319
319
|
`--focus` and `--path` are optional, bounded selection signals for the root `AGENTS.md`. `--path`
|
|
320
320
|
must be repository-relative. The collector does not retain either input in output or audit data.
|
|
@@ -337,15 +337,17 @@ The output has no generated timestamp, AI summary, architecture inference, or di
|
|
|
337
337
|
|
|
338
338
|
## JSON Output
|
|
339
339
|
|
|
340
|
-
`--format json` writes exactly one compact UTF-8 JSON document plus one trailing newline
|
|
341
|
-
Keys are serialized in sorted order with `ensure_ascii=False`.
|
|
340
|
+
`--format json` writes exactly one separator-compact UTF-8 JSON document plus one trailing newline
|
|
341
|
+
to stdout. Keys are serialized in sorted order with `ensure_ascii=False`. `--format json-compact`
|
|
342
|
+
uses the identical serialization; only the field set differs, as described under “Compact model
|
|
343
|
+
consumption”. The declared contract for `--format json` is:
|
|
342
344
|
|
|
343
345
|
```json
|
|
344
346
|
{
|
|
345
347
|
"schema_version": 1,
|
|
346
348
|
"tool": {
|
|
347
349
|
"name": "context-loader",
|
|
348
|
-
"version": "1.
|
|
350
|
+
"version": "1.2.0"
|
|
349
351
|
},
|
|
350
352
|
"repository": {
|
|
351
353
|
"requested_path": "/canonical/requested/path",
|
|
@@ -374,22 +376,45 @@ Keys are serialized in sorted order with `ensure_ascii=False`. The declared cont
|
|
|
374
376
|
],
|
|
375
377
|
"context": "the same assembled Markdown context",
|
|
376
378
|
"context_sha256": "sha256-hex",
|
|
379
|
+
"statuses": [],
|
|
377
380
|
"warnings": []
|
|
378
381
|
}
|
|
379
382
|
```
|
|
380
383
|
|
|
381
384
|
`context_sha256` hashes the UTF-8 bytes of `context`; each `content_sha256` does the same for that
|
|
382
|
-
source's `content
|
|
383
|
-
|
|
384
|
-
`
|
|
385
|
-
|
|
385
|
+
source's `content` body as rendered (after newline normalization, any `AGENTS.md` section selection
|
|
386
|
+
and any truncation marker) — never for the raw bytes of the file at `path`, which match only for
|
|
387
|
+
unselected, untruncated LF files. `sources` contains only file bodies that actually enter the final
|
|
388
|
+
context, in assembly order, after the existing newline normalization and truncation rules. `scope`
|
|
389
|
+
distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
|
|
390
|
+
only `repository` sources and does not add any global-file discovery.
|
|
391
|
+
|
|
392
|
+
`statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown: skipped or absent sources, truncated sources, unreadable directory-tree entries, sections omitted under the global output budget, and truncated working-tree or declared-command listings. Each entry has stable `code`, `subject_kind`, and `subject` fields. `sources`, `context`, and `schema_version` remain unchanged; callers can ignore `statuses` safely.
|
|
386
393
|
|
|
387
394
|
The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
|
|
388
395
|
contain heading, heading level, and fixed selection reasons; it never contains the original focus or
|
|
389
396
|
target path. Existing source fields and schema version 1 remain unchanged.
|
|
390
397
|
|
|
391
|
-
|
|
392
|
-
|
|
398
|
+
### Compact model consumption (`--format json-compact`)
|
|
399
|
+
|
|
400
|
+
`--format json-compact` emits a `schema_version` 2 document: exactly the version-1 document with
|
|
401
|
+
`sources[*].content` omitted. Every other field — `context`, `context_sha256`, `statuses`,
|
|
402
|
+
`warnings`, `tool`, `repository`, and each source's `ordinal`, `kind`, `scope`, `path`,
|
|
403
|
+
`content_sha256` and optional `selection` — is identical to `--format json` for the same arguments.
|
|
404
|
+
The projection exists because the selected source bodies already occur verbatim inside `context`,
|
|
405
|
+
so carrying them again in `sources` duplicates a large share of the document bytes (measured up to
|
|
406
|
+
~44% on real worktrees, scaling with how much of `context` those bodies occupy) for a model
|
|
407
|
+
consumer without adding information. Nothing is lost: each omitted body remains inside `context`,
|
|
408
|
+
`content_sha256` still fingerprints that rendered body, and `path` plus `canonical_root` locate the
|
|
409
|
+
underlying file for a bounded manual re-read (whose raw bytes may differ from the rendered body as
|
|
410
|
+
defined above). `schema_version` is an exact document selector, not an upgrade marker: version 2 is
|
|
411
|
+
a field-set projection of version 1, so consumers must branch on the value and must not apply
|
|
412
|
+
`>=`-superset reasoning. Default Markdown and `--format json` output bytes, exit codes and
|
|
413
|
+
determinism are unchanged.
|
|
414
|
+
|
|
415
|
+
The JSON schema version and package version are independent: `--format json` currently emits
|
|
416
|
+
`schema_version` `1` and `--format json-compact` emits `2`, while `tool.version` is `1.2.0`.
|
|
417
|
+
Callers must depend only on fields declared above.
|
|
393
418
|
The document contains no generated time or random identifier, so unchanged input produces identical
|
|
394
419
|
JSON bytes. On failure, stdout remains empty and stderr contains only a short diagnostic.
|
|
395
420
|
|
|
@@ -497,8 +522,11 @@ just check
|
|
|
497
522
|
## Version maintenance
|
|
498
523
|
|
|
499
524
|
The versions in `pyproject.toml` and `context_loader/__init__.py`, the matching `CHANGELOG.md`
|
|
500
|
-
section,
|
|
501
|
-
|
|
525
|
+
section, required tests, and the README current-stable declaration plus `context-loader==X.Y.Z`
|
|
526
|
+
install pins must change in the same release-preparation batch. `CHANGELOG.md` is the
|
|
527
|
+
authoritative version-change record. `scripts/release.py` rejects README stable/install
|
|
528
|
+
pins that disagree with the intended package version; historical changelog entries are
|
|
529
|
+
not part of that check. Merging to `master` is not a release; formal publication
|
|
502
530
|
still requires a separately created and pushed tag.
|
|
503
531
|
|
|
504
532
|
## Release closure and retries
|
|
@@ -8,15 +8,14 @@ Python standard library.
|
|
|
8
8
|
|
|
9
9
|
## Open-source quick start
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
rendering stable Markdown or JSON output.
|
|
11
|
+
Current stable release: **1.2.0**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
|
|
13
12
|
|
|
14
13
|
```bash
|
|
15
|
-
uv tool install context-loader
|
|
14
|
+
uv tool install 'context-loader==1.2.0'
|
|
16
15
|
```
|
|
17
16
|
|
|
18
17
|
```bash
|
|
19
|
-
pip install context-loader
|
|
18
|
+
pip install 'context-loader==1.2.0'
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
```bash
|
|
@@ -29,21 +28,21 @@ project-context --repo /path/to/repo --format json
|
|
|
29
28
|
|
|
30
29
|
## Install
|
|
31
30
|
|
|
32
|
-
Source version: `1.
|
|
31
|
+
Source version: `1.2.0`. Install its matching published release or an exact source commit.
|
|
33
32
|
|
|
34
33
|
Install via `uv`:
|
|
35
34
|
|
|
36
35
|
```bash
|
|
37
|
-
uv tool install context-loader
|
|
36
|
+
uv tool install 'context-loader==1.2.0'
|
|
38
37
|
```
|
|
39
38
|
|
|
40
39
|
Install via `pip`:
|
|
41
40
|
|
|
42
41
|
```bash
|
|
43
|
-
pip install context-loader
|
|
42
|
+
pip install 'context-loader==1.2.0'
|
|
44
43
|
```
|
|
45
44
|
|
|
46
|
-
For development or source-based installs tracking current repository (`1.
|
|
45
|
+
For development or source-based installs tracking current repository (`1.2.0`):
|
|
47
46
|
|
|
48
47
|
```bash
|
|
49
48
|
uv tool install git+https://github.com/xuanheng-tech/context-loader.git
|
|
@@ -94,16 +93,17 @@ project-context --version
|
|
|
94
93
|
project-context --repo /home/user/projects/example
|
|
95
94
|
project-context --repo /home/user/projects/example --format markdown
|
|
96
95
|
project-context --repo /home/user/projects/example --format json
|
|
96
|
+
project-context --repo /home/user/projects/example --format json-compact
|
|
97
97
|
project-context --repo /home/user/projects/example \
|
|
98
98
|
--focus "Authentication and session management" \
|
|
99
99
|
--path auth/session.py
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
`--format` defaults to `markdown`. In Markdown mode, `--repo` retains the 0.1.1 contract: it must be
|
|
103
|
-
the absolute, canonical root of a non-bare Git working tree. In
|
|
104
|
-
directory inside the working tree is accepted; symlinks are normalized and the
|
|
105
|
-
reported as `canonical_root`. Relative paths, non-Git directories, regular files,
|
|
106
|
-
repositories are rejected in
|
|
103
|
+
the absolute, canonical root of a non-bare Git working tree. In the `json` and `json-compact` modes,
|
|
104
|
+
an absolute existing directory inside the working tree is accepted; symlinks are normalized and the
|
|
105
|
+
discovered root is reported as `canonical_root`. Relative paths, non-Git directories, regular files,
|
|
106
|
+
and bare repositories are rejected in every mode.
|
|
107
107
|
|
|
108
108
|
`--focus` and `--path` are optional, bounded selection signals for the root `AGENTS.md`. `--path`
|
|
109
109
|
must be repository-relative. The collector does not retain either input in output or audit data.
|
|
@@ -126,15 +126,17 @@ The output has no generated timestamp, AI summary, architecture inference, or di
|
|
|
126
126
|
|
|
127
127
|
## JSON Output
|
|
128
128
|
|
|
129
|
-
`--format json` writes exactly one compact UTF-8 JSON document plus one trailing newline
|
|
130
|
-
Keys are serialized in sorted order with `ensure_ascii=False`.
|
|
129
|
+
`--format json` writes exactly one separator-compact UTF-8 JSON document plus one trailing newline
|
|
130
|
+
to stdout. Keys are serialized in sorted order with `ensure_ascii=False`. `--format json-compact`
|
|
131
|
+
uses the identical serialization; only the field set differs, as described under “Compact model
|
|
132
|
+
consumption”. The declared contract for `--format json` is:
|
|
131
133
|
|
|
132
134
|
```json
|
|
133
135
|
{
|
|
134
136
|
"schema_version": 1,
|
|
135
137
|
"tool": {
|
|
136
138
|
"name": "context-loader",
|
|
137
|
-
"version": "1.
|
|
139
|
+
"version": "1.2.0"
|
|
138
140
|
},
|
|
139
141
|
"repository": {
|
|
140
142
|
"requested_path": "/canonical/requested/path",
|
|
@@ -163,22 +165,45 @@ Keys are serialized in sorted order with `ensure_ascii=False`. The declared cont
|
|
|
163
165
|
],
|
|
164
166
|
"context": "the same assembled Markdown context",
|
|
165
167
|
"context_sha256": "sha256-hex",
|
|
168
|
+
"statuses": [],
|
|
166
169
|
"warnings": []
|
|
167
170
|
}
|
|
168
171
|
```
|
|
169
172
|
|
|
170
173
|
`context_sha256` hashes the UTF-8 bytes of `context`; each `content_sha256` does the same for that
|
|
171
|
-
source's `content
|
|
172
|
-
|
|
173
|
-
`
|
|
174
|
-
|
|
174
|
+
source's `content` body as rendered (after newline normalization, any `AGENTS.md` section selection
|
|
175
|
+
and any truncation marker) — never for the raw bytes of the file at `path`, which match only for
|
|
176
|
+
unselected, untruncated LF files. `sources` contains only file bodies that actually enter the final
|
|
177
|
+
context, in assembly order, after the existing newline normalization and truncation rules. `scope`
|
|
178
|
+
distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
|
|
179
|
+
only `repository` sources and does not add any global-file discovery.
|
|
180
|
+
|
|
181
|
+
`statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown: skipped or absent sources, truncated sources, unreadable directory-tree entries, sections omitted under the global output budget, and truncated working-tree or declared-command listings. Each entry has stable `code`, `subject_kind`, and `subject` fields. `sources`, `context`, and `schema_version` remain unchanged; callers can ignore `statuses` safely.
|
|
175
182
|
|
|
176
183
|
The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
|
|
177
184
|
contain heading, heading level, and fixed selection reasons; it never contains the original focus or
|
|
178
185
|
target path. Existing source fields and schema version 1 remain unchanged.
|
|
179
186
|
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
### Compact model consumption (`--format json-compact`)
|
|
188
|
+
|
|
189
|
+
`--format json-compact` emits a `schema_version` 2 document: exactly the version-1 document with
|
|
190
|
+
`sources[*].content` omitted. Every other field — `context`, `context_sha256`, `statuses`,
|
|
191
|
+
`warnings`, `tool`, `repository`, and each source's `ordinal`, `kind`, `scope`, `path`,
|
|
192
|
+
`content_sha256` and optional `selection` — is identical to `--format json` for the same arguments.
|
|
193
|
+
The projection exists because the selected source bodies already occur verbatim inside `context`,
|
|
194
|
+
so carrying them again in `sources` duplicates a large share of the document bytes (measured up to
|
|
195
|
+
~44% on real worktrees, scaling with how much of `context` those bodies occupy) for a model
|
|
196
|
+
consumer without adding information. Nothing is lost: each omitted body remains inside `context`,
|
|
197
|
+
`content_sha256` still fingerprints that rendered body, and `path` plus `canonical_root` locate the
|
|
198
|
+
underlying file for a bounded manual re-read (whose raw bytes may differ from the rendered body as
|
|
199
|
+
defined above). `schema_version` is an exact document selector, not an upgrade marker: version 2 is
|
|
200
|
+
a field-set projection of version 1, so consumers must branch on the value and must not apply
|
|
201
|
+
`>=`-superset reasoning. Default Markdown and `--format json` output bytes, exit codes and
|
|
202
|
+
determinism are unchanged.
|
|
203
|
+
|
|
204
|
+
The JSON schema version and package version are independent: `--format json` currently emits
|
|
205
|
+
`schema_version` `1` and `--format json-compact` emits `2`, while `tool.version` is `1.2.0`.
|
|
206
|
+
Callers must depend only on fields declared above.
|
|
182
207
|
The document contains no generated time or random identifier, so unchanged input produces identical
|
|
183
208
|
JSON bytes. On failure, stdout remains empty and stderr contains only a short diagnostic.
|
|
184
209
|
|
|
@@ -286,8 +311,11 @@ just check
|
|
|
286
311
|
## Version maintenance
|
|
287
312
|
|
|
288
313
|
The versions in `pyproject.toml` and `context_loader/__init__.py`, the matching `CHANGELOG.md`
|
|
289
|
-
section,
|
|
290
|
-
|
|
314
|
+
section, required tests, and the README current-stable declaration plus `context-loader==X.Y.Z`
|
|
315
|
+
install pins must change in the same release-preparation batch. `CHANGELOG.md` is the
|
|
316
|
+
authoritative version-change record. `scripts/release.py` rejects README stable/install
|
|
317
|
+
pins that disagree with the intended package version; historical changelog entries are
|
|
318
|
+
not part of that check. Merging to `master` is not a release; formal publication
|
|
291
319
|
still requires a separately created and pushed tag.
|
|
292
320
|
|
|
293
321
|
## Release closure and retries
|
|
@@ -10,9 +10,16 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
from . import __version__
|
|
12
12
|
from .collect import (
|
|
13
|
+
NOT_PRESENT,
|
|
14
|
+
SKIPPED_ENCODING,
|
|
15
|
+
SKIPPED_NOT_REGULAR,
|
|
16
|
+
SKIPPED_SYMLINK,
|
|
17
|
+
SKIPPED_UNREADABLE,
|
|
18
|
+
TRUNCATION_MARKER,
|
|
13
19
|
AgentsSectionAuditEntry,
|
|
14
20
|
AgentsSelectionAudit,
|
|
15
21
|
AgentsSelectionInputError,
|
|
22
|
+
CollectedFile,
|
|
16
23
|
ProjectContext,
|
|
17
24
|
collect_project_context,
|
|
18
25
|
)
|
|
@@ -20,8 +27,17 @@ from .git import ContextLoaderError, collect_repository
|
|
|
20
27
|
from .render import render_markdown_with_details, rendered_source_contents
|
|
21
28
|
|
|
22
29
|
JSON_SCHEMA_VERSION = 1
|
|
30
|
+
COMPACT_JSON_SCHEMA_VERSION = 2
|
|
23
31
|
TOOL_NAME = "context-loader"
|
|
24
32
|
|
|
33
|
+
_STATUS_CODE_BY_MESSAGE = {
|
|
34
|
+
NOT_PRESENT: "not_present",
|
|
35
|
+
SKIPPED_SYMLINK: "skipped_symlink",
|
|
36
|
+
SKIPPED_NOT_REGULAR: "skipped_not_regular",
|
|
37
|
+
SKIPPED_ENCODING: "skipped_encoding",
|
|
38
|
+
SKIPPED_UNREADABLE: "skipped_unreadable",
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
|
|
26
42
|
@dataclass(frozen=True, slots=True)
|
|
27
43
|
class ToolIdentity:
|
|
@@ -46,6 +62,15 @@ class ProjectContextSource:
|
|
|
46
62
|
selection: AgentsSelectionAudit | None = None
|
|
47
63
|
|
|
48
64
|
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class ContextStatus:
|
|
67
|
+
"""One machine-readable skipped, omitted, truncated, or unreadable condition."""
|
|
68
|
+
|
|
69
|
+
code: str
|
|
70
|
+
subject_kind: str
|
|
71
|
+
subject: str
|
|
72
|
+
|
|
73
|
+
|
|
49
74
|
@dataclass(frozen=True, slots=True)
|
|
50
75
|
class ProjectContextResult:
|
|
51
76
|
schema_version: int
|
|
@@ -55,6 +80,7 @@ class ProjectContextResult:
|
|
|
55
80
|
context: str
|
|
56
81
|
context_sha256: str
|
|
57
82
|
warnings: tuple[str, ...]
|
|
83
|
+
statuses: tuple[ContextStatus, ...]
|
|
58
84
|
|
|
59
85
|
|
|
60
86
|
def _text_sha256(content: str) -> str:
|
|
@@ -102,6 +128,79 @@ def _sources(
|
|
|
102
128
|
return tuple(sources)
|
|
103
129
|
|
|
104
130
|
|
|
131
|
+
def _status(code: str, subject_kind: str, subject: str) -> ContextStatus:
|
|
132
|
+
return ContextStatus(code=code, subject_kind=subject_kind, subject=subject)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _statuses_for_source(source: CollectedFile) -> list[ContextStatus]:
|
|
136
|
+
if source.status is not None:
|
|
137
|
+
code = _STATUS_CODE_BY_MESSAGE.get(source.status, "skipped_unreadable")
|
|
138
|
+
return [_status(code, "source", source.name)]
|
|
139
|
+
statuses: list[ContextStatus] = []
|
|
140
|
+
if source.truncated or (source.selection is not None and source.selection.truncated):
|
|
141
|
+
statuses.append(_status("truncated", "source", source.name))
|
|
142
|
+
if source.selection is not None:
|
|
143
|
+
if source.selection.parse_fallback:
|
|
144
|
+
statuses.append(_status("parse_fallback", "source", source.name))
|
|
145
|
+
if source.selection.source_scan_truncated:
|
|
146
|
+
statuses.append(_status("source_scan_truncated", "source", source.name))
|
|
147
|
+
if source.selection.index_truncated:
|
|
148
|
+
statuses.append(_status("index_truncated", "source", source.name))
|
|
149
|
+
return statuses
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _build_statuses(
|
|
153
|
+
project: ProjectContext,
|
|
154
|
+
*,
|
|
155
|
+
omitted_sections: tuple[str, ...],
|
|
156
|
+
changes_truncated: bool,
|
|
157
|
+
commands_truncated: bool,
|
|
158
|
+
rendered_sources: tuple[ProjectContextSource, ...],
|
|
159
|
+
) -> tuple[ContextStatus, ...]:
|
|
160
|
+
statuses: list[ContextStatus] = []
|
|
161
|
+
for source in (project.instructions, project.overview, *project.entry_files):
|
|
162
|
+
statuses.extend(_statuses_for_source(source))
|
|
163
|
+
|
|
164
|
+
truncated_sources = {
|
|
165
|
+
status.subject
|
|
166
|
+
for status in statuses
|
|
167
|
+
if status.subject_kind == "source" and status.code == "truncated"
|
|
168
|
+
}
|
|
169
|
+
for source in rendered_sources:
|
|
170
|
+
if TRUNCATION_MARKER not in source.content:
|
|
171
|
+
continue
|
|
172
|
+
name = source.path.name
|
|
173
|
+
if name in truncated_sources:
|
|
174
|
+
continue
|
|
175
|
+
statuses.append(_status("truncated", "source", name))
|
|
176
|
+
truncated_sources.add(name)
|
|
177
|
+
|
|
178
|
+
if commands_truncated:
|
|
179
|
+
statuses.append(_status("truncated", "commands", "Declared Commands"))
|
|
180
|
+
if changes_truncated:
|
|
181
|
+
statuses.append(_status("truncated", "changes", "Working Tree Changes"))
|
|
182
|
+
|
|
183
|
+
if project.directory_tree.truncated:
|
|
184
|
+
statuses.append(_status("truncated", "tree", "Directory Tree"))
|
|
185
|
+
for entry in project.directory_tree.entries:
|
|
186
|
+
if entry.kind == "unreadable_directory":
|
|
187
|
+
subject = entry.path if entry.path else "."
|
|
188
|
+
statuses.append(_status("unreadable", "tree_entry", subject))
|
|
189
|
+
|
|
190
|
+
for title in omitted_sections:
|
|
191
|
+
statuses.append(_status("section_omitted", "section", title))
|
|
192
|
+
|
|
193
|
+
deduped: list[ContextStatus] = []
|
|
194
|
+
seen: set[tuple[str, str, str]] = set()
|
|
195
|
+
for status in statuses:
|
|
196
|
+
key = (status.code, status.subject_kind, status.subject)
|
|
197
|
+
if key in seen:
|
|
198
|
+
continue
|
|
199
|
+
seen.add(key)
|
|
200
|
+
deduped.append(status)
|
|
201
|
+
return tuple(deduped)
|
|
202
|
+
|
|
203
|
+
|
|
105
204
|
def load_project_context(
|
|
106
205
|
repo: str | os.PathLike[str],
|
|
107
206
|
*,
|
|
@@ -120,6 +219,14 @@ def load_project_context(
|
|
|
120
219
|
raise ContextLoaderError(str(exc), exit_code=2) from None
|
|
121
220
|
rendered = render_markdown_with_details(state, project)
|
|
122
221
|
context = rendered.output.decode("utf-8")
|
|
222
|
+
sources = _sources(state.repository, project, rendered.included_sections)
|
|
223
|
+
statuses = _build_statuses(
|
|
224
|
+
project,
|
|
225
|
+
omitted_sections=rendered.omitted_sections,
|
|
226
|
+
changes_truncated=rendered.changes_truncated,
|
|
227
|
+
commands_truncated=rendered.commands_truncated,
|
|
228
|
+
rendered_sources=sources,
|
|
229
|
+
)
|
|
123
230
|
return ProjectContextResult(
|
|
124
231
|
schema_version=JSON_SCHEMA_VERSION,
|
|
125
232
|
tool=ToolIdentity(name=TOOL_NAME, version=__version__),
|
|
@@ -127,10 +234,11 @@ def load_project_context(
|
|
|
127
234
|
requested_path=location.requested_path,
|
|
128
235
|
canonical_root=location.canonical_root,
|
|
129
236
|
),
|
|
130
|
-
sources=
|
|
237
|
+
sources=sources,
|
|
131
238
|
context=context,
|
|
132
239
|
context_sha256=hashlib.sha256(rendered.output).hexdigest(),
|
|
133
240
|
warnings=(),
|
|
241
|
+
statuses=statuses,
|
|
134
242
|
)
|
|
135
243
|
|
|
136
244
|
|
|
@@ -157,24 +265,38 @@ def _selection_document(selection: AgentsSelectionAudit) -> dict[str, object]:
|
|
|
157
265
|
}
|
|
158
266
|
|
|
159
267
|
|
|
160
|
-
def
|
|
268
|
+
def _status_document(status: ContextStatus) -> dict[str, object]:
|
|
269
|
+
return {
|
|
270
|
+
"code": status.code,
|
|
271
|
+
"subject": status.subject,
|
|
272
|
+
"subject_kind": status.subject_kind,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _source_document(source: ProjectContextSource, *, include_content: bool) -> dict[str, object]:
|
|
161
277
|
document: dict[str, object] = {
|
|
162
278
|
"ordinal": source.ordinal,
|
|
163
279
|
"kind": source.kind,
|
|
164
280
|
"scope": source.scope,
|
|
165
281
|
"path": os.fspath(source.path),
|
|
166
282
|
"content_sha256": source.content_sha256,
|
|
167
|
-
"content": source.content,
|
|
168
283
|
}
|
|
284
|
+
if include_content:
|
|
285
|
+
document["content"] = source.content
|
|
169
286
|
if source.selection is not None:
|
|
170
287
|
document["selection"] = _selection_document(source.selection)
|
|
171
288
|
return document
|
|
172
289
|
|
|
173
290
|
|
|
174
|
-
def render_json(result: ProjectContextResult) -> bytes:
|
|
175
|
-
"""Serialize one result as stable UTF-8 JSON followed by exactly one newline.
|
|
291
|
+
def render_json(result: ProjectContextResult, *, compact: bool = False) -> bytes:
|
|
292
|
+
"""Serialize one result as stable UTF-8 JSON followed by exactly one newline.
|
|
293
|
+
|
|
294
|
+
``compact`` projects away the duplicated source bodies only at this
|
|
295
|
+
boundary: statuses and context were computed from the full model at load
|
|
296
|
+
time, so both documents agree on everything except ``sources[*].content``.
|
|
297
|
+
"""
|
|
176
298
|
document = {
|
|
177
|
-
"schema_version": result.schema_version,
|
|
299
|
+
"schema_version": (COMPACT_JSON_SCHEMA_VERSION if compact else result.schema_version),
|
|
178
300
|
"tool": {
|
|
179
301
|
"name": result.tool.name,
|
|
180
302
|
"version": result.tool.version,
|
|
@@ -183,7 +305,10 @@ def render_json(result: ProjectContextResult) -> bytes:
|
|
|
183
305
|
"requested_path": os.fspath(result.repository.requested_path),
|
|
184
306
|
"canonical_root": os.fspath(result.repository.canonical_root),
|
|
185
307
|
},
|
|
186
|
-
"sources": [
|
|
308
|
+
"sources": [
|
|
309
|
+
_source_document(source, include_content=not compact) for source in result.sources
|
|
310
|
+
],
|
|
311
|
+
"statuses": [_status_document(status) for status in result.statuses],
|
|
187
312
|
"context": result.context,
|
|
188
313
|
"context_sha256": result.context_sha256,
|
|
189
314
|
"warnings": list(result.warnings),
|
|
@@ -36,9 +36,12 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
36
36
|
)
|
|
37
37
|
parser.add_argument(
|
|
38
38
|
"--format",
|
|
39
|
-
choices=("markdown", "json"),
|
|
39
|
+
choices=("markdown", "json", "json-compact"),
|
|
40
40
|
default="markdown",
|
|
41
|
-
help=
|
|
41
|
+
help=(
|
|
42
|
+
"output format (default: markdown); json-compact emits schema "
|
|
43
|
+
"version 2 without duplicated source bodies"
|
|
44
|
+
),
|
|
42
45
|
)
|
|
43
46
|
return parser
|
|
44
47
|
|
|
@@ -52,11 +55,12 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
52
55
|
focus=arguments.focus,
|
|
53
56
|
path=arguments.target_path,
|
|
54
57
|
)
|
|
55
|
-
|
|
56
|
-
result.context.encode("utf-8")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
if arguments.format == "markdown":
|
|
59
|
+
output = result.context.encode("utf-8")
|
|
60
|
+
elif arguments.format == "json":
|
|
61
|
+
output = render_json(result)
|
|
62
|
+
else:
|
|
63
|
+
output = render_json(result, compact=True)
|
|
60
64
|
except ContextLoaderError as exc:
|
|
61
65
|
print(f"error: {exc}", file=sys.stderr)
|
|
62
66
|
return exc.exit_code
|
|
@@ -34,6 +34,9 @@ GLOBAL_OMISSION = "Omitted: global output limit reached."
|
|
|
34
34
|
class MarkdownRender:
|
|
35
35
|
output: bytes
|
|
36
36
|
included_sections: tuple[str, ...]
|
|
37
|
+
omitted_sections: tuple[str, ...]
|
|
38
|
+
changes_truncated: bool
|
|
39
|
+
commands_truncated: bool
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
def _display(value: str) -> str:
|
|
@@ -173,10 +176,12 @@ def _command_line(command: DeclaredCommand) -> str:
|
|
|
173
176
|
return f"- `{invocation}` → `{_display(command.target)}`"
|
|
174
177
|
|
|
175
178
|
|
|
176
|
-
def _bounded_lines(
|
|
179
|
+
def _bounded_lines(
|
|
180
|
+
lines: list[str], limit: int, *, already_truncated: bool = False
|
|
181
|
+
) -> tuple[str, bool]:
|
|
177
182
|
complete = "\n".join(lines)
|
|
178
183
|
if not already_truncated and len(complete.encode()) <= limit:
|
|
179
|
-
return complete
|
|
184
|
+
return complete, False
|
|
180
185
|
marker_size = len(TRUNCATION_MARKER.encode())
|
|
181
186
|
budget = max(0, limit - marker_size - 1)
|
|
182
187
|
selected: list[str] = []
|
|
@@ -188,20 +193,15 @@ def _bounded_lines(lines: list[str], limit: int, *, already_truncated: bool = Fa
|
|
|
188
193
|
selected.append(line)
|
|
189
194
|
used += size
|
|
190
195
|
selected.append(TRUNCATION_MARKER)
|
|
191
|
-
return "\n".join(selected)
|
|
196
|
+
return "\n".join(selected), True
|
|
192
197
|
|
|
193
198
|
|
|
194
|
-
def _render_commands(commands: tuple[DeclaredCommand, ...]) -> str:
|
|
199
|
+
def _render_commands(commands: tuple[DeclaredCommand, ...]) -> tuple[str, bool]:
|
|
195
200
|
lines = [_command_line(command) for command in commands]
|
|
196
201
|
if not lines:
|
|
197
202
|
lines.append("No supported command declarations found.")
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"## Declared Commands",
|
|
201
|
-
"",
|
|
202
|
-
_bounded_lines(lines, DECLARED_COMMANDS_LIMIT_BYTES),
|
|
203
|
-
)
|
|
204
|
-
)
|
|
203
|
+
body, truncated = _bounded_lines(lines, DECLARED_COMMANDS_LIMIT_BYTES)
|
|
204
|
+
return "\n".join(("## Declared Commands", "", body)), truncated
|
|
205
205
|
|
|
206
206
|
|
|
207
207
|
def _entry_body(source: CollectedFile, remaining: int) -> tuple[str | None, int]:
|
|
@@ -276,7 +276,7 @@ def _render_directory_tree(tree: DirectoryTree) -> str:
|
|
|
276
276
|
lines = [_tree_line(entry) for entry in tree.entries]
|
|
277
277
|
if not lines:
|
|
278
278
|
lines.append("No entries.")
|
|
279
|
-
body = _bounded_lines(
|
|
279
|
+
body, _truncated = _bounded_lines(
|
|
280
280
|
lines,
|
|
281
281
|
DIRECTORY_TREE_LIMIT_BYTES,
|
|
282
282
|
already_truncated=tree.truncated,
|
|
@@ -298,6 +298,8 @@ def render_markdown_with_details(state: RepositoryState, project: ProjectContext
|
|
|
298
298
|
f"- Repository: `{_display(os.fspath(state.repository))}`",
|
|
299
299
|
)
|
|
300
300
|
)
|
|
301
|
+
commands_section, commands_truncated = _render_commands(project.commands)
|
|
302
|
+
changes_truncated = _change_lines(state)[1]
|
|
301
303
|
sections: list[tuple[str, str | None]] = [
|
|
302
304
|
("Git State", _render_git_state(state)),
|
|
303
305
|
(
|
|
@@ -312,7 +314,7 @@ def render_markdown_with_details(state: RepositoryState, project: ProjectContext
|
|
|
312
314
|
"Project Overview",
|
|
313
315
|
_render_source_section("Project Overview", project.overview, README_LIMIT_BYTES),
|
|
314
316
|
),
|
|
315
|
-
("Declared Commands",
|
|
317
|
+
("Declared Commands", commands_section),
|
|
316
318
|
("Project Entry Files", _render_entry_files(project.entry_files)),
|
|
317
319
|
("Recent Commits", _render_recent_commits(state.commits)),
|
|
318
320
|
("Directory Tree", _render_directory_tree(project.directory_tree)),
|
|
@@ -320,6 +322,7 @@ def render_markdown_with_details(state: RepositoryState, project: ProjectContext
|
|
|
320
322
|
|
|
321
323
|
rendered_parts = [header.encode()]
|
|
322
324
|
included_sections: list[str] = []
|
|
325
|
+
omitted_sections: list[str] = []
|
|
323
326
|
omission_started = False
|
|
324
327
|
for index, (title, full_section) in enumerate(sections):
|
|
325
328
|
omission = _omitted_section(title).encode()
|
|
@@ -330,6 +333,7 @@ def render_markdown_with_details(state: RepositoryState, project: ProjectContext
|
|
|
330
333
|
if omission_started or full_section is None:
|
|
331
334
|
selected = omission
|
|
332
335
|
omission_started = True
|
|
336
|
+
omitted_sections.append(title)
|
|
333
337
|
else:
|
|
334
338
|
candidate = full_section.encode()
|
|
335
339
|
tentative = b"\n\n".join((*rendered_parts, candidate, *future_omissions)) + b"\n"
|
|
@@ -339,12 +343,19 @@ def render_markdown_with_details(state: RepositoryState, project: ProjectContext
|
|
|
339
343
|
else:
|
|
340
344
|
selected = omission
|
|
341
345
|
omission_started = True
|
|
346
|
+
omitted_sections.append(title)
|
|
342
347
|
rendered_parts.append(selected)
|
|
343
348
|
|
|
344
349
|
output = b"\n\n".join(rendered_parts) + b"\n"
|
|
345
350
|
if len(output) > GLOBAL_OUTPUT_LIMIT_BYTES:
|
|
346
351
|
raise RuntimeError("global context output limit could not be satisfied")
|
|
347
|
-
return MarkdownRender(
|
|
352
|
+
return MarkdownRender(
|
|
353
|
+
output=output,
|
|
354
|
+
included_sections=tuple(included_sections),
|
|
355
|
+
omitted_sections=tuple(omitted_sections),
|
|
356
|
+
changes_truncated=changes_truncated,
|
|
357
|
+
commands_truncated=commands_truncated,
|
|
358
|
+
)
|
|
348
359
|
|
|
349
360
|
|
|
350
361
|
def rendered_source_contents(
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"repo_is_absolute_git_worktree",
|
|
16
16
|
"markdown_requires_canonical_root",
|
|
17
17
|
"json_accepts_path_inside_worktree",
|
|
18
|
+
"json_compact_accepts_path_inside_worktree",
|
|
18
19
|
"target_path_is_repository_relative"
|
|
19
20
|
],
|
|
20
21
|
"result_statuses": [
|
|
@@ -24,11 +25,12 @@
|
|
|
24
25
|
]
|
|
25
26
|
}
|
|
26
27
|
],
|
|
27
|
-
"contract_version":
|
|
28
|
+
"contract_version": 3,
|
|
28
29
|
"flags": {
|
|
29
30
|
"format": [
|
|
30
31
|
"markdown",
|
|
31
|
-
"json"
|
|
32
|
+
"json",
|
|
33
|
+
"json-compact"
|
|
32
34
|
],
|
|
33
35
|
"output": "stdout",
|
|
34
36
|
"stderr": "bounded_diagnostic_only"
|
|
@@ -44,5 +46,5 @@
|
|
|
44
46
|
],
|
|
45
47
|
"schema_version": 1,
|
|
46
48
|
"tool_name": "context-loader",
|
|
47
|
-
"tool_version": "1.
|
|
49
|
+
"tool_version": "1.2.0"
|
|
48
50
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|