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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: context-loader
3
- Version: 1.3.0
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.0**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
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.0'
225
+ uv tool install 'context-loader==1.3.1'
226
226
  ```
227
227
 
228
228
  ```bash
229
- pip install 'context-loader==1.3.0'
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.0`. Install its matching published release or an exact source commit.
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.0'
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.0'
253
+ pip install 'context-loader==1.3.1'
254
254
  ```
255
255
 
256
- For development or source-based installs tracking current repository (`1.3.0`):
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. Keys are serialized in sorted order with `ensure_ascii=False`. `--format json-compact`
342
- uses the identical serialization; only the field set differs, as described under “Compact model
343
- consumption”. The declared contract for `--format json` is:
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.0"
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 (32 paths and 4 KiB of
412
- path bytes); `scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, or
413
- a directory or entry could not be read, so absence of a path is not proof of absence of the file.
414
- Paths are sanitized with the same escaping the Markdown uses for repository-derived text, so the
415
- document is always valid UTF-8. The corresponding
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.0`. Version 1
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
- - Final stdout: 98,304 bytes
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, and at most 32 reported paths;
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.0 does not provide AI summaries, project-type detection, loading or transport of
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.0**. Local CLI for coding-agent workflows; renders deterministic Markdown or JSON.
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.0'
14
+ uv tool install 'context-loader==1.3.1'
15
15
  ```
16
16
 
17
17
  ```bash
18
- pip install 'context-loader==1.3.0'
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.0`. Install its matching published release or an exact source commit.
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.0'
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.0'
42
+ pip install 'context-loader==1.3.1'
43
43
  ```
44
44
 
45
- For development or source-based installs tracking current repository (`1.3.0`):
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. Keys are serialized in sorted order with `ensure_ascii=False`. `--format json-compact`
131
- uses the identical serialization; only the field set differs, as described under “Compact model
132
- consumption”. The declared contract for `--format json` is:
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.0"
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 (32 paths and 4 KiB of
201
- path bytes); `scan_truncated` means the depth (4) or directory-count (2,000) budget was reached, or
202
- a directory or entry could not be read, so absence of a path is not proof of absence of the file.
203
- Paths are sanitized with the same escaping the Markdown uses for repository-derived text, so the
204
- document is always valid UTF-8. The corresponding
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.0`. Version 1
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
- - Final stdout: 98,304 bytes
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, and at most 32 reported paths;
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.0 does not provide AI summaries, project-type detection, loading or transport of
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.
@@ -1,3 +1,3 @@
1
1
  """Project Context Loader."""
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.3.1"
@@ -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 _display, render_markdown_with_details, rendered_source_contents
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", _display(subject)))
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": [_display(path) for path in result.nested_context.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
- return f"{serialized}\n".encode()
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
- name_bytes = len(path.encode("utf-8", errors="surrogateescape"))
1039
- name_bytes += 1 if files else 0
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 = _display(character)
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} {_display(change.path)}`"
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: `{_display(state.branch)}`",
138
- f"- HEAD: `{_display(state.head)}`",
139
- f"- Upstream: `{_display(state.upstream)}`",
140
- f"- Ahead / behind: `{_display(state.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 = _display(command.invocation)
155
+ invocation = display_text(command.invocation)
174
156
  if command.target is None:
175
157
  return f"- `{invocation}`"
176
- return f"- `{invocation}` → `{_display(command.target)}`"
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 = _display(entry.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: `{_display(os.fspath(state.repository))}`",
280
+ f"- Repository: `{display_text(os.fspath(state.repository))}`",
299
281
  )
300
282
  )
301
283
  commands_section, commands_truncated = _render_commands(project.commands)
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "context-loader"
7
- version = "1.3.0"
7
+ version = "1.3.1"
8
8
  description = "Render deterministic local Git context for any executor"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12,<3.13"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "context-loader"
7
- version = "1.3.0"
7
+ version = "1.3.1"
8
8
  description = "Render deterministic local Git context for any executor"
9
9
  license = { file = "LICENSE" }
10
10
  readme = "README.md"
@@ -46,5 +46,5 @@
46
46
  ],
47
47
  "schema_version": 1,
48
48
  "tool_name": "context-loader",
49
- "tool_version": "1.3.0"
49
+ "tool_version": "1.3.1"
50
50
  }
File without changes