context-loader 1.3.0__tar.gz → 1.3.1__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.3.0 → context_loader-1.3.1}/PKG-INFO +48 -22
- {context_loader-1.3.0 → context_loader-1.3.1}/README.md +47 -21
- {context_loader-1.3.0 → context_loader-1.3.1}/context_loader/__init__.py +1 -1
- {context_loader-1.3.0 → context_loader-1.3.1}/context_loader/application.py +16 -4
- {context_loader-1.3.0 → context_loader-1.3.1}/context_loader/collect.py +37 -2
- {context_loader-1.3.0 → context_loader-1.3.1}/context_loader/render.py +11 -29
- {context_loader-1.3.0 → context_loader-1.3.1}/pyproject.toml +1 -1
- {context_loader-1.3.0 → context_loader-1.3.1}/pyproject.toml.orig +1 -1
- {context_loader-1.3.0 → context_loader-1.3.1}/tool_cli_contract.json +1 -1
- {context_loader-1.3.0 → context_loader-1.3.1}/LICENSE +0 -0
- {context_loader-1.3.0 → context_loader-1.3.1}/context_loader/cli.py +0 -0
- {context_loader-1.3.0 → context_loader-1.3.1}/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.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Render deterministic local Git context for any executor
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -219,14 +219,14 @@ Python standard library.
|
|
|
219
219
|
|
|
220
220
|
## Open-source quick start
|
|
221
221
|
|
|
222
|
-
Current stable release: **1.3.
|
|
222
|
+
Current stable release: **1.3.1**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
|
|
223
223
|
|
|
224
224
|
```bash
|
|
225
|
-
uv tool install 'context-loader==1.3.
|
|
225
|
+
uv tool install 'context-loader==1.3.1'
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
```bash
|
|
229
|
-
pip install 'context-loader==1.3.
|
|
229
|
+
pip install 'context-loader==1.3.1'
|
|
230
230
|
```
|
|
231
231
|
|
|
232
232
|
```bash
|
|
@@ -239,21 +239,21 @@ project-context --repo /path/to/repo --format json
|
|
|
239
239
|
|
|
240
240
|
## Install
|
|
241
241
|
|
|
242
|
-
Source version: `1.3.
|
|
242
|
+
Source version: `1.3.1`. Install its matching published release or an exact source commit.
|
|
243
243
|
|
|
244
244
|
Install via `uv`:
|
|
245
245
|
|
|
246
246
|
```bash
|
|
247
|
-
uv tool install 'context-loader==1.3.
|
|
247
|
+
uv tool install 'context-loader==1.3.1'
|
|
248
248
|
```
|
|
249
249
|
|
|
250
250
|
Install via `pip`:
|
|
251
251
|
|
|
252
252
|
```bash
|
|
253
|
-
pip install 'context-loader==1.3.
|
|
253
|
+
pip install 'context-loader==1.3.1'
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
For development or source-based installs tracking current repository (`1.3.
|
|
256
|
+
For development or source-based installs tracking current repository (`1.3.1`):
|
|
257
257
|
|
|
258
258
|
```bash
|
|
259
259
|
uv tool install git+https://github.com/xuanheng-tech/context-loader.git
|
|
@@ -338,16 +338,17 @@ The output has no generated timestamp, AI summary, architecture inference, or di
|
|
|
338
338
|
## JSON Output
|
|
339
339
|
|
|
340
340
|
`--format json` writes exactly one separator-compact UTF-8 JSON document plus one trailing newline
|
|
341
|
-
to stdout
|
|
342
|
-
uses the identical serialization; only the field
|
|
343
|
-
consumption”. The declared contract for
|
|
341
|
+
to stdout, within the JSON document bound under “Limits”. Keys are serialized in sorted order
|
|
342
|
+
with `ensure_ascii=False`. `--format json-compact` uses the identical serialization; only the field
|
|
343
|
+
set differs, as described under “Compact model consumption”. The declared contract for
|
|
344
|
+
`--format json` is:
|
|
344
345
|
|
|
345
346
|
```json
|
|
346
347
|
{
|
|
347
348
|
"schema_version": 3,
|
|
348
349
|
"tool": {
|
|
349
350
|
"name": "context-loader",
|
|
350
|
-
"version": "1.3.
|
|
351
|
+
"version": "1.3.1"
|
|
351
352
|
},
|
|
352
353
|
"repository": {
|
|
353
354
|
"requested_path": "/canonical/requested/path",
|
|
@@ -408,11 +409,16 @@ no sizes, no mtimes and no contents. A symlinked (even dangling) `AGENTS.md` is
|
|
|
408
409
|
existence comes from the directory entry itself; the scan never resolves what it points at. Any
|
|
409
410
|
`.git`, `.venv`, `venv`, `node_modules` or `site-packages` directory is skipped at every depth:
|
|
410
411
|
package-manager and interpreter-managed trees are not authored repository instructions.
|
|
411
|
-
`list_truncated` means more matching entries exist beyond the report caps
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
`list_truncated` means more matching entries exist beyond the report caps: 32 paths, and a 4 KiB
|
|
413
|
+
budget metered on the emitted array itself — each path's escaped, JSON-serialized string plus its
|
|
414
|
+
separator, with the array's brackets charged to the first entry — so the emitted array never costs
|
|
415
|
+
more than the budget it declares. Because quoting is counted, an array already near the cap can
|
|
416
|
+
report one path fewer than an older release did, and then says so through `list_truncated`.
|
|
417
|
+
`scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, or a directory
|
|
418
|
+
or entry could not be read, so absence of a path is not proof of absence of the file. Nested paths
|
|
419
|
+
are sanitized with the same escaping the Markdown uses for repository-derived text, so a nested name
|
|
420
|
+
the filesystem could not decode never breaks the document;
|
|
421
|
+
see “Limits” for the root paths that are emitted verbatim. The corresponding
|
|
416
422
|
`nested_agents_list_truncated` and `nested_agents_scan_truncated` status entries mirror both flags.
|
|
417
423
|
Presence is not instruction: whether a nested file applies, and its text, remain the caller's
|
|
418
424
|
judgment; Markdown output is unchanged.
|
|
@@ -444,7 +450,7 @@ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward eac
|
|
|
444
450
|
names exactly one key set, and the `nested_context` renumber below enforces that rule going forward.
|
|
445
451
|
|
|
446
452
|
The JSON schema version and package version are independent: this build emits `schema_version` `3`
|
|
447
|
-
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.
|
|
453
|
+
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.1`. Version 1
|
|
448
454
|
and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
|
|
449
455
|
`nested_context` changes the default document's key set, version 3 names the current full-document
|
|
450
456
|
key set and version 4 names the compact projection.
|
|
@@ -497,7 +503,23 @@ scan that ends before EOF still reports the characters it could not select.
|
|
|
497
503
|
|
|
498
504
|
## Limits
|
|
499
505
|
|
|
500
|
-
|
|
506
|
+
Final output bounds differ per format. The two stdout bounds below are enforced on the bytes written
|
|
507
|
+
to stdout and the nested report cap on the bytes the document emits for that array; every other
|
|
508
|
+
bullet is a per-component, read or capture bound, as each one states:
|
|
509
|
+
|
|
510
|
+
- Markdown stdout: 98,304 bytes. A section that cannot fit is replaced by an omission notice, and so
|
|
511
|
+
is every later section, so the limit holds without cutting a section mid-body.
|
|
512
|
+
- JSON and JSON-compact stdout: 8,388,608 bytes (8 MiB) for the serialized document, including its
|
|
513
|
+
trailing newline. This bound is fail-closed: if a repository's evidence cannot fit, the tool exits
|
|
514
|
+
1, writes nothing to stdout and prints `error: JSON output exceeded the 8388608 byte document
|
|
515
|
+
limit`. It never emits a truncated document. The component budgets below keep legitimate
|
|
516
|
+
documents far inside it — the largest escape-dense repository audited for this release emitted
|
|
517
|
+
263 KiB, of which 248 KiB was escaped `statuses` subjects — so it is a final backstop rather
|
|
518
|
+
than an expected truncation point.
|
|
519
|
+
- Component budgets below are metered on the Markdown `context` content, and that same string is the
|
|
520
|
+
JSON `context` value; because JSON escaping expands quotes, backslashes and control characters,
|
|
521
|
+
the serialized bytes can be several times larger. The JSON document also repeats the source
|
|
522
|
+
bodies, so no single component figure bounds JSON stdout.
|
|
501
523
|
- `AGENTS.md`: 256-KiB bounded source scan; selected source plus selection audit remains at most
|
|
502
524
|
16 KiB, including a 4-KiB maximum small head
|
|
503
525
|
- `README.md`: 16 KiB
|
|
@@ -505,10 +527,14 @@ scan that ends before EOF still reports the characters it could not select.
|
|
|
505
527
|
- All entry-file bodies: 24 KiB
|
|
506
528
|
- Declared commands: 8 KiB
|
|
507
529
|
- Directory tree: 12 KiB, 300 entries, and depth 2
|
|
508
|
-
- Nested `AGENTS.md` presence scan: depth 4, 2,000 directories,
|
|
509
|
-
file contents are never read
|
|
530
|
+
- Nested `AGENTS.md` presence scan: depth 4, 2,000 directories, at most 32 reported paths and
|
|
531
|
+
a 4 KiB budget metered on the emitted array; file contents are never read
|
|
510
532
|
- Working-tree changes: 100 paths and 4 KiB
|
|
511
533
|
- Recent commits: 8
|
|
534
|
+
- JSON-only listings have no byte budget of their own: `statuses` subjects are escaped names bounded
|
|
535
|
+
by the entry counts above, while `repository` and `sources[*].path` are emitted verbatim, so a
|
|
536
|
+
repository root the filesystem could not decode makes the machine formats exit 1 where Markdown
|
|
537
|
+
still renders
|
|
512
538
|
- Git subprocess output: 16 MiB (bounded while reading)
|
|
513
539
|
- Candidate file validation: 16 MiB hard read bound
|
|
514
540
|
|
|
@@ -545,7 +571,7 @@ or candidate-file content.
|
|
|
545
571
|
|
|
546
572
|
## Not Included
|
|
547
573
|
|
|
548
|
-
Version 1.3.
|
|
574
|
+
Version 1.3.1 does not provide AI summaries, project-type detection, loading or transport of
|
|
549
575
|
nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
|
|
550
576
|
Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
|
|
551
577
|
network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
|
|
@@ -8,14 +8,14 @@ Python standard library.
|
|
|
8
8
|
|
|
9
9
|
## Open-source quick start
|
|
10
10
|
|
|
11
|
-
Current stable release: **1.3.
|
|
11
|
+
Current stable release: **1.3.1**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
uv tool install 'context-loader==1.3.
|
|
14
|
+
uv tool install 'context-loader==1.3.1'
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
pip install 'context-loader==1.3.
|
|
18
|
+
pip install 'context-loader==1.3.1'
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
```bash
|
|
@@ -28,21 +28,21 @@ project-context --repo /path/to/repo --format json
|
|
|
28
28
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
31
|
-
Source version: `1.3.
|
|
31
|
+
Source version: `1.3.1`. Install its matching published release or an exact source commit.
|
|
32
32
|
|
|
33
33
|
Install via `uv`:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
uv tool install 'context-loader==1.3.
|
|
36
|
+
uv tool install 'context-loader==1.3.1'
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Install via `pip`:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
pip install 'context-loader==1.3.
|
|
42
|
+
pip install 'context-loader==1.3.1'
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
For development or source-based installs tracking current repository (`1.3.
|
|
45
|
+
For development or source-based installs tracking current repository (`1.3.1`):
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
uv tool install git+https://github.com/xuanheng-tech/context-loader.git
|
|
@@ -127,16 +127,17 @@ The output has no generated timestamp, AI summary, architecture inference, or di
|
|
|
127
127
|
## JSON Output
|
|
128
128
|
|
|
129
129
|
`--format json` writes exactly one separator-compact UTF-8 JSON document plus one trailing newline
|
|
130
|
-
to stdout
|
|
131
|
-
uses the identical serialization; only the field
|
|
132
|
-
consumption”. The declared contract for
|
|
130
|
+
to stdout, within the JSON document bound under “Limits”. Keys are serialized in sorted order
|
|
131
|
+
with `ensure_ascii=False`. `--format json-compact` uses the identical serialization; only the field
|
|
132
|
+
set differs, as described under “Compact model consumption”. The declared contract for
|
|
133
|
+
`--format json` is:
|
|
133
134
|
|
|
134
135
|
```json
|
|
135
136
|
{
|
|
136
137
|
"schema_version": 3,
|
|
137
138
|
"tool": {
|
|
138
139
|
"name": "context-loader",
|
|
139
|
-
"version": "1.3.
|
|
140
|
+
"version": "1.3.1"
|
|
140
141
|
},
|
|
141
142
|
"repository": {
|
|
142
143
|
"requested_path": "/canonical/requested/path",
|
|
@@ -197,11 +198,16 @@ no sizes, no mtimes and no contents. A symlinked (even dangling) `AGENTS.md` is
|
|
|
197
198
|
existence comes from the directory entry itself; the scan never resolves what it points at. Any
|
|
198
199
|
`.git`, `.venv`, `venv`, `node_modules` or `site-packages` directory is skipped at every depth:
|
|
199
200
|
package-manager and interpreter-managed trees are not authored repository instructions.
|
|
200
|
-
`list_truncated` means more matching entries exist beyond the report caps
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
`list_truncated` means more matching entries exist beyond the report caps: 32 paths, and a 4 KiB
|
|
202
|
+
budget metered on the emitted array itself — each path's escaped, JSON-serialized string plus its
|
|
203
|
+
separator, with the array's brackets charged to the first entry — so the emitted array never costs
|
|
204
|
+
more than the budget it declares. Because quoting is counted, an array already near the cap can
|
|
205
|
+
report one path fewer than an older release did, and then says so through `list_truncated`.
|
|
206
|
+
`scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, or a directory
|
|
207
|
+
or entry could not be read, so absence of a path is not proof of absence of the file. Nested paths
|
|
208
|
+
are sanitized with the same escaping the Markdown uses for repository-derived text, so a nested name
|
|
209
|
+
the filesystem could not decode never breaks the document;
|
|
210
|
+
see “Limits” for the root paths that are emitted verbatim. The corresponding
|
|
205
211
|
`nested_agents_list_truncated` and `nested_agents_scan_truncated` status entries mirror both flags.
|
|
206
212
|
Presence is not instruction: whether a nested file applies, and its text, remain the caller's
|
|
207
213
|
judgment; Markdown output is unchanged.
|
|
@@ -233,7 +239,7 @@ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward eac
|
|
|
233
239
|
names exactly one key set, and the `nested_context` renumber below enforces that rule going forward.
|
|
234
240
|
|
|
235
241
|
The JSON schema version and package version are independent: this build emits `schema_version` `3`
|
|
236
|
-
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.
|
|
242
|
+
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.1`. Version 1
|
|
237
243
|
and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
|
|
238
244
|
`nested_context` changes the default document's key set, version 3 names the current full-document
|
|
239
245
|
key set and version 4 names the compact projection.
|
|
@@ -286,7 +292,23 @@ scan that ends before EOF still reports the characters it could not select.
|
|
|
286
292
|
|
|
287
293
|
## Limits
|
|
288
294
|
|
|
289
|
-
|
|
295
|
+
Final output bounds differ per format. The two stdout bounds below are enforced on the bytes written
|
|
296
|
+
to stdout and the nested report cap on the bytes the document emits for that array; every other
|
|
297
|
+
bullet is a per-component, read or capture bound, as each one states:
|
|
298
|
+
|
|
299
|
+
- Markdown stdout: 98,304 bytes. A section that cannot fit is replaced by an omission notice, and so
|
|
300
|
+
is every later section, so the limit holds without cutting a section mid-body.
|
|
301
|
+
- JSON and JSON-compact stdout: 8,388,608 bytes (8 MiB) for the serialized document, including its
|
|
302
|
+
trailing newline. This bound is fail-closed: if a repository's evidence cannot fit, the tool exits
|
|
303
|
+
1, writes nothing to stdout and prints `error: JSON output exceeded the 8388608 byte document
|
|
304
|
+
limit`. It never emits a truncated document. The component budgets below keep legitimate
|
|
305
|
+
documents far inside it — the largest escape-dense repository audited for this release emitted
|
|
306
|
+
263 KiB, of which 248 KiB was escaped `statuses` subjects — so it is a final backstop rather
|
|
307
|
+
than an expected truncation point.
|
|
308
|
+
- Component budgets below are metered on the Markdown `context` content, and that same string is the
|
|
309
|
+
JSON `context` value; because JSON escaping expands quotes, backslashes and control characters,
|
|
310
|
+
the serialized bytes can be several times larger. The JSON document also repeats the source
|
|
311
|
+
bodies, so no single component figure bounds JSON stdout.
|
|
290
312
|
- `AGENTS.md`: 256-KiB bounded source scan; selected source plus selection audit remains at most
|
|
291
313
|
16 KiB, including a 4-KiB maximum small head
|
|
292
314
|
- `README.md`: 16 KiB
|
|
@@ -294,10 +316,14 @@ scan that ends before EOF still reports the characters it could not select.
|
|
|
294
316
|
- All entry-file bodies: 24 KiB
|
|
295
317
|
- Declared commands: 8 KiB
|
|
296
318
|
- Directory tree: 12 KiB, 300 entries, and depth 2
|
|
297
|
-
- Nested `AGENTS.md` presence scan: depth 4, 2,000 directories,
|
|
298
|
-
file contents are never read
|
|
319
|
+
- Nested `AGENTS.md` presence scan: depth 4, 2,000 directories, at most 32 reported paths and
|
|
320
|
+
a 4 KiB budget metered on the emitted array; file contents are never read
|
|
299
321
|
- Working-tree changes: 100 paths and 4 KiB
|
|
300
322
|
- Recent commits: 8
|
|
323
|
+
- JSON-only listings have no byte budget of their own: `statuses` subjects are escaped names bounded
|
|
324
|
+
by the entry counts above, while `repository` and `sources[*].path` are emitted verbatim, so a
|
|
325
|
+
repository root the filesystem could not decode makes the machine formats exit 1 where Markdown
|
|
326
|
+
still renders
|
|
301
327
|
- Git subprocess output: 16 MiB (bounded while reading)
|
|
302
328
|
- Candidate file validation: 16 MiB hard read bound
|
|
303
329
|
|
|
@@ -334,7 +360,7 @@ or candidate-file content.
|
|
|
334
360
|
|
|
335
361
|
## Not Included
|
|
336
362
|
|
|
337
|
-
Version 1.3.
|
|
363
|
+
Version 1.3.1 does not provide AI summaries, project-type detection, loading or transport of
|
|
338
364
|
nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
|
|
339
365
|
Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
|
|
340
366
|
network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
|
|
@@ -23,15 +23,19 @@ from .collect import (
|
|
|
23
23
|
NestedContextPresence,
|
|
24
24
|
ProjectContext,
|
|
25
25
|
collect_project_context,
|
|
26
|
+
display_text,
|
|
26
27
|
)
|
|
27
28
|
from .git import ContextLoaderError, collect_repository
|
|
28
|
-
from .render import
|
|
29
|
+
from .render import render_markdown_with_details, rendered_source_contents
|
|
29
30
|
|
|
30
31
|
# Each value names one exact document shape and is never reused: 1 and 2 are the
|
|
31
32
|
# shapes published in release 1.2.0, so adding the nested_context object to both
|
|
32
33
|
# formats moved the default document to 3 and the compact projection to 4.
|
|
33
34
|
JSON_SCHEMA_VERSION = 3
|
|
34
35
|
COMPACT_JSON_SCHEMA_VERSION = 4
|
|
36
|
+
# Both machine formats share this final bound on the serialized document: output is
|
|
37
|
+
# either valid JSON within it or a fail-closed error, never a truncated document.
|
|
38
|
+
JSON_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024
|
|
35
39
|
TOOL_NAME = "context-loader"
|
|
36
40
|
|
|
37
41
|
_STATUS_CODE_BY_MESSAGE = {
|
|
@@ -190,7 +194,7 @@ def _build_statuses(
|
|
|
190
194
|
for entry in project.directory_tree.entries:
|
|
191
195
|
if entry.kind == "unreadable_directory":
|
|
192
196
|
subject = entry.path if entry.path else "."
|
|
193
|
-
statuses.append(_status("unreadable", "tree_entry",
|
|
197
|
+
statuses.append(_status("unreadable", "tree_entry", display_text(subject)))
|
|
194
198
|
|
|
195
199
|
for title in omitted_sections:
|
|
196
200
|
statuses.append(_status("section_omitted", "section", title))
|
|
@@ -305,6 +309,9 @@ def render_json(result: ProjectContextResult, *, compact: bool = False) -> bytes
|
|
|
305
309
|
``compact`` projects away the duplicated source bodies only at this
|
|
306
310
|
boundary: statuses and context were computed from the full model at load
|
|
307
311
|
time, so both documents agree on everything except ``sources[*].content``.
|
|
312
|
+
|
|
313
|
+
The bound is on the final serialized document: a repository whose evidence
|
|
314
|
+
cannot fit fails closed with no output rather than emitting a partial file.
|
|
308
315
|
"""
|
|
309
316
|
document = {
|
|
310
317
|
"schema_version": (COMPACT_JSON_SCHEMA_VERSION if compact else result.schema_version),
|
|
@@ -323,7 +330,7 @@ def render_json(result: ProjectContextResult, *, compact: bool = False) -> bytes
|
|
|
323
330
|
"nested_context": {
|
|
324
331
|
# Filesystem names arrive undecoded (surrogate-escaped); sanitize exactly like
|
|
325
332
|
# tree/change paths so no raw byte can reach the UTF-8 document.
|
|
326
|
-
"files": [
|
|
333
|
+
"files": [display_text(path) for path in result.nested_context.files],
|
|
327
334
|
"list_truncated": result.nested_context.list_truncated,
|
|
328
335
|
"scan_truncated": result.nested_context.scan_truncated,
|
|
329
336
|
},
|
|
@@ -337,4 +344,9 @@ def render_json(result: ProjectContextResult, *, compact: bool = False) -> bytes
|
|
|
337
344
|
sort_keys=True,
|
|
338
345
|
separators=(",", ":"),
|
|
339
346
|
)
|
|
340
|
-
|
|
347
|
+
encoded = f"{serialized}\n".encode()
|
|
348
|
+
if len(encoded) > JSON_OUTPUT_LIMIT_BYTES:
|
|
349
|
+
raise ContextLoaderError(
|
|
350
|
+
f"JSON output exceeded the {JSON_OUTPUT_LIMIT_BYTES} byte document limit"
|
|
351
|
+
)
|
|
352
|
+
return encoded
|
|
@@ -992,6 +992,40 @@ def _collect_directory_tree(root: Path) -> DirectoryTree:
|
|
|
992
992
|
return DirectoryTree(tuple(collected), truncated)
|
|
993
993
|
|
|
994
994
|
|
|
995
|
+
def display_text(value: str) -> str:
|
|
996
|
+
r"""Render one filesystem or Git string as printable, encodable ASCII-safe text.
|
|
997
|
+
|
|
998
|
+
Backticks, lone surrogates produced by ``surrogateescape`` decoding, and every
|
|
999
|
+
other non-printable codepoint become ``\xNN``/``\uNNNN``/``\UNNNNNNNN`` escapes so
|
|
1000
|
+
the character can cross the JSON and Markdown serialization boundaries intact.
|
|
1001
|
+
"""
|
|
1002
|
+
rendered: list[str] = []
|
|
1003
|
+
for character in value:
|
|
1004
|
+
codepoint = ord(character)
|
|
1005
|
+
if character == "`":
|
|
1006
|
+
rendered.append(r"\x60")
|
|
1007
|
+
elif 0xDC80 <= codepoint <= 0xDCFF:
|
|
1008
|
+
rendered.append(f"\\x{codepoint - 0xDC00:02x}")
|
|
1009
|
+
elif character.isprintable():
|
|
1010
|
+
rendered.append(character)
|
|
1011
|
+
elif codepoint <= 0xFF:
|
|
1012
|
+
rendered.append(f"\\x{codepoint:02x}")
|
|
1013
|
+
elif codepoint <= 0xFFFF:
|
|
1014
|
+
rendered.append(f"\\u{codepoint:04x}")
|
|
1015
|
+
else:
|
|
1016
|
+
rendered.append(f"\\U{codepoint:08x}")
|
|
1017
|
+
return "".join(rendered)
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def serialized_display_length(value: str) -> int:
|
|
1021
|
+
"""Return the UTF-8 byte size of ``value`` once escaped and JSON-serialized.
|
|
1022
|
+
|
|
1023
|
+
This is the exact bytes one ``nested_context`` entry costs, quotes included, so the
|
|
1024
|
+
report budget is metered on emitted output rather than on raw path bytes.
|
|
1025
|
+
"""
|
|
1026
|
+
return len(json.dumps(display_text(value), ensure_ascii=False).encode("utf-8"))
|
|
1027
|
+
|
|
1028
|
+
|
|
995
1029
|
def collect_nested_agents_presence(repository: Path) -> NestedContextPresence:
|
|
996
1030
|
"""List nested AGENTS.md paths under a bounded, contents-blind directory scan.
|
|
997
1031
|
|
|
@@ -1035,8 +1069,9 @@ def collect_nested_agents_presence(repository: Path) -> NestedContextPresence:
|
|
|
1035
1069
|
if len(files) >= NESTED_AGENTS_MAX_FILES:
|
|
1036
1070
|
state["list_truncated"] = True
|
|
1037
1071
|
continue
|
|
1038
|
-
|
|
1039
|
-
|
|
1072
|
+
# Charge the array's two bracket bytes to the first entry, so the budget
|
|
1073
|
+
# covers everything this list makes the document emit.
|
|
1074
|
+
name_bytes = serialized_display_length(path) + (1 if files else 2)
|
|
1040
1075
|
if reported_bytes + name_bytes > NESTED_AGENTS_MAX_LIST_BYTES:
|
|
1041
1076
|
state["list_truncated"] = True
|
|
1042
1077
|
continue
|
|
@@ -19,6 +19,7 @@ from .collect import (
|
|
|
19
19
|
DirectoryTree,
|
|
20
20
|
ProjectContext,
|
|
21
21
|
TreeEntry,
|
|
22
|
+
display_text,
|
|
22
23
|
render_agents_selection_audit,
|
|
23
24
|
)
|
|
24
25
|
from .git import RecentCommit, RepositoryState
|
|
@@ -39,30 +40,11 @@ class MarkdownRender:
|
|
|
39
40
|
commands_truncated: bool
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
def _display(value: str) -> str:
|
|
43
|
-
rendered: list[str] = []
|
|
44
|
-
for character in value:
|
|
45
|
-
codepoint = ord(character)
|
|
46
|
-
if character == "`":
|
|
47
|
-
rendered.append(r"\x60")
|
|
48
|
-
elif 0xDC80 <= codepoint <= 0xDCFF:
|
|
49
|
-
rendered.append(f"\\x{codepoint - 0xDC00:02x}")
|
|
50
|
-
elif character.isprintable():
|
|
51
|
-
rendered.append(character)
|
|
52
|
-
elif codepoint <= 0xFF:
|
|
53
|
-
rendered.append(f"\\x{codepoint:02x}")
|
|
54
|
-
elif codepoint <= 0xFFFF:
|
|
55
|
-
rendered.append(f"\\u{codepoint:04x}")
|
|
56
|
-
else:
|
|
57
|
-
rendered.append(f"\\U{codepoint:08x}")
|
|
58
|
-
return "".join(rendered)
|
|
59
|
-
|
|
60
|
-
|
|
61
43
|
def _display_limited(value: str, limit: int) -> str | None:
|
|
62
44
|
rendered: list[str] = []
|
|
63
45
|
used = 0
|
|
64
46
|
for character in value:
|
|
65
|
-
piece =
|
|
47
|
+
piece = display_text(character)
|
|
66
48
|
size = len(piece.encode())
|
|
67
49
|
if used + size > limit:
|
|
68
50
|
return None
|
|
@@ -121,7 +103,7 @@ def _change_lines(state: RepositoryState) -> tuple[list[str], bool]:
|
|
|
121
103
|
for change in state.changes:
|
|
122
104
|
if len(lines) == MAX_CHANGE_ENTRIES:
|
|
123
105
|
return lines, True
|
|
124
|
-
line = f"- `{change.status} {
|
|
106
|
+
line = f"- `{change.status} {display_text(change.path)}`"
|
|
125
107
|
line_bytes = len(f"{line}\n".encode())
|
|
126
108
|
if used_bytes + line_bytes > MAX_CHANGE_MARKDOWN_BYTES:
|
|
127
109
|
return lines, True
|
|
@@ -134,10 +116,10 @@ def _render_git_state(state: RepositoryState) -> str:
|
|
|
134
116
|
lines = [
|
|
135
117
|
"## Git State",
|
|
136
118
|
"",
|
|
137
|
-
f"- Branch: `{
|
|
138
|
-
f"- HEAD: `{
|
|
139
|
-
f"- Upstream: `{
|
|
140
|
-
f"- Ahead / behind: `{
|
|
119
|
+
f"- Branch: `{display_text(state.branch)}`",
|
|
120
|
+
f"- HEAD: `{display_text(state.head)}`",
|
|
121
|
+
f"- Upstream: `{display_text(state.upstream)}`",
|
|
122
|
+
f"- Ahead / behind: `{display_text(state.ahead_behind)}`",
|
|
141
123
|
f"- Worktree: `{state.worktree}`",
|
|
142
124
|
"",
|
|
143
125
|
"### Working Tree Changes",
|
|
@@ -170,10 +152,10 @@ def _command_line(command: DeclaredCommand) -> str:
|
|
|
170
152
|
if command.parse_error:
|
|
171
153
|
return f"- `{command.source}`: unable to parse command declarations."
|
|
172
154
|
assert command.invocation is not None
|
|
173
|
-
invocation =
|
|
155
|
+
invocation = display_text(command.invocation)
|
|
174
156
|
if command.target is None:
|
|
175
157
|
return f"- `{invocation}`"
|
|
176
|
-
return f"- `{invocation}` → `{
|
|
158
|
+
return f"- `{invocation}` → `{display_text(command.target)}`"
|
|
177
159
|
|
|
178
160
|
|
|
179
161
|
def _bounded_lines(
|
|
@@ -262,7 +244,7 @@ def _render_recent_commits(commits: tuple[RecentCommit, ...]) -> str | None:
|
|
|
262
244
|
|
|
263
245
|
|
|
264
246
|
def _tree_line(entry: TreeEntry) -> str:
|
|
265
|
-
path =
|
|
247
|
+
path = display_text(entry.path)
|
|
266
248
|
if entry.kind == "directory":
|
|
267
249
|
return f"{path}/"
|
|
268
250
|
if entry.kind == "symlink":
|
|
@@ -295,7 +277,7 @@ def render_markdown_with_details(state: RepositoryState, project: ProjectContext
|
|
|
295
277
|
"# Project Context",
|
|
296
278
|
"",
|
|
297
279
|
f"- Schema: `{SCHEMA}`",
|
|
298
|
-
f"- Repository: `{
|
|
280
|
+
f"- Repository: `{display_text(os.fspath(state.repository))}`",
|
|
299
281
|
)
|
|
300
282
|
)
|
|
301
283
|
commands_section, commands_truncated = _render_commands(project.commands)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|