context-loader 1.3.2__tar.gz → 1.3.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: context-loader
3
- Version: 1.3.2
3
+ Version: 1.3.4
4
4
  Summary: Render deterministic local Git context for any executor
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -204,7 +204,7 @@ License: Apache License
204
204
  See the License for the specific language governing permissions and
205
205
  limitations under the License.
206
206
 
207
- Requires-Python: >=3.12, <3.13
207
+ Requires-Python: >=3.12, <3.15
208
208
  Project-URL: Repository, https://github.com/xuanheng-tech/context-loader
209
209
  Project-URL: Issues, https://github.com/xuanheng-tech/context-loader/issues
210
210
  Description-Content-Type: text/markdown
@@ -219,14 +219,14 @@ Python standard library.
219
219
 
220
220
  ## Open-source quick start
221
221
 
222
- Current stable release: **1.3.2**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
222
+ Current stable release: **1.3.4**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
223
223
 
224
224
  ```bash
225
- uv tool install 'context-loader==1.3.2'
225
+ uv tool install 'context-loader==1.3.4'
226
226
  ```
227
227
 
228
228
  ```bash
229
- pip install 'context-loader==1.3.2'
229
+ pip install 'context-loader==1.3.4'
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.2`. Install its matching published release or an exact source commit.
242
+ Source version: `1.3.4`. 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.2'
247
+ uv tool install 'context-loader==1.3.4'
248
248
  ```
249
249
 
250
250
  Install via `pip`:
251
251
 
252
252
  ```bash
253
- pip install 'context-loader==1.3.2'
253
+ pip install 'context-loader==1.3.4'
254
254
  ```
255
255
 
256
- For development or source-based installs tracking current repository (`1.3.2`):
256
+ For development or source-based installs tracking current repository (`1.3.4`):
257
257
 
258
258
  ```bash
259
259
  uv tool install git+https://github.com/xuanheng-tech/context-loader.git
@@ -279,7 +279,7 @@ part of either distribution; run them from a checkout of the matching tag.
279
279
 
280
280
  ## Platform and Runtime Requirements
281
281
 
282
- - **Python**: Python 3.12 (`>=3.12,<3.13`). Runtime code uses only the Python standard library with zero runtime dependencies.
282
+ - **Python**: Python 3.12–3.14 (`>=3.12,<3.15`). Runtime code uses only the Python standard library with zero runtime dependencies.
283
283
  - **Git**: Requires the standard `git` CLI at the fixed absolute path `/usr/bin/git`. The
284
284
  executable is never resolved through `PATH`, so an installation elsewhere is not used and
285
285
  every invocation fails with exit `1`. Git runs with a sanitized environment that ignores
@@ -350,7 +350,7 @@ set differs, as described under “Compact model consumption”. The declared co
350
350
  "schema_version": 3,
351
351
  "tool": {
352
352
  "name": "context-loader",
353
- "version": "1.3.2"
353
+ "version": "1.3.4"
354
354
  },
