context-loader 1.3.1__tar.gz → 1.3.2__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.1 → context_loader-1.3.2}/PKG-INFO +54 -19
- {context_loader-1.3.1 → context_loader-1.3.2}/README.md +53 -18
- {context_loader-1.3.1 → context_loader-1.3.2}/context_loader/__init__.py +1 -1
- {context_loader-1.3.1 → context_loader-1.3.2}/context_loader/application.py +64 -41
- {context_loader-1.3.1 → context_loader-1.3.2}/context_loader/cli.py +21 -1
- {context_loader-1.3.1 → context_loader-1.3.2}/context_loader/collect.py +164 -329
- context_loader-1.3.2/context_loader/filesystem.py +236 -0
- context_loader-1.3.2/context_loader/model.py +186 -0
- {context_loader-1.3.1 → context_loader-1.3.2}/context_loader/render.py +110 -30
- {context_loader-1.3.1 → context_loader-1.3.2}/pyproject.toml +1 -1
- {context_loader-1.3.1 → context_loader-1.3.2}/pyproject.toml.orig +1 -1
- {context_loader-1.3.1 → context_loader-1.3.2}/tool_cli_contract.json +1 -1
- {context_loader-1.3.1 → context_loader-1.3.2}/LICENSE +0 -0
- {context_loader-1.3.1 → context_loader-1.3.2}/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.2
|
|
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.2**. 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.2'
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
```bash
|
|
229
|
-
pip install 'context-loader==1.3.
|
|
229
|
+
pip install 'context-loader==1.3.2'
|
|
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.2`. 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.2'
|
|
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.2'
|
|
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.2`):
|
|
257
257
|
|
|
258
258
|
```bash
|
|
259
259
|
uv tool install git+https://github.com/xuanheng-tech/context-loader.git
|
|
@@ -319,6 +319,8 @@ and bare repositories are rejected in every mode.
|
|
|
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.
|
|
321
321
|
Calls that omit both options remain valid and use the conservative fallback described below.
|
|
322
|
+
Each value option (`--repo`, `--focus`, `--path`, `--format`) may be supplied once;
|
|
323
|
+
duplicates are rejected before collection rather than silently overriding an earlier value.
|
|
322
324
|
|
|
323
325
|
## Markdown Output
|
|
324
326
|
|
|
@@ -348,7 +350,7 @@ set differs, as described under “Compact model consumption”. The declared co
|
|
|
348
350
|
"schema_version": 3,
|
|
349
351
|
"tool": {
|
|
350
352
|
"name": "context-loader",
|
|
351
|
-
"version": "1.3.
|
|
353
|
+
"version": "1.3.2"
|
|
352
354
|
},
|
|
353
355
|
"repository": {
|
|
354
356
|
"requested_path": "/canonical/requested/path",
|
|
@@ -395,12 +397,12 @@ context, in assembly order, after the existing newline normalization and truncat
|
|
|
395
397
|
distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
|
|
396
398
|
only `repository` sources and does not add any global-file discovery.
|
|
397
399
|
|
|
398
|
-
`statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown — plus the nested-context truncation flags, which exist only in machine form: skipped or absent sources, truncated sources, unreadable directory-tree entries, sections omitted under the global output budget, truncated working-tree
|
|
400
|
+
`statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown — plus the nested-context truncation flags, which exist only in machine form: skipped or absent sources, truncated sources, unreadable directory-tree entries, directory-tree entries whose directory holds more entries than one enumeration examines, sections omitted under the global output budget, truncated working-tree, declared-command or directory-tree listings, and truncated nested-`AGENTS.md` presence reports. A skipped or absent source reports the code of the condition the collector observed (`not_present`, `skipped_symlink`, `skipped_not_regular`, `skipped_encoding`, `skipped_unreadable`); the sentence in `context` is rendered from that same condition, so wording and code cannot drift apart. Each entry has stable `code`, `subject_kind`, and `subject` fields. The two `nested_agents_*` codes mirror the `nested_context` truncation booleans for consumers that branch only on `statuses`; the full `nested_context` object remains a first-class field, never folded into `statuses`. Callers can ignore `statuses` safely.
|
|
399
401
|
|
|
400
402
|
The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
|
|
401
403
|
contain heading, heading level, and fixed selection reasons; it never contains the original focus or
|
|
402
404
|
target path. Existing per-source fields remain unchanged; the document's exact key
|
|
403
|
-
set (
|
|
405
|
+
set (which includes `nested_context`) is named by its `schema_version`, described below.
|
|
404
406
|
|
|
405
407
|
`nested_context` is always present and existence-only: `files` lists repository-relative paths of
|
|
406
408
|
non-directory `AGENTS.md` entries under subdirectories, found by a bounded scan that enumerates
|
|
@@ -414,8 +416,9 @@ budget metered on the emitted array itself — each path's escaped, JSON-seriali
|
|
|
414
416
|
separator, with the array's brackets charged to the first entry — so the emitted array never costs
|
|
415
417
|
more than the budget it declares. Because quoting is counted, an array already near the cap can
|
|
416
418
|
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,
|
|
418
|
-
|
|
419
|
+
`scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, a directory
|
|
420
|
+
held more entries than one enumeration examines (1,024), or a directory or entry could not be
|
|
421
|
+
read, so absence of a path is not proof of absence of the file. Nested paths
|
|
419
422
|
are sanitized with the same escaping the Markdown uses for repository-derived text, so a nested name
|
|
420
423
|
the filesystem could not decode never breaks the document;
|
|
421
424
|
see “Limits” for the root paths that are emitted verbatim. The corresponding
|
|
@@ -447,10 +450,10 @@ not otherwise change default Markdown bytes, exit codes or determinism.
|
|
|
447
450
|
|
|
448
451
|
One historical exception is acknowledged: releases 1.0.0 and 1.1.0 both emitted `schema_version` 1
|
|
449
452
|
although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward each emitted number
|
|
450
|
-
names exactly one key set, and
|
|
453
|
+
names exactly one key set, and a new number is issued whenever a key set would otherwise be reused.
|
|
451
454
|
|
|
452
455
|
The JSON schema version and package version are independent: this build emits `schema_version` `3`
|
|
453
|
-
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.
|
|
456
|
+
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.2`. Version 1
|
|
454
457
|
and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
|
|
455
458
|
`nested_context` changes the default document's key set, version 3 names the current full-document
|
|
456
459
|
key set and version 4 names the compact projection.
|
|
@@ -526,9 +529,40 @@ bullet is a per-component, read or capture bound, as each one states:
|
|
|
526
529
|
- Each entry file: 8 KiB
|
|
527
530
|
- All entry-file bodies: 24 KiB
|
|
528
531
|
- Declared commands: 8 KiB
|
|
529
|
-
- Directory tree: 12 KiB, 300 entries, and depth 2
|
|
530
|
-
|
|
531
|
-
|
|
532
|
+
- Directory tree: 12 KiB of listing body, 300 entries, and depth 2. Each directory is
|
|
533
|
+
enumerated up to 512 entries, keeping the alphabetically first names so the listing never
|
|
534
|
+
depends on operating system order. A directory that holds more is reported by a single
|
|
535
|
+
`Listing incomplete:` line stating the total, the limit and up to three names, and by at most
|
|
536
|
+
eight `directory_listing_incomplete` status entries naming individual directories plus one
|
|
537
|
+
aggregate entry giving the totals when more directories were capped than are named: beyond the
|
|
538
|
+
retained examples a capped directory is counted but not individually named, and no listing
|
|
539
|
+
claims completeness it does not have. That total covers the capped directories the listing
|
|
540
|
+
reached: a directory is reached only when its parent offered it inside the parent's own
|
|
541
|
+
alphabetical prefix and the entry budget kept it, so capped siblings displaced by the budget are
|
|
542
|
+
disclosed as their parent's truncation rather than as separate numbers.
|
|
543
|
+
Three limits act in sequence and a name survives only if all three leave room. The 512-entry
|
|
544
|
+
enumeration limit decides which names a directory offers at all; above it only an alphabetical
|
|
545
|
+
prefix is ever seen. The 300-entry item budget is then shared between a directory's
|
|
546
|
+
subdirectories and its non-directories: each may take up to half of what remains before the other
|
|
547
|
+
claims the surplus, which stops alphabetically-earlier directories from evicting every file, but
|
|
548
|
+
it is a share rather than a floor -- with a single slot left the non-directories take it. The 12
|
|
549
|
+
KiB listing budget then decides which kept names are rendered. Descent
|
|
550
|
+
happens after a directory's own entries, so deep content cannot displace top-level names, but
|
|
551
|
+
the tool promises no specific file: a root `README.md` is listed only when all three limits leave
|
|
552
|
+
room for it -- the enumeration offered it, the entry budget still has a slot, and the byte budget
|
|
553
|
+
still has room -- and each of those three can fail on its own. Being offered is not the same as
|
|
554
|
+
an uncapped root: the 512-entry cut drops only the names that sort after its retained prefix, so
|
|
555
|
+
an early-sorting `README.md` is still listed from a capped root while a late-sorting one never
|
|
556
|
+
reaches the listing. Whenever one of the three bites, the listing says so through the
|
|
557
|
+
note, the marker, or a `truncated` status instead of presenting itself as complete.
|
|
558
|
+
The `Listing incomplete:` line is charged to the 12 KiB listing budget before the body is cut,
|
|
559
|
+
so the claim can never inflate the section; its cost is paid in listing bytes, so a long note
|
|
560
|
+
removes that many bytes of entries, and the `truncated` status plus the marker inside the
|
|
561
|
+
fence say so. The heading and the code-fence lines are not metered, so the whole Markdown
|
|
562
|
+
section can exceed 12 KiB by that fixed few dozen bytes.
|
|
563
|
+
- Nested `AGENTS.md` presence scan: depth 4, 2,000 directories, at most 32 reported paths, a
|
|
564
|
+
4 KiB budget metered on the emitted array, and at most 1,024 entries examined per directory;
|
|
565
|
+
file contents are never read
|
|
532
566
|
- Working-tree changes: 100 paths and 4 KiB
|
|
533
567
|
- Recent commits: 8
|
|
534
568
|
- JSON-only listings have no byte budget of their own: `statuses` subjects are escaped names bounded
|
|
@@ -571,7 +605,7 @@ or candidate-file content.
|
|
|
571
605
|
|
|
572
606
|
## Not Included
|
|
573
607
|
|
|
574
|
-
Version 1.3.
|
|
608
|
+
Version 1.3.2 does not provide AI summaries, project-type detection, loading or transport of
|
|
575
609
|
nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
|
|
576
610
|
Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
|
|
577
611
|
network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
|
|
@@ -609,6 +643,7 @@ PyPI file bytes, metadata and SHA-256, plus the publisher/tag/commit claims in P
|
|
|
609
643
|
provenance. This is an identity check, not an independent cryptographic Sigstore verifier.
|
|
610
644
|
|
|
611
645
|
- Tag/version or expected-commit mismatch and `just check` failure stop before build/upload.
|
|
646
|
+
- Annotated tag headers must match the exact tag name and commit, without extra or duplicate headers.
|
|
612
647
|
- The build refuses to run unless `dist/` is empty, and refuses any produced file set other than
|
|
613
648
|
the current version's wheel and sdist. A leftover artifact can carry a release filename while
|
|
614
649
|
holding different bytes, so it is never treated as the current release.
|
|
@@ -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.2**. 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.2'
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
pip install 'context-loader==1.3.
|
|
18
|
+
pip install 'context-loader==1.3.2'
|
|
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.2`. 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.2'
|
|
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.2'
|
|
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.2`):
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
uv tool install git+https://github.com/xuanheng-tech/context-loader.git
|
|
@@ -108,6 +108,8 @@ and bare repositories are rejected in every mode.
|
|
|
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.
|
|
110
110
|
Calls that omit both options remain valid and use the conservative fallback described below.
|
|
111
|
+
Each value option (`--repo`, `--focus`, `--path`, `--format`) may be supplied once;
|
|
112
|
+
duplicates are rejected before collection rather than silently overriding an earlier value.
|
|
111
113
|
|
|
112
114
|
## Markdown Output
|
|
113
115
|
|
|
@@ -137,7 +139,7 @@ set differs, as described under “Compact model consumption”. The declared co
|
|
|
137
139
|
"schema_version": 3,
|
|
138
140
|
"tool": {
|
|
139
141
|
"name": "context-loader",
|
|
140
|
-
"version": "1.3.
|
|
142
|
+
"version": "1.3.2"
|
|
141
143
|
},
|
|
142
144
|
"repository": {
|
|
143
145
|
"requested_path": "/canonical/requested/path",
|
|
@@ -184,12 +186,12 @@ context, in assembly order, after the existing newline normalization and truncat
|
|
|
184
186
|
distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
|
|
185
187
|
only `repository` sources and does not add any global-file discovery.
|
|
186
188
|
|
|
187
|
-
`statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown — plus the nested-context truncation flags, which exist only in machine form: skipped or absent sources, truncated sources, unreadable directory-tree entries, sections omitted under the global output budget, truncated working-tree
|
|
189
|
+
`statuses` lists machine-readable collection and render conditions that previously appeared only inside `context` Markdown — plus the nested-context truncation flags, which exist only in machine form: skipped or absent sources, truncated sources, unreadable directory-tree entries, directory-tree entries whose directory holds more entries than one enumeration examines, sections omitted under the global output budget, truncated working-tree, declared-command or directory-tree listings, and truncated nested-`AGENTS.md` presence reports. A skipped or absent source reports the code of the condition the collector observed (`not_present`, `skipped_symlink`, `skipped_not_regular`, `skipped_encoding`, `skipped_unreadable`); the sentence in `context` is rendered from that same condition, so wording and code cannot drift apart. Each entry has stable `code`, `subject_kind`, and `subject` fields. The two `nested_agents_*` codes mirror the `nested_context` truncation booleans for consumers that branch only on `statuses`; the full `nested_context` object remains a first-class field, never folded into `statuses`. Callers can ignore `statuses` safely.
|
|
188
190
|
|
|
189
191
|
The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
|
|
190
192
|
contain heading, heading level, and fixed selection reasons; it never contains the original focus or
|
|
191
193
|
target path. Existing per-source fields remain unchanged; the document's exact key
|
|
192
|
-
set (
|
|
194
|
+
set (which includes `nested_context`) is named by its `schema_version`, described below.
|
|
193
195
|
|
|
194
196
|
`nested_context` is always present and existence-only: `files` lists repository-relative paths of
|
|
195
197
|
non-directory `AGENTS.md` entries under subdirectories, found by a bounded scan that enumerates
|
|
@@ -203,8 +205,9 @@ budget metered on the emitted array itself — each path's escaped, JSON-seriali
|
|
|
203
205
|
separator, with the array's brackets charged to the first entry — so the emitted array never costs
|
|
204
206
|
more than the budget it declares. Because quoting is counted, an array already near the cap can
|
|
205
207
|
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,
|
|
207
|
-
|
|
208
|
+
`scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, a directory
|
|
209
|
+
held more entries than one enumeration examines (1,024), or a directory or entry could not be
|
|
210
|
+
read, so absence of a path is not proof of absence of the file. Nested paths
|
|
208
211
|
are sanitized with the same escaping the Markdown uses for repository-derived text, so a nested name
|
|
209
212
|
the filesystem could not decode never breaks the document;
|
|
210
213
|
see “Limits” for the root paths that are emitted verbatim. The corresponding
|
|
@@ -236,10 +239,10 @@ not otherwise change default Markdown bytes, exit codes or determinism.
|
|
|
236
239
|
|
|
237
240
|
One historical exception is acknowledged: releases 1.0.0 and 1.1.0 both emitted `schema_version` 1
|
|
238
241
|
although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward each emitted number
|
|
239
|
-
names exactly one key set, and
|
|
242
|
+
names exactly one key set, and a new number is issued whenever a key set would otherwise be reused.
|
|
240
243
|
|
|
241
244
|
The JSON schema version and package version are independent: this build emits `schema_version` `3`
|
|
242
|
-
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.
|
|
245
|
+
for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.2`. Version 1
|
|
243
246
|
and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
|
|
244
247
|
`nested_context` changes the default document's key set, version 3 names the current full-document
|
|
245
248
|
key set and version 4 names the compact projection.
|
|
@@ -315,9 +318,40 @@ bullet is a per-component, read or capture bound, as each one states:
|
|
|
315
318
|
- Each entry file: 8 KiB
|
|
316
319
|
- All entry-file bodies: 24 KiB
|
|
317
320
|
- Declared commands: 8 KiB
|
|
318
|
-
- Directory tree: 12 KiB, 300 entries, and depth 2
|
|
319
|
-
|
|
320
|
-
|
|
321
|
+
- Directory tree: 12 KiB of listing body, 300 entries, and depth 2. Each directory is
|
|
322
|
+
enumerated up to 512 entries, keeping the alphabetically first names so the listing never
|
|
323
|
+
depends on operating system order. A directory that holds more is reported by a single
|
|
324
|
+
`Listing incomplete:` line stating the total, the limit and up to three names, and by at most
|
|
325
|
+
eight `directory_listing_incomplete` status entries naming individual directories plus one
|
|
326
|
+
aggregate entry giving the totals when more directories were capped than are named: beyond the
|
|
327
|
+
retained examples a capped directory is counted but not individually named, and no listing
|
|
328
|
+
claims completeness it does not have. That total covers the capped directories the listing
|
|
329
|
+
reached: a directory is reached only when its parent offered it inside the parent's own
|
|
330
|
+
alphabetical prefix and the entry budget kept it, so capped siblings displaced by the budget are
|
|
331
|
+
disclosed as their parent's truncation rather than as separate numbers.
|
|
332
|
+
Three limits act in sequence and a name survives only if all three leave room. The 512-entry
|
|
333
|
+
enumeration limit decides which names a directory offers at all; above it only an alphabetical
|
|
334
|
+
prefix is ever seen. The 300-entry item budget is then shared between a directory's
|
|
335
|
+
subdirectories and its non-directories: each may take up to half of what remains before the other
|
|
336
|
+
claims the surplus, which stops alphabetically-earlier directories from evicting every file, but
|
|
337
|
+
it is a share rather than a floor -- with a single slot left the non-directories take it. The 12
|
|
338
|
+
KiB listing budget then decides which kept names are rendered. Descent
|
|
339
|
+
happens after a directory's own entries, so deep content cannot displace top-level names, but
|
|
340
|
+
the tool promises no specific file: a root `README.md` is listed only when all three limits leave
|
|
341
|
+
room for it -- the enumeration offered it, the entry budget still has a slot, and the byte budget
|
|
342
|
+
still has room -- and each of those three can fail on its own. Being offered is not the same as
|
|
343
|
+
an uncapped root: the 512-entry cut drops only the names that sort after its retained prefix, so
|
|
344
|
+
an early-sorting `README.md` is still listed from a capped root while a late-sorting one never
|
|
345
|
+
reaches the listing. Whenever one of the three bites, the listing says so through the
|
|
346
|
+
note, the marker, or a `truncated` status instead of presenting itself as complete.
|
|
347
|
+
The `Listing incomplete:` line is charged to the 12 KiB listing budget before the body is cut,
|
|
348
|
+
so the claim can never inflate the section; its cost is paid in listing bytes, so a long note
|
|
349
|
+
removes that many bytes of entries, and the `truncated` status plus the marker inside the
|
|
350
|
+
fence say so. The heading and the code-fence lines are not metered, so the whole Markdown
|
|
351
|
+
section can exceed 12 KiB by that fixed few dozen bytes.
|
|
352
|
+
- Nested `AGENTS.md` presence scan: depth 4, 2,000 directories, at most 32 reported paths, a
|
|
353
|
+
4 KiB budget metered on the emitted array, and at most 1,024 entries examined per directory;
|
|
354
|
+
file contents are never read
|
|
321
355
|
- Working-tree changes: 100 paths and 4 KiB
|
|
322
356
|
- Recent commits: 8
|
|
323
357
|
- JSON-only listings have no byte budget of their own: `statuses` subjects are escaped names bounded
|
|
@@ -360,7 +394,7 @@ or candidate-file content.
|
|
|
360
394
|
|
|
361
395
|
## Not Included
|
|
362
396
|
|
|
363
|
-
Version 1.3.
|
|
397
|
+
Version 1.3.2 does not provide AI summaries, project-type detection, loading or transport of
|
|
364
398
|
nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
|
|
365
399
|
Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
|
|
366
400
|
network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
|
|
@@ -398,6 +432,7 @@ PyPI file bytes, metadata and SHA-256, plus the publisher/tag/commit claims in P
|
|
|
398
432
|
provenance. This is an identity check, not an independent cryptographic Sigstore verifier.
|
|
399
433
|
|
|
400
434
|
- Tag/version or expected-commit mismatch and `just check` failure stop before build/upload.
|
|
435
|
+
- Annotated tag headers must match the exact tag name and commit, without extra or duplicate headers.
|
|
401
436
|
- The build refuses to run unless `dist/` is empty, and refuses any produced file set other than
|
|
402
437
|
the current version's wheel and sdist. A leftover artifact can carry a release filename while
|
|
403
438
|
holding different bytes, so it is never treated as the current release.
|
|
@@ -5,28 +5,29 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
-
from dataclasses import dataclass
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
from . import __version__
|
|
12
12
|
from .collect import (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
AgentsSelectionInputError,
|
|
14
|
+
collect_project_context,
|
|
15
|
+
)
|
|
16
|
+
from .filesystem import display_text
|
|
17
|
+
from .git import ContextLoaderError, collect_repository
|
|
18
|
+
from .model import (
|
|
19
19
|
AgentsSectionAuditEntry,
|
|
20
20
|
AgentsSelectionAudit,
|
|
21
|
-
|
|
21
|
+
Availability,
|
|
22
22
|
CollectedFile,
|
|
23
23
|
NestedContextPresence,
|
|
24
24
|
ProjectContext,
|
|
25
|
-
collect_project_context,
|
|
26
|
-
display_text,
|
|
27
25
|
)
|
|
28
|
-
from .
|
|
29
|
-
|
|
26
|
+
from .render import (
|
|
27
|
+
RenderedSourceBody,
|
|
28
|
+
render_markdown_with_details,
|
|
29
|
+
rendered_source_contents,
|
|
30
|
+
)
|
|
30
31
|
|
|
31
32
|
# Each value names one exact document shape and is never reused: 1 and 2 are the
|
|
32
33
|
# shapes published in release 1.2.0, so adding the nested_context object to both
|
|
@@ -38,14 +39,6 @@ COMPACT_JSON_SCHEMA_VERSION = 4
|
|
|
38
39
|
JSON_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024
|
|
39
40
|
TOOL_NAME = "context-loader"
|
|
40
41
|
|
|
41
|
-
_STATUS_CODE_BY_MESSAGE = {
|
|
42
|
-
NOT_PRESENT: "not_present",
|
|
43
|
-
SKIPPED_SYMLINK: "skipped_symlink",
|
|
44
|
-
SKIPPED_NOT_REGULAR: "skipped_not_regular",
|
|
45
|
-
SKIPPED_ENCODING: "skipped_encoding",
|
|
46
|
-
SKIPPED_UNREADABLE: "skipped_unreadable",
|
|
47
|
-
}
|
|
48
|
-
|
|
49
42
|
|
|
50
43
|
@dataclass(frozen=True, slots=True)
|
|
51
44
|
class ToolIdentity:
|
|
@@ -77,6 +70,10 @@ class ContextStatus:
|
|
|
77
70
|
code: str
|
|
78
71
|
subject_kind: str
|
|
79
72
|
subject: str
|
|
73
|
+
# The raw fact behind the displayed subject. Display escaping is not injective, so
|
|
74
|
+
# deduplication must key on this and never on `subject`, or two different directories
|
|
75
|
+
# could collapse into one claim. It is not serialized.
|
|
76
|
+
identity: str = field(default="", compare=False)
|
|
80
77
|
|
|
81
78
|
|
|
82
79
|
@dataclass(frozen=True, slots=True)
|
|
@@ -115,13 +112,11 @@ def _source_kind(name: str) -> str:
|
|
|
115
112
|
|
|
116
113
|
def _sources(
|
|
117
114
|
canonical_root: Path,
|
|
118
|
-
|
|
119
|
-
included_sections: tuple[str, ...],
|
|
115
|
+
rendered_bodies: tuple[RenderedSourceBody, ...],
|
|
120
116
|
) -> tuple[ProjectContextSource, ...]:
|
|
121
117
|
sources: list[ProjectContextSource] = []
|
|
122
|
-
for ordinal,
|
|
123
|
-
|
|
124
|
-
):
|
|
118
|
+
for ordinal, rendered_body in enumerate(rendered_bodies):
|
|
119
|
+
source = rendered_body.source
|
|
125
120
|
path = canonical_root / source.name
|
|
126
121
|
sources.append(
|
|
127
122
|
ProjectContextSource(
|
|
@@ -129,22 +124,23 @@ def _sources(
|
|
|
129
124
|
kind=_source_kind(source.name),
|
|
130
125
|
scope=source_scope_for_path(path, canonical_root),
|
|
131
126
|
path=path,
|
|
132
|
-
content_sha256=_text_sha256(
|
|
133
|
-
content=
|
|
127
|
+
content_sha256=_text_sha256(rendered_body.body),
|
|
128
|
+
content=rendered_body.body,
|
|
134
129
|
selection=source.selection,
|
|
135
130
|
)
|
|
136
131
|
)
|
|
137
132
|
return tuple(sources)
|
|
138
133
|
|
|
139
134
|
|
|
140
|
-
def _status(code: str, subject_kind: str, subject: str) -> ContextStatus:
|
|
141
|
-
return ContextStatus(
|
|
135
|
+
def _status(code: str, subject_kind: str, subject: str, identity: str = "") -> ContextStatus:
|
|
136
|
+
return ContextStatus(
|
|
137
|
+
code=code, subject_kind=subject_kind, subject=subject, identity=identity or subject
|
|
138
|
+
)
|
|
142
139
|
|
|
143
140
|
|
|
144
141
|
def _statuses_for_source(source: CollectedFile) -> list[ContextStatus]:
|
|
145
|
-
if source.
|
|
146
|
-
|
|
147
|
-
return [_status(code, "source", source.name)]
|
|
142
|
+
if source.reason is not Availability.PRESENT:
|
|
143
|
+
return [_status(source.reason.value, "source", source.name)]
|
|
148
144
|
statuses: list[ContextStatus] = []
|
|
149
145
|
if source.truncated or (source.selection is not None and source.selection.truncated):
|
|
150
146
|
statuses.append(_status("truncated", "source", source.name))
|
|
@@ -164,7 +160,8 @@ def _build_statuses(
|
|
|
164
160
|
omitted_sections: tuple[str, ...],
|
|
165
161
|
changes_truncated: bool,
|
|
166
162
|
commands_truncated: bool,
|
|
167
|
-
|
|
163
|
+
tree_truncated: bool,
|
|
164
|
+
rendered_bodies: tuple[RenderedSourceBody, ...],
|
|
168
165
|
) -> tuple[ContextStatus, ...]:
|
|
169
166
|
statuses: list[ContextStatus] = []
|
|
170
167
|
for source in (project.instructions, project.overview, *project.entry_files):
|
|
@@ -175,10 +172,10 @@ def _build_statuses(
|
|
|
175
172
|
for status in statuses
|
|
176
173
|
if status.subject_kind == "source" and status.code == "truncated"
|
|
177
174
|
}
|
|
178
|
-
for
|
|
179
|
-
if
|
|
175
|
+
for rendered_body in rendered_bodies:
|
|
176
|
+
if not rendered_body.truncated:
|
|
180
177
|
continue
|
|
181
|
-
name = source.
|
|
178
|
+
name = rendered_body.source.name
|
|
182
179
|
if name in truncated_sources:
|
|
183
180
|
continue
|
|
184
181
|
statuses.append(_status("truncated", "source", name))
|
|
@@ -189,13 +186,37 @@ def _build_statuses(
|
|
|
189
186
|
if changes_truncated:
|
|
190
187
|
statuses.append(_status("truncated", "changes", "Working Tree Changes"))
|
|
191
188
|
|
|
192
|
-
if project.directory_tree.truncated:
|
|
189
|
+
if project.directory_tree.truncated or tree_truncated:
|
|
193
190
|
statuses.append(_status("truncated", "tree", "Directory Tree"))
|
|
194
191
|
for entry in project.directory_tree.entries:
|
|
195
192
|
if entry.kind == "unreadable_directory":
|
|
196
193
|
subject = entry.path if entry.path else "."
|
|
197
194
|
statuses.append(_status("unreadable", "tree_entry", display_text(subject)))
|
|
198
|
-
|
|
195
|
+
tree = project.directory_tree
|
|
196
|
+
named = list(dict.fromkeys(tree.incomplete_directories))
|
|
197
|
+
for path in named:
|
|
198
|
+
subject = path if path else "."
|
|
199
|
+
statuses.append(
|
|
200
|
+
_status(
|
|
201
|
+
"directory_listing_incomplete",
|
|
202
|
+
"tree_entry",
|
|
203
|
+
display_text(subject),
|
|
204
|
+
identity=path,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
unnamed = tree.incomplete_count - len(named)
|
|
208
|
+
if unnamed > 0:
|
|
209
|
+
stem = "directory" if unnamed == 1 else "directories"
|
|
210
|
+
statuses.append(
|
|
211
|
+
_status(
|
|
212
|
+
"directory_listing_incomplete",
|
|
213
|
+
"tree",
|
|
214
|
+
f"{tree.incomplete_count} directories exceeded the "
|
|
215
|
+
f"{tree.enumeration_limit}-entry per-directory enumeration limit; "
|
|
216
|
+
f"{unnamed} further {stem} not named individually",
|
|
217
|
+
identity=f"aggregate:{tree.incomplete_count}:{unnamed}",
|
|
218
|
+
)
|
|
219
|
+
)
|
|
199
220
|
for title in omitted_sections:
|
|
200
221
|
statuses.append(_status("section_omitted", "section", title))
|
|
201
222
|
|
|
@@ -207,7 +228,7 @@ def _build_statuses(
|
|
|
207
228
|
deduped: list[ContextStatus] = []
|
|
208
229
|
seen: set[tuple[str, str, str]] = set()
|
|
209
230
|
for status in statuses:
|
|
210
|
-
key = (status.code, status.subject_kind, status.
|
|
231
|
+
key = (status.code, status.subject_kind, status.identity)
|
|
211
232
|
if key in seen:
|
|
212
233
|
continue
|
|
213
234
|
seen.add(key)
|
|
@@ -233,13 +254,15 @@ def load_project_context(
|
|
|
233
254
|
raise ContextLoaderError(str(exc), exit_code=2) from None
|
|
234
255
|
rendered = render_markdown_with_details(state, project)
|
|
235
256
|
context = rendered.output.decode("utf-8")
|
|
236
|
-
|
|
257
|
+
rendered_bodies = rendered_source_contents(project, rendered.included_sections)
|
|
258
|
+
sources = _sources(state.repository, rendered_bodies)
|
|
237
259
|
statuses = _build_statuses(
|
|
238
260
|
project,
|
|
239
261
|
omitted_sections=rendered.omitted_sections,
|
|
240
262
|
changes_truncated=rendered.changes_truncated,
|
|
241
263
|
commands_truncated=rendered.commands_truncated,
|
|
242
|
-
|
|
264
|
+
tree_truncated=rendered.tree_truncated,
|
|
265
|
+
rendered_bodies=rendered_bodies,
|
|
243
266
|
)
|
|
244
267
|
return ProjectContextResult(
|
|
245
268
|
schema_version=JSON_SCHEMA_VERSION,
|
|
@@ -17,6 +17,21 @@ class SafeArgumentParser(argparse.ArgumentParser):
|
|
|
17
17
|
raise ContextLoaderError("invalid command-line arguments", exit_code=2)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class _OnceAction(argparse.Action):
|
|
21
|
+
def __call__(
|
|
22
|
+
self,
|
|
23
|
+
parser: argparse.ArgumentParser,
|
|
24
|
+
namespace: argparse.Namespace,
|
|
25
|
+
values: str,
|
|
26
|
+
option_string: str | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
seen = f"_seen_{self.dest}"
|
|
29
|
+
if getattr(namespace, seen, False):
|
|
30
|
+
parser.error("option supplied more than once")
|
|
31
|
+
setattr(namespace, seen, True)
|
|
32
|
+
setattr(namespace, self.dest, values)
|
|
33
|
+
|
|
34
|
+
|
|
20
35
|
def _parser() -> argparse.ArgumentParser:
|
|
21
36
|
parser = SafeArgumentParser(
|
|
22
37
|
prog="project-context",
|
|
@@ -24,20 +39,25 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
24
39
|
allow_abbrev=False,
|
|
25
40
|
)
|
|
26
41
|
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
27
|
-
parser.add_argument(
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--repo", required=True, action=_OnceAction, help="absolute Git worktree path"
|
|
44
|
+
)
|
|
28
45
|
parser.add_argument(
|
|
29
46
|
"--focus",
|
|
47
|
+
action=_OnceAction,
|
|
30
48
|
help="optional bounded task focus used for deterministic AGENTS section selection",
|
|
31
49
|
)
|
|
32
50
|
parser.add_argument(
|
|
33
51
|
"--path",
|
|
34
52
|
dest="target_path",
|
|
53
|
+
action=_OnceAction,
|
|
35
54
|
help="optional repository-relative target path used for AGENTS section selection",
|
|
36
55
|
)
|
|
37
56
|
parser.add_argument(
|
|
38
57
|
"--format",
|
|
39
58
|
choices=("markdown", "json", "json-compact"),
|
|
40
59
|
default="markdown",
|
|
60
|
+
action=_OnceAction,
|
|
41
61
|
help=(
|
|
42
62
|
"output format (default: markdown); json-compact omits the "
|
|
43
63
|
"duplicated source bodies (see README, Compact model consumption)"
|