355
355
  "repository": {
356
356
  "requested_path": "/canonical/requested/path",
@@ -397,7 +397,14 @@ context, in assembly order, after the existing newline normalization and truncat
397
397
  distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
398
398
  only `repository` sources and does not add any global-file discovery.
399
399
 
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.
400
+ `statuses` lists skipped or absent sources, truncated sources, unreadable tree entries,
401
+ directories whose listing exceeds its retention cap, globally omitted sections, and truncated
402
+ working-tree, command, tree or nested-`AGENTS.md` listings. A skipped or absent source reports
403
+ the observed condition (`not_present`, `skipped_symlink`, `skipped_not_regular`,
404
+ `skipped_encoding`, `skipped_unreadable`); its sentence in `context` comes from that same
405
+ condition. Each entry has stable `code`, `subject_kind`, and `subject` fields. The two
406
+ `nested_agents_*` codes mirror the `nested_context` truncation booleans; the complete object
407
+ remains a separate field. Callers can ignore `statuses` safely.
401
408
 
402
409
  The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
403
410
  contain heading, heading level, and fixed selection reasons; it never contains the original focus or
@@ -417,7 +424,7 @@ separator, with the array's brackets charged to the first entry — so the emitt
417
424
  more than the budget it declares. Because quoting is counted, an array already near the cap can
418
425
  report one path fewer than an older release did, and then says so through `list_truncated`.
419
426
  `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
427
+ held more entries than one listing retains (1,024), or a directory or entry could not be
421
428
  read, so absence of a path is not proof of absence of the file. Nested paths
422
429
  are sanitized with the same escaping the Markdown uses for repository-derived text, so a nested name
423
430
  the filesystem could not decode never breaks the document;
@@ -426,6 +433,16 @@ see “Limits” for the root paths that are emitted verbatim. The corresponding
426
433
  Presence is not instruction: whether a nested file applies, and its text, remain the caller's
427
434
  judgment; Markdown output is unchanged.
428
435
 
436
+ Escaped names remain reversible and distinct: a literal backslash becomes `\\`, a backtick
437
+ becomes `\x60`, ASCII controls and undecodable filesystem bytes become `\xNN`, and other
438
+ non-printable Unicode characters become `\uNNNN` or `\UNNNNNNNN`. Printable Unicode stays
439
+ unchanged. After JSON decoding, recover POSIX path bytes by reading escapes from left to right:
440
+ `\\` yields one backslash byte, `\xNN` yields that byte, and Unicode escapes and ordinary
441
+ characters yield their UTF-8 bytes. Thus a backtick, a literal `\x60`, an undecodable byte
442
+ `0x80`, and Unicode `U+0080` all have different representations. This applies to nested paths
443
+ and displayed tree/change/status names; the verbatim root/source paths retain their existing
444
+ semantics.
445
+
429
446
  ### Compact model consumption (`--format json-compact`)
430
447
 
431
448
  `--format json-compact` emits a `schema_version` 4 document: exactly the version-3 document with
@@ -453,7 +470,7 @@ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward eac
453
470
  names exactly one key set, and a new number is issued whenever a key set would otherwise be reused.
454
471
 
455
472
  The JSON schema version and package version are independent: this build emits `schema_version` `3`
456
- for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.2`. Version 1
473
+ for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.4`. Version 1
457
474
  and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
458
475
  `nested_context` changes the default document's key set, version 3 names the current full-document
459
476
  key set and version 4 names the compact projection.
@@ -527,10 +544,11 @@ bullet is a per-component, read or capture bound, as each one states:
527
544
  16 KiB, including a 4-KiB maximum small head
528
545
  - `README.md`: 16 KiB
529
546
  - Each entry file: 8 KiB
530
- - All entry-file bodies: 24 KiB
547
+ - All entry-file bodies: 24 KiB, including truncation markers. Room is reserved for markers
548
+ on later text sources, so exhausting the budget never hides that their bodies were cut.
531
549
  - Declared commands: 8 KiB
532
550
  - 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
551
+ retains and classifies up to 512 entries, keeping the alphabetically first names so the listing never
534
552
  depends on operating system order. A directory that holds more is reported by a single
535
553
  `Listing incomplete:` line stating the total, the limit and up to three names, and by at most
536
554
  eight `directory_listing_incomplete` status entries naming individual directories plus one
@@ -541,8 +559,10 @@ bullet is a per-component, read or capture bound, as each one states:
541
559
  alphabetical prefix and the entry budget kept it, so capped siblings displaced by the budget are
542
560
  disclosed as their parent's truncation rather than as separate numbers.
543
561
  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
562
+ retention limit decides which names a directory offers at all; above it only an alphabetical
563
+ prefix is classified. All names must still be scanned to find that prefix, subject to the
564
+ separate hard name-scan bound below; the retention cap bounds memory and classification work.
565
+ The 300-entry item budget is then shared between a directory's
546
566
  subdirectories and its non-directories: each may take up to half of what remains before the other
547
567
  claims the surplus, which stops alphabetically-earlier directories from evicting every file, but
548
568
  it is a share rather than a floor -- with a single slot left the non-directories take it. The 12
@@ -561,8 +581,14 @@ bullet is a per-component, read or capture bound, as each one states:
561
581
  fence say so. The heading and the code-fence lines are not metered, so the whole Markdown
562
582
  section can exceed 12 KiB by that fixed few dozen bytes.
563
583
  - 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
584
+ 4 KiB budget metered on the emitted array, and at most 1,024 entries retained and classified
585
+ per directory. Names are scanned to select that prefix, subject to the separate hard
586
+ name-scan bound below; file contents are never read
587
+ - Directory name scans stop on the first entry beyond 65,536 names per directory. An
588
+ over-limit directory's entire partial prefix is discarded to keep results independent of
589
+ operating system order. The tree emits `Skipped: directory name scan limit reached.` and
590
+ a `directory_scan_limit_exceeded` status; the nested presence scan sets `scan_truncated`.
591
+ This bounds name iteration, not elapsed time on a blocked filesystem call.
566
592
  - Working-tree changes: 100 paths and 4 KiB
567
593
  - Recent commits: 8
568
594
  - JSON-only listings have no byte budget of their own: `statuses` subjects are escaped names bounded
@@ -570,6 +596,7 @@ bullet is a per-component, read or capture bound, as each one states:
570
596
  repository root the filesystem could not decode makes the machine formats exit 1 where Markdown
571
597
  still renders
572
598
  - Git subprocess output: 16 MiB (bounded while reading)
599
+ - Git status preflight: at most 128 repositories, including initialized nested submodules
573
600
  - Candidate file validation: 16 MiB hard read bound
574
601
 
575
602
  Truncation occurs only at complete UTF-8 and line boundaries and is marked explicitly.
@@ -593,6 +620,13 @@ the remote.
593
620
  this is normal after cloning an empty remote.
594
621
  - `Ahead / behind: not available` means the commit relationship cannot currently be computed.
595
622
  Counts are reported only when the configured upstream commit is available locally.
623
+ - Executable Git clean/process filters can run during `status`, even with optional locks
624
+ disabled. Before reading status, the loader checks effective repository configuration,
625
+ including includes, worktree configuration and initialized nested submodules. A non-empty
626
+ clean/process command causes a safe error with no stdout, even if no current attribute uses
627
+ it. Disabling a driver silently would change cleanliness semantics. Filter-dependent repositories
628
+ (including locally configured Git LFS drivers) therefore need ordinary Git inspection outside
629
+ this loader. Git protocols are also disabled to prevent lazy object retrieval.
596
630
 
597
631
  ## Exit Codes
598
632
 
@@ -605,7 +639,7 @@ or candidate-file content.
605
639
 
606
640
  ## Not Included
607
641
 
608
- Version 1.3.2 does not provide AI summaries, project-type detection, loading or transport of
642
+ Version 1.3.4 does not provide AI summaries, project-type detection, loading or transport of
609
643
  nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
610
644
  Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
611
645
  network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
@@ -618,6 +652,44 @@ Run the complete local check:
618
652
  just check
619
653
  ```
620
654
 
655
+ The shared check also builds a wheel and installs it into a fresh virtual environment. Package
656
+ acceptance runs the installed CLI outside the source checkout, checking deterministic agreement
657
+ between all three formats, linked worktrees, and safe failures. Build and installation run offline
658
+ using the backend cache populated by `uv run --frozen`; the tests use only disposable repositories.
659
+ Git deadline regressions verify that silent, streaming, and stdout-closed child processes are
660
+ terminated and reaped, and that a timeout leaves CLI stdout empty.
661
+
662
+ The lockfile uses public PyPI artifact URLs; no organization mirror or local Renovate preset
663
+ is needed. Renovate keeps scheduled, rate-limited updates and manual merges using a public
664
+ recommended preset plus the repository's explicit settings. The GitHub quality gate runs on
665
+ Python 3.12.13, 3.13.13 and 3.14.5; the Gitea root-runner gate and release builds use 3.12.13.
666
+ Other interpreter/platform combinations require validation before widening the supported range.
667
+
668
+ Git test fixtures discard inherited `GIT_*` routing/configuration before creating repositories.
669
+ Permission-sensitive tests must run as a non-root user. Gitea uses `just ci-check`, which installs
670
+ Python 3.12.13 without user-level executable links and keeps its interpreter/cache under
671
+ `$RUNNER_TEMP/context-loader-ci` (or ignored `.ci-runtime/context-loader-ci` locally).
672
+ Non-root runners execute directly; root containers transfer ownership of that runtime and the
673
+ checkout, then use `runuser` with an existing account named by `CONTEXT_LOADER_CI_USER` (default
674
+ `ubuntu`). That account must be able to execute the installed `uv` and `just`; it does not need
675
+ access to `/root` or a root-owned Python/cache. No `sudo` dependency is required for this CI step.
676
+
677
+ ### Performance baseline
678
+
679
+ On Linux with GNU `/usr/bin/time`, measure an explicit installed CLI using an existing empty
680
+ scratch directory:
681
+
682
+ ```bash
683
+ python scripts/benchmark.py --cli /absolute/path/to/project-context --scratch /absolute/empty/scratch
684
+ ```
685
+
686
+ The opt-in benchmark creates only synthetic repositories under that directory and reports median
687
+ elapsed time, peak RSS (KiB) and JSON-compact output bytes over five runs. It checks identical output
688
+ between runs and covers 1,000 AGENTS headings, a 5,000-file directory, and 1,000 untracked files.
689
+ Keep the CLI version, scratch path, interpreter and machine comparable when comparing runs;
690
+ measurements are observations, not timing thresholds in CI. Retain or remove the scratch directory
691
+ through your own temporary-resource workflow after inspection.
692
+
621
693
  ## Version maintenance
622
694
 
623
695
  The versions in `pyproject.toml` and `context_loader/__init__.py`, the `tool_version` in
@@ -638,6 +710,14 @@ only verifies the public release, mirrors its exact formal tag object if missing
638
710
  the Gitea Release record. It builds no package and has no PyPI publishing credentials.
639
711
  No private endpoint or credential is needed on GitHub.
640
712
 
713
+ For environments that permit direct public reads but share an exhausted proxy quota,
714
+ set the Gitea Actions repository variable `RELEASE_PUBLIC_API_DIRECT=true`. The record
715
+ step appends only public GitHub API/PyPI hosts to the existing proxy bypass lists. Without
716
+ this explicit option, the workflow uses its inherited proxy settings.
717
+ Both existing bypass lists and an explicit `*` wildcard are preserved. Public HTTPS GET
718
+ transport failures get at most three attempts within the existing total retry budget;
719
+ API writes stop on transport failure so their outcome can be verified before retrying.
720
+
641
721
  Release checks use Python 3.12, Git, and the runner's existing OpenSSL CLI. They compare downloaded
642
722
  PyPI file bytes, metadata and SHA-256, plus the publisher/tag/commit claims in PyPI's HTTPS-served
643
723
  provenance. This is an identity check, not an independent cryptographic Sigstore verifier.
@@ -8,14 +8,14 @@ Python standard library.
8
8
 
9
9
  ## Open-source quick start
10
10
 
11
- Current stable release: **1.3.2**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
11
+ Current stable release: **1.3.4**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
12
12
 
13
13
  ```bash
14
- uv tool install 'context-loader==1.3.2'
14
+ uv tool install 'context-loader==1.3.4'
15
15
  ```
16
16
 
17
17
  ```bash
18
- pip install 'context-loader==1.3.2'
18
+ pip install 'context-loader==1.3.4'
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.2`. Install its matching published release or an exact source commit.
31
+ Source version: `1.3.4`. 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.2'
36
+ uv tool install 'context-loader==1.3.4'
37
37
  ```
38
38
 
39
39
  Install via `pip`:
40
40
 
41
41
  ```bash
42
- pip install 'context-loader==1.3.2'
42
+ pip install 'context-loader==1.3.4'
43
43
  ```
44
44
 
45
- For development or source-based installs tracking current repository (`1.3.2`):
45
+ For development or source-based installs tracking current repository (`1.3.4`):
46
46
 
47
47
  ```bash
48
48
  uv tool install git+https://github.com/xuanheng-tech/context-loader.git
@@ -68,7 +68,7 @@ part of either distribution; run them from a checkout of the matching tag.
68
68
 
69
69
  ## Platform and Runtime Requirements
70
70
 
71
- - **Python**: Python 3.12 (`>=3.12,<3.13`). Runtime code uses only the Python standard library with zero runtime dependencies.
71
+ - **Python**: Python 3.12–3.14 (`>=3.12,<3.15`). Runtime code uses only the Python standard library with zero runtime dependencies.
72
72
  - **Git**: Requires the standard `git` CLI at the fixed absolute path `/usr/bin/git`. The
73
73
  executable is never resolved through `PATH`, so an installation elsewhere is not used and
74
74
  every invocation fails with exit `1`. Git runs with a sanitized environment that ignores
@@ -139,7 +139,7 @@ set differs, as described under “Compact model consumption”. The declared co
139
139
  "schema_version": 3,
140
140
  "tool": {
141
141
  "name": "context-loader",
142
- "version": "1.3.2"
142
+ "version": "1.3.4"
143
143
  },
144
144
  "repository": {
145
145
  "requested_path": "/canonical/requested/path",
@@ -186,7 +186,14 @@ context, in assembly order, after the existing newline normalization and truncat
186
186
  distinguishes `repository` from `global`; version 1.0.0's fixed root-file selection currently emits
187
187
  only `repository` sources and does not add any global-file discovery.
188
188
 
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.
189
+ `statuses` lists skipped or absent sources, truncated sources, unreadable tree entries,
190
+ directories whose listing exceeds its retention cap, globally omitted sections, and truncated
191
+ working-tree, command, tree or nested-`AGENTS.md` listings. A skipped or absent source reports
192
+ the observed condition (`not_present`, `skipped_symlink`, `skipped_not_regular`,
193
+ `skipped_encoding`, `skipped_unreadable`); its sentence in `context` comes from that same
194
+ condition. Each entry has stable `code`, `subject_kind`, and `subject` fields. The two
195
+ `nested_agents_*` codes mirror the `nested_context` truncation booleans; the complete object
196
+ remains a separate field. Callers can ignore `statuses` safely.
190
197
 
191
198
  The optional `selection` object is present only on a rendered `AGENTS.md` source. Its section entries
192
199
  contain heading, heading level, and fixed selection reasons; it never contains the original focus or
@@ -206,7 +213,7 @@ separator, with the array's brackets charged to the first entry — so the emitt
206
213
  more than the budget it declares. Because quoting is counted, an array already near the cap can
207
214
  report one path fewer than an older release did, and then says so through `list_truncated`.
208
215
  `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
216
+ held more entries than one listing retains (1,024), or a directory or entry could not be
210
217
  read, so absence of a path is not proof of absence of the file. Nested paths
211
218
  are sanitized with the same escaping the Markdown uses for repository-derived text, so a nested name
212
219
  the filesystem could not decode never breaks the document;
@@ -215,6 +222,16 @@ see “Limits” for the root paths that are emitted verbatim. The corresponding
215
222
  Presence is not instruction: whether a nested file applies, and its text, remain the caller's
216
223
  judgment; Markdown output is unchanged.
217
224
 
225
+ Escaped names remain reversible and distinct: a literal backslash becomes `\\`, a backtick
226
+ becomes `\x60`, ASCII controls and undecodable filesystem bytes become `\xNN`, and other
227
+ non-printable Unicode characters become `\uNNNN` or `\UNNNNNNNN`. Printable Unicode stays
228
+ unchanged. After JSON decoding, recover POSIX path bytes by reading escapes from left to right:
229
+ `\\` yields one backslash byte, `\xNN` yields that byte, and Unicode escapes and ordinary
230
+ characters yield their UTF-8 bytes. Thus a backtick, a literal `\x60`, an undecodable byte
231
+ `0x80`, and Unicode `U+0080` all have different representations. This applies to nested paths
232
+ and displayed tree/change/status names; the verbatim root/source paths retain their existing
233
+ semantics.
234
+
218
235
  ### Compact model consumption (`--format json-compact`)
219
236
 
220
237
  `--format json-compact` emits a `schema_version` 4 document: exactly the version-3 document with
@@ -242,7 +259,7 @@ although 1.1.0 added the top-level `statuses` key. From release 1.2.0 onward eac
242
259
  names exactly one key set, and a new number is issued whenever a key set would otherwise be reused.
243
260
 
244
261
  The JSON schema version and package version are independent: this build emits `schema_version` `3`
245
- for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.2`. Version 1
262
+ for `--format json` and `4` for `--format json-compact`, while `tool.version` is `1.3.4`. Version 1
246
263
  and 2 are the exact shapes already published in release 1.2.0 and are never reused: because
247
264
  `nested_context` changes the default document's key set, version 3 names the current full-document
248
265
  key set and version 4 names the compact projection.
@@ -316,10 +333,11 @@ bullet is a per-component, read or capture bound, as each one states:
316
333
  16 KiB, including a 4-KiB maximum small head
317
334
  - `README.md`: 16 KiB
318
335
  - Each entry file: 8 KiB
319
- - All entry-file bodies: 24 KiB
336
+ - All entry-file bodies: 24 KiB, including truncation markers. Room is reserved for markers
337
+ on later text sources, so exhausting the budget never hides that their bodies were cut.
320
338
  - Declared commands: 8 KiB
321
339
  - 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
340
+ retains and classifies up to 512 entries, keeping the alphabetically first names so the listing never
323
341
  depends on operating system order. A directory that holds more is reported by a single
324
342
  `Listing incomplete:` line stating the total, the limit and up to three names, and by at most
325
343
  eight `directory_listing_incomplete` status entries naming individual directories plus one
@@ -330,8 +348,10 @@ bullet is a per-component, read or capture bound, as each one states:
330
348
  alphabetical prefix and the entry budget kept it, so capped siblings displaced by the budget are
331
349
  disclosed as their parent's truncation rather than as separate numbers.
332
350
  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
351
+ retention limit decides which names a directory offers at all; above it only an alphabetical
352
+ prefix is classified. All names must still be scanned to find that prefix, subject to the
353
+ separate hard name-scan bound below; the retention cap bounds memory and classification work.
354
+ The 300-entry item budget is then shared between a directory's
335
355
  subdirectories and its non-directories: each may take up to half of what remains before the other
336
356
  claims the surplus, which stops alphabetically-earlier directories from evicting every file, but
337
357
  it is a share rather than a floor -- with a single slot left the non-directories take it. The 12
@@ -350,8 +370,14 @@ bullet is a per-component, read or capture bound, as each one states:
350
370
  fence say so. The heading and the code-fence lines are not metered, so the whole Markdown
351
371
  section can exceed 12 KiB by that fixed few dozen bytes.
352
372
  - 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
373
+ 4 KiB budget metered on the emitted array, and at most 1,024 entries retained and classified
374
+ per directory. Names are scanned to select that prefix, subject to the separate hard
375
+ name-scan bound below; file contents are never read
376
+ - Directory name scans stop on the first entry beyond 65,536 names per directory. An
377
+ over-limit directory's entire partial prefix is discarded to keep results independent of
378
+ operating system order. The tree emits `Skipped: directory name scan limit reached.` and
379
+ a `directory_scan_limit_exceeded` status; the nested presence scan sets `scan_truncated`.
380
+ This bounds name iteration, not elapsed time on a blocked filesystem call.
355
381
  - Working-tree changes: 100 paths and 4 KiB
356
382
  - Recent commits: 8
357
383
  - JSON-only listings have no byte budget of their own: `statuses` subjects are escaped names bounded
@@ -359,6 +385,7 @@ bullet is a per-component, read or capture bound, as each one states:
359
385
  repository root the filesystem could not decode makes the machine formats exit 1 where Markdown
360
386
  still renders
361
387
  - Git subprocess output: 16 MiB (bounded while reading)
388
+ - Git status preflight: at most 128 repositories, including initialized nested submodules
362
389
  - Candidate file validation: 16 MiB hard read bound
363
390
 
364
391
  Truncation occurs only at complete UTF-8 and line boundaries and is marked explicitly.
@@ -382,6 +409,13 @@ the remote.
382
409
  this is normal after cloning an empty remote.
383
410
  - `Ahead / behind: not available` means the commit relationship cannot currently be computed.
384
411
  Counts are reported only when the configured upstream commit is available locally.
412
+ - Executable Git clean/process filters can run during `status`, even with optional locks
413
+ disabled. Before reading status, the loader checks effective repository configuration,
414
+ including includes, worktree configuration and initialized nested submodules. A non-empty
415
+ clean/process command causes a safe error with no stdout, even if no current attribute uses
416
+ it. Disabling a driver silently would change cleanliness semantics. Filter-dependent repositories
417
+ (including locally configured Git LFS drivers) therefore need ordinary Git inspection outside
418
+ this loader. Git protocols are also disabled to prevent lazy object retrieval.
385
419
 
386
420
  ## Exit Codes
387
421
 
@@ -394,7 +428,7 @@ or candidate-file content.
394
428
 
395
429
  ## Not Included
396
430
 
397
- Version 1.3.2 does not provide AI summaries, project-type detection, loading or transport of
431
+ Version 1.3.4 does not provide AI summaries, project-type detection, loading or transport of
398
432
  nested `AGENTS.md` contents (the JSON `nested_context` field reports bounded existence only),
399
433
  Memory retrieval, semantic ranking, ignore-rule parsing, plugins, profiles, caches, databases,
400
434
  network services, MCP, daemons, GUIs, CI/CD, telemetry, or automatic updates.
@@ -407,6 +441,44 @@ Run the complete local check:
407
441
  just check
408
442
  ```
409
443
 
444
+ The shared check also builds a wheel and installs it into a fresh virtual environment. Package
445
+ acceptance runs the installed CLI outside the source checkout, checking deterministic agreement
446
+ between all three formats, linked worktrees, and safe failures. Build and installation run offline
447
+ using the backend cache populated by `uv run --frozen`; the tests use only disposable repositories.
448
+ Git deadline regressions verify that silent, streaming, and stdout-closed child processes are
449
+ terminated and reaped, and that a timeout leaves CLI stdout empty.
450
+
451
+ The lockfile uses public PyPI artifact URLs; no organization mirror or local Renovate preset
452
+ is needed. Renovate keeps scheduled, rate-limited updates and manual merges using a public
453
+ recommended preset plus the repository's explicit settings. The GitHub quality gate runs on
454
+ Python 3.12.13, 3.13.13 and 3.14.5; the Gitea root-runner gate and release builds use 3.12.13.
455
+ Other interpreter/platform combinations require validation before widening the supported range.
456
+
457
+ Git test fixtures discard inherited `GIT_*` routing/configuration before creating repositories.
458
+ Permission-sensitive tests must run as a non-root user. Gitea uses `just ci-check`, which installs
459
+ Python 3.12.13 without user-level executable links and keeps its interpreter/cache under
460
+ `$RUNNER_TEMP/context-loader-ci` (or ignored `.ci-runtime/context-loader-ci` locally).
461
+ Non-root runners execute directly; root containers transfer ownership of that runtime and the
462
+ checkout, then use `runuser` with an existing account named by `CONTEXT_LOADER_CI_USER` (default
463
+ `ubuntu`). That account must be able to execute the installed `uv` and `just`; it does not need
464
+ access to `/root` or a root-owned Python/cache. No `sudo` dependency is required for this CI step.
465
+
466
+ ### Performance baseline
467
+
468
+ On Linux with GNU `/usr/bin/time`, measure an explicit installed CLI using an existing empty
469
+ scratch directory:
470
+
471
+ ```bash
472
+ python scripts/benchmark.py --cli /absolute/path/to/project-context --scratch /absolute/empty/scratch
473
+ ```
474
+
475
+ The opt-in benchmark creates only synthetic repositories under that directory and reports median
476
+ elapsed time, peak RSS (KiB) and JSON-compact output bytes over five runs. It checks identical output
477
+ between runs and covers 1,000 AGENTS headings, a 5,000-file directory, and 1,000 untracked files.
478
+ Keep the CLI version, scratch path, interpreter and machine comparable when comparing runs;
479
+ measurements are observations, not timing thresholds in CI. Retain or remove the scratch directory
480
+ through your own temporary-resource workflow after inspection.
481
+
410
482
  ## Version maintenance
411
483
 
412
484
  The versions in `pyproject.toml` and `context_loader/__init__.py`, the `tool_version` in
@@ -427,6 +499,14 @@ only verifies the public release, mirrors its exact formal tag object if missing
427
499
  the Gitea Release record. It builds no package and has no PyPI publishing credentials.
428
500
  No private endpoint or credential is needed on GitHub.
429
501
 
502
+ For environments that permit direct public reads but share an exhausted proxy quota,
503
+ set the Gitea Actions repository variable `RELEASE_PUBLIC_API_DIRECT=true`. The record
504
+ step appends only public GitHub API/PyPI hosts to the existing proxy bypass lists. Without
505
+ this explicit option, the workflow uses its inherited proxy settings.
506
+ Both existing bypass lists and an explicit `*` wildcard are preserved. Public HTTPS GET
507
+ transport failures get at most three attempts within the existing total retry budget;
508
+ API writes stop on transport failure so their outcome can be verified before retrying.
509
+
430
510
  Release checks use Python 3.12, Git, and the runner's existing OpenSSL CLI. They compare downloaded
431
511
  PyPI file bytes, metadata and SHA-256, plus the publisher/tag/commit claims in PyPI's HTTPS-served
432
512
  provenance. This is an identity check, not an independent cryptographic Sigstore verifier.
@@ -1,3 +1,3 @@
1
1
  """Project Context Loader."""
2
2
 
3
- __version__ = "1.3.2"
3
+ __version__ = "1.3.4"
@@ -26,7 +26,6 @@ from .model import (
26
26
  from .render import (
27
27
  RenderedSourceBody,
28
28
  render_markdown_with_details,
29
- rendered_source_contents,
30
29
  )
31
30
 
32
31
  # Each value names one exact document shape and is never reused: 1 and 2 are the
@@ -70,9 +69,7 @@ class ContextStatus:
70
69
  code: str
71
70
  subject_kind: str
72
71
  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.
72
+ # Deduplicate raw facts independently of display formatting. Not serialized.
76
73
  identity: str = field(default="", compare=False)
77
74
 
78
75
 
@@ -191,7 +188,19 @@ def _build_statuses(
191
188
  for entry in project.directory_tree.entries:
192
189
  if entry.kind == "unreadable_directory":
193
190
  subject = entry.path if entry.path else "."
194
- statuses.append(_status("unreadable", "tree_entry", display_text(subject)))
191
+ statuses.append(
192
+ _status("unreadable", "tree_entry", display_text(subject), identity=subject)
193
+ )
194
+ elif entry.kind == "scan_limit_directory":
195
+ subject = entry.path if entry.path else "."
196
+ statuses.append(
197
+ _status(
198
+ "directory_scan_limit_exceeded",
199
+ "tree_entry",
200
+ display_text(subject),
201
+ identity=subject,
202
+ )
203
+ )
195
204
  tree = project.directory_tree
196
205
  named = list(dict.fromkeys(tree.incomplete_directories))
197
206
  for path in named:
@@ -212,7 +221,7 @@ def _build_statuses(
212
221
  "directory_listing_incomplete",
213
222
  "tree",
214
223
  f"{tree.incomplete_count} directories exceeded the "
215
- f"{tree.enumeration_limit}-entry per-directory enumeration limit; "
224
+ f"{tree.enumeration_limit}-entry per-directory retention limit; "
216
225
  f"{unnamed} further {stem} not named individually",
217
226
  identity=f"aggregate:{tree.incomplete_count}:{unnamed}",
218
227
  )
@@ -254,7 +263,7 @@ def load_project_context(
254
263
  raise ContextLoaderError(str(exc), exit_code=2) from None
255
264
  rendered = render_markdown_with_details(state, project)
256
265
  context = rendered.output.decode("utf-8")
257
- rendered_bodies = rendered_source_contents(project, rendered.included_sections)
266
+ rendered_bodies = rendered.source_bodies
258
267
  sources = _sources(state.repository, rendered_bodies)
259
268
  statuses = _build_statuses(
260
269
  project,