@ictechgy/context-guard 0.4.13 → 0.4.15
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.
- package/CHANGELOG.md +13 -0
- package/README.ko.md +36 -3
- package/README.md +40 -3
- package/docs/benchmark-fixtures/image-context-pack-full-evidence.prompt.example.md +28 -0
- package/docs/benchmark-fixtures/image-context-pack-packed-evidence.prompt.example.md +31 -0
- package/docs/benchmark-fixtures/image-context-pack.evidence.example.jsonl +2 -0
- package/docs/benchmark-fixtures/image-context-pack.tasks.example.json +18 -0
- package/docs/benchmark-fixtures/image-context-pack.variants.example.json +10 -0
- package/docs/benchmark-workflow-examples.md +16 -0
- package/docs/experimental-benchmark-fixtures.md +52 -1
- package/package.json +2 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +30 -1
- package/plugins/context-guard/README.md +33 -2
- package/plugins/context-guard/bin/context-guard-bench +1305 -115
- package/plugins/context-guard/bin/context-guard-experiments +3548 -146
- package/plugins/context-guard/bin/context-guard-mcp +999 -0
- package/plugins/context-guard/bin/context-guard-pack +634 -9
- package/plugins/context-guard/lib/context_guard_commands.py +8 -0
|
@@ -11,8 +11,10 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
13
|
import ast
|
|
14
|
+
from collections import Counter, deque
|
|
14
15
|
import copy
|
|
15
16
|
import hashlib
|
|
17
|
+
import heapq
|
|
16
18
|
import importlib.machinery
|
|
17
19
|
import importlib.util
|
|
18
20
|
import json
|
|
@@ -35,6 +37,8 @@ DEFAULT_BUDGET_BYTES = 12_000
|
|
|
35
37
|
MIN_BUDGET_BYTES = 0
|
|
36
38
|
MAX_BUDGET_BYTES = 2_000_000
|
|
37
39
|
MAX_RECEIPT_BYTES = 64_000
|
|
40
|
+
ROLLING_DELTA_SAMPLE_BYTES = 65_536
|
|
41
|
+
ROLLING_DELTA_WINDOW_BYTES = 64
|
|
38
42
|
MAX_MANIFEST_BYTES = 1_000_000
|
|
39
43
|
MAX_LABEL_CHARS = 160
|
|
40
44
|
MAX_REASON_CHARS = 120
|
|
@@ -45,6 +49,16 @@ AUTO_EXPLAIN_SCHEMA_VERSION = "contextguard.pack-auto-explain.v1"
|
|
|
45
49
|
REPO_MAP_SCHEMA_VERSION = "contextguard.pack-repo-map.v1"
|
|
46
50
|
ADAPTIVE_K_SCHEMA_VERSION = "contextguard.pack-adaptive-k.v1"
|
|
47
51
|
SYMBOL_MEMORY_SCHEMA_VERSION = "contextguard.pack-symbol-memory.v1"
|
|
52
|
+
CONTENT_ADDRESS_SCHEMA_VERSION = "contextguard.pack-content-address.v1"
|
|
53
|
+
ROLLING_DELTA_SCHEMA_VERSION = "contextguard.pack-rolling-delta.v1"
|
|
54
|
+
SKETCH_DUPLICATE_SHINGLE_WIDTH = 5
|
|
55
|
+
SKETCH_DUPLICATE_RETAINED_DIGESTS = 64
|
|
56
|
+
SKETCH_DUPLICATE_MIN_CARDINALITY = 12
|
|
57
|
+
SKETCH_DUPLICATE_THRESHOLD_NUMERATOR = 9
|
|
58
|
+
SKETCH_DUPLICATE_THRESHOLD_DENOMINATOR = 10
|
|
59
|
+
SKETCH_DUPLICATE_COMPARISON_CAP = 100_000
|
|
60
|
+
SKETCH_DUPLICATE_SHINGLE_DOMAIN = b"context-guard-pack/sketch-duplicate-veto/shingle/v1\x00"
|
|
61
|
+
SKETCH_DUPLICATE_TOKEN_RE = re.compile(r"\w+")
|
|
48
62
|
DEFAULT_SUGGEST_TOP = 8
|
|
49
63
|
MAX_SUGGEST_TOP = 50
|
|
50
64
|
DEFAULT_SUGGEST_CONTEXT_LINES = 20
|
|
@@ -165,6 +179,18 @@ class ResolvedSource:
|
|
|
165
179
|
redacted_lines: int
|
|
166
180
|
|
|
167
181
|
|
|
182
|
+
@dataclass
|
|
183
|
+
class _PairedCandidate:
|
|
184
|
+
source: ResolvedSource
|
|
185
|
+
canonical: dict[str, Any]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass(frozen=True)
|
|
189
|
+
class _DuplicateSignature:
|
|
190
|
+
exact_digest: bytes
|
|
191
|
+
sketch: frozenset[bytes] | None
|
|
192
|
+
|
|
193
|
+
|
|
168
194
|
@dataclass
|
|
169
195
|
class SuggestCandidate:
|
|
170
196
|
path: str
|
|
@@ -290,6 +316,303 @@ def sha256_text(text: str) -> str:
|
|
|
290
316
|
return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
|
|
291
317
|
|
|
292
318
|
|
|
319
|
+
def _sketch_duplicate_shingle_digest(tokens: tuple[str, ...]) -> bytes:
|
|
320
|
+
if len(tokens) != SKETCH_DUPLICATE_SHINGLE_WIDTH:
|
|
321
|
+
raise ValueError("sketch duplicate shingles require exactly five tokens")
|
|
322
|
+
digest = hashlib.sha256()
|
|
323
|
+
digest.update(SKETCH_DUPLICATE_SHINGLE_DOMAIN)
|
|
324
|
+
for token in tokens:
|
|
325
|
+
encoded = token.encode("utf-8", errors="strict")
|
|
326
|
+
digest.update(len(encoded).to_bytes(8, "big", signed=False))
|
|
327
|
+
digest.update(encoded)
|
|
328
|
+
return digest.digest()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _retain_bottom_sketch_digest(heap: list[tuple[int, bytes]], retained: set[bytes], digest: bytes) -> None:
|
|
332
|
+
if digest in retained:
|
|
333
|
+
return
|
|
334
|
+
number = int.from_bytes(digest, "big", signed=False)
|
|
335
|
+
if len(heap) < SKETCH_DUPLICATE_RETAINED_DIGESTS:
|
|
336
|
+
heapq.heappush(heap, (-number, digest))
|
|
337
|
+
retained.add(digest)
|
|
338
|
+
return
|
|
339
|
+
largest = heap[0][1]
|
|
340
|
+
if digest >= largest:
|
|
341
|
+
return
|
|
342
|
+
_negative, removed = heapq.heapreplace(heap, (-number, digest))
|
|
343
|
+
retained.remove(removed)
|
|
344
|
+
retained.add(digest)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _sketch_duplicate_signature(lines: list[str], *, include_sketch: bool) -> _DuplicateSignature:
|
|
348
|
+
exact = hashlib.sha256()
|
|
349
|
+
if not include_sketch:
|
|
350
|
+
for line in lines:
|
|
351
|
+
exact.update(line.encode("utf-8", errors="replace"))
|
|
352
|
+
return _DuplicateSignature(exact.digest(), None)
|
|
353
|
+
|
|
354
|
+
token_window: deque[str] = deque(maxlen=SKETCH_DUPLICATE_SHINGLE_WIDTH)
|
|
355
|
+
heap: list[tuple[int, bytes]] = []
|
|
356
|
+
retained: set[bytes] = set()
|
|
357
|
+
for line in lines:
|
|
358
|
+
exact.update(line.encode("utf-8", errors="replace"))
|
|
359
|
+
for match in SKETCH_DUPLICATE_TOKEN_RE.finditer(line.casefold()):
|
|
360
|
+
token_window.append(match.group(0))
|
|
361
|
+
if len(token_window) == SKETCH_DUPLICATE_SHINGLE_WIDTH:
|
|
362
|
+
digest = _sketch_duplicate_shingle_digest(tuple(token_window))
|
|
363
|
+
_retain_bottom_sketch_digest(heap, retained, digest)
|
|
364
|
+
return _DuplicateSignature(exact.digest(), frozenset(retained))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _sanitized_source_bytes_equal(left: ResolvedSource, right: ResolvedSource) -> bool:
|
|
368
|
+
if len(left.selected_lines) != len(right.selected_lines):
|
|
369
|
+
return False
|
|
370
|
+
return all(
|
|
371
|
+
left_line.encode("utf-8", errors="replace") == right_line.encode("utf-8", errors="replace")
|
|
372
|
+
for left_line, right_line in zip(left.selected_lines, right.selected_lines)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _sketch_sets_match(left: frozenset[bytes], right: frozenset[bytes]) -> bool:
|
|
377
|
+
intersection = len(left & right)
|
|
378
|
+
union = len(left) + len(right) - intersection
|
|
379
|
+
return (
|
|
380
|
+
union > 0
|
|
381
|
+
and SKETCH_DUPLICATE_THRESHOLD_DENOMINATOR * intersection
|
|
382
|
+
>= SKETCH_DUPLICATE_THRESHOLD_NUMERATOR * union
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _ordered_sketch_winner_ids(
|
|
387
|
+
sketch: frozenset[bytes],
|
|
388
|
+
postings: dict[bytes, list[int]],
|
|
389
|
+
) -> Any:
|
|
390
|
+
heap: list[tuple[int, int, int, list[int]]] = []
|
|
391
|
+
for ordinal, digest in enumerate(sorted(sketch)):
|
|
392
|
+
winner_ids = postings.get(digest)
|
|
393
|
+
if winner_ids:
|
|
394
|
+
heapq.heappush(heap, (winner_ids[0], ordinal, 0, winner_ids))
|
|
395
|
+
last_yielded: int | None = None
|
|
396
|
+
while heap:
|
|
397
|
+
winner_id, ordinal, index, winner_ids = heapq.heappop(heap)
|
|
398
|
+
next_index = index + 1
|
|
399
|
+
if next_index < len(winner_ids):
|
|
400
|
+
heapq.heappush(heap, (winner_ids[next_index], ordinal, next_index, winner_ids))
|
|
401
|
+
if winner_id != last_yielded:
|
|
402
|
+
last_yielded = winner_id
|
|
403
|
+
yield winner_id
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _sketch_duplicate_omission(source: ResolvedSource, *, root_arg: str) -> dict[str, Any]:
|
|
407
|
+
requested = source.requested_lines or LineRange(1, source.total_lines)
|
|
408
|
+
item = omission(
|
|
409
|
+
source.spec,
|
|
410
|
+
"sketch_duplicate_source",
|
|
411
|
+
path=source.display_path,
|
|
412
|
+
redacted_path=source.redacted_path,
|
|
413
|
+
)
|
|
414
|
+
item["requested_lines"] = requested.as_dict()
|
|
415
|
+
retrieval, retrieval_omitted_reason = retrieval_for(
|
|
416
|
+
root_arg,
|
|
417
|
+
source.display_path,
|
|
418
|
+
requested,
|
|
419
|
+
redacted_path=source.redacted_path,
|
|
420
|
+
)
|
|
421
|
+
if retrieval:
|
|
422
|
+
item["retrieval_cli"] = retrieval
|
|
423
|
+
item.pop("retrieval_omitted_reason", None)
|
|
424
|
+
elif retrieval_omitted_reason:
|
|
425
|
+
item["retrieval_omitted_reason"] = retrieval_omitted_reason
|
|
426
|
+
return item
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _apply_sketch_duplicate_veto(
|
|
430
|
+
candidates: list[_PairedCandidate],
|
|
431
|
+
omitted: list[dict[str, Any]],
|
|
432
|
+
*,
|
|
433
|
+
root_arg: str,
|
|
434
|
+
) -> tuple[list[ResolvedSource], bool]:
|
|
435
|
+
winners: list[_PairedCandidate] = []
|
|
436
|
+
exact_winners: dict[bytes, list[int]] = {}
|
|
437
|
+
winner_sketches: dict[int, frozenset[bytes]] = {}
|
|
438
|
+
postings: dict[bytes, list[int]] = {}
|
|
439
|
+
comparisons_remaining = SKETCH_DUPLICATE_COMPARISON_CAP
|
|
440
|
+
comparison_cap_reached = False
|
|
441
|
+
|
|
442
|
+
for candidate in candidates:
|
|
443
|
+
signature = _sketch_duplicate_signature(
|
|
444
|
+
candidate.source.selected_lines,
|
|
445
|
+
include_sketch=not comparison_cap_reached,
|
|
446
|
+
)
|
|
447
|
+
duplicate = False
|
|
448
|
+
for winner_id in exact_winners.get(signature.exact_digest, ()):
|
|
449
|
+
if _sanitized_source_bytes_equal(candidate.source, winners[winner_id].source):
|
|
450
|
+
duplicate = True
|
|
451
|
+
break
|
|
452
|
+
|
|
453
|
+
skipped_pair = False
|
|
454
|
+
sketch = signature.sketch
|
|
455
|
+
if (
|
|
456
|
+
not duplicate
|
|
457
|
+
and not comparison_cap_reached
|
|
458
|
+
and sketch is not None
|
|
459
|
+
and len(sketch) >= SKETCH_DUPLICATE_MIN_CARDINALITY
|
|
460
|
+
):
|
|
461
|
+
for winner_id in _ordered_sketch_winner_ids(sketch, postings):
|
|
462
|
+
if comparisons_remaining == 0:
|
|
463
|
+
comparison_cap_reached = True
|
|
464
|
+
skipped_pair = True
|
|
465
|
+
sketch = None
|
|
466
|
+
break
|
|
467
|
+
comparisons_remaining -= 1
|
|
468
|
+
if _sketch_sets_match(sketch, winner_sketches[winner_id]):
|
|
469
|
+
duplicate = True
|
|
470
|
+
break
|
|
471
|
+
|
|
472
|
+
if duplicate:
|
|
473
|
+
candidate.canonical["status"] = "sketch_duplicate_source"
|
|
474
|
+
omitted.append(_sketch_duplicate_omission(candidate.source, root_arg=root_arg))
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
winner_id = len(winners)
|
|
478
|
+
winners.append(candidate)
|
|
479
|
+
exact_winners.setdefault(signature.exact_digest, []).append(winner_id)
|
|
480
|
+
if (
|
|
481
|
+
not skipped_pair
|
|
482
|
+
and not comparison_cap_reached
|
|
483
|
+
and sketch is not None
|
|
484
|
+
and len(sketch) >= SKETCH_DUPLICATE_MIN_CARDINALITY
|
|
485
|
+
):
|
|
486
|
+
winner_sketches[winner_id] = sketch
|
|
487
|
+
for digest in sketch:
|
|
488
|
+
postings.setdefault(digest, []).append(winner_id)
|
|
489
|
+
|
|
490
|
+
return [candidate.source for candidate in winners], comparison_cap_reached
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def pack_id_arg(value: str) -> str:
|
|
494
|
+
if re.fullmatch(r"[0-9a-f]{20}", value) is None:
|
|
495
|
+
raise argparse.ArgumentTypeError("PACK_ID must be exactly 20 lowercase hexadecimal characters")
|
|
496
|
+
return value
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def content_address(digest: str, bytes_count: int) -> dict[str, Any]:
|
|
500
|
+
return {
|
|
501
|
+
"schema_version": CONTENT_ADDRESS_SCHEMA_VERSION,
|
|
502
|
+
"id": f"sha256:{digest}",
|
|
503
|
+
"algorithm": "sha256",
|
|
504
|
+
"digest": digest,
|
|
505
|
+
"bytes": bytes_count,
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def rolling_sample_metadata(body: bytes) -> dict[str, Any]:
|
|
510
|
+
sampled_bytes = min(len(body), ROLLING_DELTA_SAMPLE_BYTES)
|
|
511
|
+
if sampled_bytes == 0:
|
|
512
|
+
window_count = 0
|
|
513
|
+
elif sampled_bytes < ROLLING_DELTA_WINDOW_BYTES:
|
|
514
|
+
window_count = 1
|
|
515
|
+
else:
|
|
516
|
+
window_count = sampled_bytes - ROLLING_DELTA_WINDOW_BYTES + 1
|
|
517
|
+
return {
|
|
518
|
+
"total_bytes": len(body),
|
|
519
|
+
"sampled_bytes": sampled_bytes,
|
|
520
|
+
"window_count": window_count,
|
|
521
|
+
"truncated": len(body) > ROLLING_DELTA_SAMPLE_BYTES,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def rolling_window_multiset(body: bytes) -> Counter[bytes]:
|
|
526
|
+
sample = body[:ROLLING_DELTA_SAMPLE_BYTES]
|
|
527
|
+
if not sample:
|
|
528
|
+
return Counter()
|
|
529
|
+
if len(sample) < ROLLING_DELTA_WINDOW_BYTES:
|
|
530
|
+
return Counter((hashlib.sha256(sample).digest(),))
|
|
531
|
+
return Counter(
|
|
532
|
+
hashlib.sha256(sample[index:index + ROLLING_DELTA_WINDOW_BYTES]).digest()
|
|
533
|
+
for index in range(len(sample) - ROLLING_DELTA_WINDOW_BYTES + 1)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def rolling_delta_algorithm() -> dict[str, Any]:
|
|
538
|
+
return {
|
|
539
|
+
"name": "sha256_sliding_window_multiset",
|
|
540
|
+
"window_bytes": ROLLING_DELTA_WINDOW_BYTES,
|
|
541
|
+
"stride_bytes": 1,
|
|
542
|
+
"max_sample_bytes_per_pack": ROLLING_DELTA_SAMPLE_BYTES,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def rolling_delta_claim_boundary() -> dict[str, bool]:
|
|
547
|
+
return {
|
|
548
|
+
"diagnostic_only": True,
|
|
549
|
+
"changes_manifest_selection_or_pack": False,
|
|
550
|
+
"provider_token_or_cost_savings_claim_allowed": False,
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def build_rolling_delta(
|
|
555
|
+
current_pack: str,
|
|
556
|
+
previous_pack: str,
|
|
557
|
+
previous_pack_id: str,
|
|
558
|
+
current_address: str,
|
|
559
|
+
) -> dict[str, Any]:
|
|
560
|
+
current_body = current_pack.encode("utf-8")
|
|
561
|
+
previous_body = previous_pack.encode("utf-8")
|
|
562
|
+
current_meta = rolling_sample_metadata(current_body)
|
|
563
|
+
previous_meta = rolling_sample_metadata(previous_body)
|
|
564
|
+
current_windows = rolling_window_multiset(current_body)
|
|
565
|
+
previous_windows = rolling_window_multiset(previous_body)
|
|
566
|
+
matched = sum((current_windows & previous_windows).values())
|
|
567
|
+
current_count = current_meta["window_count"]
|
|
568
|
+
previous_count = previous_meta["window_count"]
|
|
569
|
+
both_empty = current_count == 0 and previous_count == 0
|
|
570
|
+
if both_empty:
|
|
571
|
+
current_ratio = 1.0
|
|
572
|
+
previous_ratio = 1.0
|
|
573
|
+
else:
|
|
574
|
+
current_ratio = round(matched / current_count, 6) if current_count else 0.0
|
|
575
|
+
previous_ratio = round(matched / previous_count, 6) if previous_count else 0.0
|
|
576
|
+
return {
|
|
577
|
+
"schema_version": ROLLING_DELTA_SCHEMA_VERSION,
|
|
578
|
+
"status": "partial" if current_meta["truncated"] or previous_meta["truncated"] else "available",
|
|
579
|
+
"previous_pack_id": previous_pack_id,
|
|
580
|
+
"current_content_address": current_address,
|
|
581
|
+
"previous_content_address": f"sha256:{hashlib.sha256(previous_body).hexdigest()}",
|
|
582
|
+
"algorithm": rolling_delta_algorithm(),
|
|
583
|
+
"current": current_meta,
|
|
584
|
+
"previous": previous_meta,
|
|
585
|
+
"matched_window_count": matched,
|
|
586
|
+
"current_reuse_ratio_proxy": current_ratio,
|
|
587
|
+
"previous_retention_ratio_proxy": previous_ratio,
|
|
588
|
+
"reason": None,
|
|
589
|
+
"claim_boundary": rolling_delta_claim_boundary(),
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def unavailable_rolling_delta(
|
|
594
|
+
current_pack: str,
|
|
595
|
+
previous_pack_id: str,
|
|
596
|
+
current_address: str,
|
|
597
|
+
reason: str,
|
|
598
|
+
) -> dict[str, Any]:
|
|
599
|
+
return {
|
|
600
|
+
"schema_version": ROLLING_DELTA_SCHEMA_VERSION,
|
|
601
|
+
"status": "unavailable",
|
|
602
|
+
"previous_pack_id": previous_pack_id,
|
|
603
|
+
"current_content_address": current_address,
|
|
604
|
+
"previous_content_address": None,
|
|
605
|
+
"algorithm": rolling_delta_algorithm(),
|
|
606
|
+
"current": rolling_sample_metadata(current_pack.encode("utf-8")),
|
|
607
|
+
"previous": {"total_bytes": 0, "sampled_bytes": 0, "window_count": 0, "truncated": False},
|
|
608
|
+
"matched_window_count": 0,
|
|
609
|
+
"current_reuse_ratio_proxy": 0.0,
|
|
610
|
+
"previous_retention_ratio_proxy": 0.0,
|
|
611
|
+
"reason": reason,
|
|
612
|
+
"claim_boundary": rolling_delta_claim_boundary(),
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
|
|
293
616
|
def path_hash(path: Path) -> str:
|
|
294
617
|
return hashlib.sha256(str(path).encode("utf-8", "replace")).hexdigest()[:12]
|
|
295
618
|
|
|
@@ -1168,6 +1491,211 @@ def shrink_receipt_for_write(data: dict[str, Any]) -> tuple[dict[str, Any], bool
|
|
|
1168
1491
|
return receipt, capped
|
|
1169
1492
|
|
|
1170
1493
|
|
|
1494
|
+
class ReceiptJSONError(ValueError):
|
|
1495
|
+
pass
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
def strict_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
1499
|
+
result: dict[str, Any] = {}
|
|
1500
|
+
for key, value in pairs:
|
|
1501
|
+
if key in result:
|
|
1502
|
+
raise ReceiptJSONError("duplicate key")
|
|
1503
|
+
result[key] = value
|
|
1504
|
+
return result
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
def reject_json_constant(_value: str) -> Any:
|
|
1508
|
+
raise ReceiptJSONError("non-finite number")
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def parse_receipt_int(value: str) -> int:
|
|
1512
|
+
digits = value.removeprefix("-")
|
|
1513
|
+
if len(digits) > 20:
|
|
1514
|
+
raise ReceiptJSONError("integer too large")
|
|
1515
|
+
return int(value)
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
def json_depth(value: Any, depth: int = 1) -> int:
|
|
1519
|
+
if depth > 100:
|
|
1520
|
+
raise ReceiptJSONError("maximum depth exceeded")
|
|
1521
|
+
if isinstance(value, dict):
|
|
1522
|
+
for item in value.values():
|
|
1523
|
+
json_depth(item, depth + 1)
|
|
1524
|
+
elif isinstance(value, list):
|
|
1525
|
+
for item in value:
|
|
1526
|
+
json_depth(item, depth + 1)
|
|
1527
|
+
return depth
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def prior_read_capabilities_available() -> bool:
|
|
1531
|
+
return (
|
|
1532
|
+
hasattr(os, "O_NOFOLLOW")
|
|
1533
|
+
and hasattr(os, "geteuid")
|
|
1534
|
+
and os.open in getattr(os, "supports_dir_fd", set())
|
|
1535
|
+
and os.stat in getattr(os, "supports_dir_fd", set())
|
|
1536
|
+
and os.stat in getattr(os, "supports_follow_symlinks", set())
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def private_receipt_stat_safe(st: os.stat_result, *, directory: bool) -> bool:
|
|
1541
|
+
expected_mode = 0o700 if directory else 0o600
|
|
1542
|
+
expected_type = stat.S_ISDIR(st.st_mode) if directory else stat.S_ISREG(st.st_mode)
|
|
1543
|
+
return (
|
|
1544
|
+
expected_type
|
|
1545
|
+
and st.st_uid == os.geteuid()
|
|
1546
|
+
and stat.S_IMODE(st.st_mode) == expected_mode
|
|
1547
|
+
and (directory or st.st_nlink == 1)
|
|
1548
|
+
)
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def stat_identity(st: os.stat_result) -> tuple[int, int, int, int, int, int]:
|
|
1552
|
+
return (st.st_dev, st.st_ino, st.st_mode, st.st_uid, st.st_nlink, st.st_size)
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
def read_previous_receipt(root: Path, requested_id: str) -> tuple[str | None, str | None]:
|
|
1556
|
+
if not prior_read_capabilities_available():
|
|
1557
|
+
return None, "previous_receipt_unsafe"
|
|
1558
|
+
current_fd: int | None = None
|
|
1559
|
+
file_fd: int | None = None
|
|
1560
|
+
parent_stats: list[os.stat_result] = []
|
|
1561
|
+
try:
|
|
1562
|
+
current_fd = open_dir_no_follow(root)
|
|
1563
|
+
for part in (".context-guard", "packs"):
|
|
1564
|
+
try:
|
|
1565
|
+
next_fd = open_dir_no_follow(part, dir_fd=current_fd)
|
|
1566
|
+
except FileNotFoundError:
|
|
1567
|
+
return None, "previous_receipt_not_found"
|
|
1568
|
+
except (OSError, PackError, NotImplementedError):
|
|
1569
|
+
return None, "previous_receipt_unsafe"
|
|
1570
|
+
os.close(current_fd)
|
|
1571
|
+
current_fd = next_fd
|
|
1572
|
+
try:
|
|
1573
|
+
parent_stat = os.fstat(current_fd)
|
|
1574
|
+
except OSError:
|
|
1575
|
+
return None, "previous_receipt_unsafe"
|
|
1576
|
+
parent_stats.append(parent_stat)
|
|
1577
|
+
|
|
1578
|
+
filename = f"{requested_id}.json"
|
|
1579
|
+
try:
|
|
1580
|
+
before = os.stat(filename, dir_fd=current_fd, follow_symlinks=False)
|
|
1581
|
+
except FileNotFoundError:
|
|
1582
|
+
return None, "previous_receipt_not_found"
|
|
1583
|
+
except (OSError, NotImplementedError):
|
|
1584
|
+
return None, "previous_receipt_unsafe"
|
|
1585
|
+
unsafe_parent = any(not private_receipt_stat_safe(item, directory=True) for item in parent_stats)
|
|
1586
|
+
if unsafe_parent or not private_receipt_stat_safe(before, directory=False):
|
|
1587
|
+
return None, "previous_receipt_unsafe"
|
|
1588
|
+
if before.st_size > MAX_RECEIPT_BYTES:
|
|
1589
|
+
return None, "previous_receipt_too_large"
|
|
1590
|
+
|
|
1591
|
+
flags = os.O_RDONLY | os.O_NOFOLLOW
|
|
1592
|
+
for name in ("O_CLOEXEC", "O_NONBLOCK", "O_NOCTTY"):
|
|
1593
|
+
flags |= getattr(os, name, 0)
|
|
1594
|
+
try:
|
|
1595
|
+
file_fd = os.open(filename, flags, dir_fd=current_fd)
|
|
1596
|
+
except FileNotFoundError:
|
|
1597
|
+
return None, "previous_receipt_invalid"
|
|
1598
|
+
except (OSError, NotImplementedError):
|
|
1599
|
+
return None, "previous_receipt_unsafe"
|
|
1600
|
+
opened = os.fstat(file_fd)
|
|
1601
|
+
if not private_receipt_stat_safe(opened, directory=False):
|
|
1602
|
+
return None, "previous_receipt_unsafe"
|
|
1603
|
+
if opened.st_size > MAX_RECEIPT_BYTES:
|
|
1604
|
+
return None, "previous_receipt_too_large"
|
|
1605
|
+
chunks: list[bytes] = []
|
|
1606
|
+
observed = 0
|
|
1607
|
+
while observed < MAX_RECEIPT_BYTES + 1:
|
|
1608
|
+
chunk = os.read(file_fd, min(16 * 1024, MAX_RECEIPT_BYTES + 1 - observed))
|
|
1609
|
+
if not chunk:
|
|
1610
|
+
break
|
|
1611
|
+
chunks.append(chunk)
|
|
1612
|
+
observed += len(chunk)
|
|
1613
|
+
raw = b"".join(chunks)
|
|
1614
|
+
after = os.fstat(file_fd)
|
|
1615
|
+
try:
|
|
1616
|
+
path_after = os.stat(filename, dir_fd=current_fd, follow_symlinks=False)
|
|
1617
|
+
except FileNotFoundError:
|
|
1618
|
+
return None, "previous_receipt_invalid"
|
|
1619
|
+
except (OSError, NotImplementedError):
|
|
1620
|
+
return None, "previous_receipt_unsafe"
|
|
1621
|
+
if not private_receipt_stat_safe(after, directory=False) or not private_receipt_stat_safe(path_after, directory=False):
|
|
1622
|
+
return None, "previous_receipt_unsafe"
|
|
1623
|
+
if len(raw) > MAX_RECEIPT_BYTES or after.st_size > MAX_RECEIPT_BYTES or path_after.st_size > MAX_RECEIPT_BYTES:
|
|
1624
|
+
return None, "previous_receipt_too_large"
|
|
1625
|
+
identities = (stat_identity(before), stat_identity(opened), stat_identity(after), stat_identity(path_after))
|
|
1626
|
+
if len(set(identities)) != 1 or len(raw) != after.st_size:
|
|
1627
|
+
return None, "previous_receipt_invalid"
|
|
1628
|
+
except OSError:
|
|
1629
|
+
return None, "previous_receipt_unsafe"
|
|
1630
|
+
finally:
|
|
1631
|
+
if file_fd is not None:
|
|
1632
|
+
try:
|
|
1633
|
+
os.close(file_fd)
|
|
1634
|
+
except OSError:
|
|
1635
|
+
pass
|
|
1636
|
+
if current_fd is not None:
|
|
1637
|
+
try:
|
|
1638
|
+
os.close(current_fd)
|
|
1639
|
+
except OSError:
|
|
1640
|
+
pass
|
|
1641
|
+
|
|
1642
|
+
try:
|
|
1643
|
+
receipt = json.loads(
|
|
1644
|
+
raw.decode("utf-8", errors="strict"),
|
|
1645
|
+
object_pairs_hook=strict_json_object,
|
|
1646
|
+
parse_constant=reject_json_constant,
|
|
1647
|
+
parse_int=parse_receipt_int,
|
|
1648
|
+
)
|
|
1649
|
+
json_depth(receipt)
|
|
1650
|
+
except (UnicodeDecodeError, ValueError, RecursionError):
|
|
1651
|
+
return None, "previous_receipt_invalid"
|
|
1652
|
+
if not isinstance(receipt, dict):
|
|
1653
|
+
return None, "previous_receipt_invalid"
|
|
1654
|
+
prior_id = receipt.get("pack_id")
|
|
1655
|
+
prior_bytes = receipt.get("pack_bytes")
|
|
1656
|
+
pack_present = "pack" in receipt
|
|
1657
|
+
address_present = "content_address" in receipt
|
|
1658
|
+
prior_pack = receipt.get("pack")
|
|
1659
|
+
prior_address = receipt.get("content_address")
|
|
1660
|
+
if not isinstance(prior_id, str) or re.fullmatch(r"[0-9a-f]{20}", prior_id) is None:
|
|
1661
|
+
return None, "previous_receipt_invalid"
|
|
1662
|
+
if not isinstance(prior_bytes, int) or isinstance(prior_bytes, bool) or prior_bytes < 0:
|
|
1663
|
+
return None, "previous_receipt_invalid"
|
|
1664
|
+
if pack_present and not isinstance(prior_pack, str):
|
|
1665
|
+
return None, "previous_receipt_invalid"
|
|
1666
|
+
if address_present and not isinstance(prior_address, dict):
|
|
1667
|
+
return None, "previous_receipt_invalid"
|
|
1668
|
+
if prior_id != requested_id:
|
|
1669
|
+
return None, "previous_pack_integrity_mismatch"
|
|
1670
|
+
if not pack_present:
|
|
1671
|
+
if receipt.get("pack_omitted_from_receipt") is True:
|
|
1672
|
+
return None, "previous_pack_body_unavailable"
|
|
1673
|
+
return None, "previous_receipt_invalid"
|
|
1674
|
+
assert isinstance(prior_pack, str)
|
|
1675
|
+
try:
|
|
1676
|
+
prior_body = prior_pack.encode("utf-8", errors="strict")
|
|
1677
|
+
except UnicodeEncodeError:
|
|
1678
|
+
return None, "previous_receipt_invalid"
|
|
1679
|
+
digest = hashlib.sha256(prior_body).hexdigest()
|
|
1680
|
+
if len(prior_body) != prior_bytes:
|
|
1681
|
+
return None, "previous_pack_integrity_mismatch"
|
|
1682
|
+
if address_present and prior_address != content_address(digest, prior_bytes):
|
|
1683
|
+
return None, "previous_pack_integrity_mismatch"
|
|
1684
|
+
return prior_pack, None
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def rolling_delta_from_receipt(
|
|
1688
|
+
root: Path,
|
|
1689
|
+
current_pack: str,
|
|
1690
|
+
previous_pack_id: str,
|
|
1691
|
+
current_address: str,
|
|
1692
|
+
) -> dict[str, Any]:
|
|
1693
|
+
previous_pack, reason = read_previous_receipt(root, previous_pack_id)
|
|
1694
|
+
if reason is not None or previous_pack is None:
|
|
1695
|
+
return unavailable_rolling_delta(current_pack, previous_pack_id, current_address, reason or "previous_receipt_invalid")
|
|
1696
|
+
return build_rolling_delta(current_pack, previous_pack, previous_pack_id, current_address)
|
|
1697
|
+
|
|
1698
|
+
|
|
1171
1699
|
def store_receipt(root: Path, result: dict[str, Any]) -> dict[str, Any]:
|
|
1172
1700
|
out_dir, dir_fd, dir_error = ensure_private_pack_dir(root)
|
|
1173
1701
|
if out_dir is None or dir_fd is None:
|
|
@@ -1204,9 +1732,19 @@ def store_receipt(root: Path, result: dict[str, Any]) -> dict[str, Any]:
|
|
|
1204
1732
|
}
|
|
1205
1733
|
|
|
1206
1734
|
|
|
1207
|
-
def build_pack(
|
|
1735
|
+
def build_pack(
|
|
1736
|
+
root: Path,
|
|
1737
|
+
specs: list[SourceSpec],
|
|
1738
|
+
*,
|
|
1739
|
+
budget_bytes: int,
|
|
1740
|
+
root_arg: str,
|
|
1741
|
+
store_artifact: bool,
|
|
1742
|
+
delta_from_pack_id: str | None = None,
|
|
1743
|
+
sketch_duplicate_veto: bool = False,
|
|
1744
|
+
) -> dict[str, Any]:
|
|
1208
1745
|
seen: set[tuple[str, str]] = set()
|
|
1209
1746
|
resolved: list[ResolvedSource] = []
|
|
1747
|
+
paired_candidates: list[_PairedCandidate] = []
|
|
1210
1748
|
omitted: list[dict[str, Any]] = []
|
|
1211
1749
|
canonical_specs: list[dict[str, Any]] = []
|
|
1212
1750
|
for spec in specs:
|
|
@@ -1237,8 +1775,20 @@ def build_pack(root: Path, specs: list[SourceSpec], *, budget_bytes: int, root_a
|
|
|
1237
1775
|
continue
|
|
1238
1776
|
assert source is not None
|
|
1239
1777
|
resolved.append(source)
|
|
1240
|
-
|
|
1241
|
-
|
|
1778
|
+
canonical = {"path": source.display_path, "priority": spec.priority, "lines": identity_lines, "status": "candidate"}
|
|
1779
|
+
canonical_specs.append(canonical)
|
|
1780
|
+
paired_candidates.append(_PairedCandidate(source, canonical))
|
|
1781
|
+
paired_candidates.sort(key=lambda item: (-item.source.spec.priority, item.source.spec.input_index, item.source.display_path))
|
|
1782
|
+
all_resolved = resolved
|
|
1783
|
+
comparison_cap_reached = False
|
|
1784
|
+
if sketch_duplicate_veto:
|
|
1785
|
+
resolved, comparison_cap_reached = _apply_sketch_duplicate_veto(
|
|
1786
|
+
paired_candidates,
|
|
1787
|
+
omitted,
|
|
1788
|
+
root_arg=root_arg,
|
|
1789
|
+
)
|
|
1790
|
+
else:
|
|
1791
|
+
resolved = [item.source for item in paired_candidates]
|
|
1242
1792
|
header = "# Context Pack\n\nGenerated by context-guard-pack. Token counts are estimated proxies; byte counts are observed.\n\n"
|
|
1243
1793
|
parts: list[str] = []
|
|
1244
1794
|
included: list[dict[str, Any]] = []
|
|
@@ -1267,7 +1817,8 @@ def build_pack(root: Path, specs: list[SourceSpec], *, budget_bytes: int, root_a
|
|
|
1267
1817
|
omitted.append(budget_omission(source, root_arg=root_arg))
|
|
1268
1818
|
pack = "".join(parts)
|
|
1269
1819
|
pack_bytes = current_pack_bytes
|
|
1270
|
-
|
|
1820
|
+
pack_digest = sha256_text(pack)
|
|
1821
|
+
redacted_lines = sum(source.redacted_lines for source in all_resolved)
|
|
1271
1822
|
partial_count = sum(1 for item in included if item.get("status") == "partial")
|
|
1272
1823
|
omitted_sorted = sorted(omitted, key=lambda item: (item.get("input_index", 0), str(item.get("path", "")), str(item.get("reason", ""))))
|
|
1273
1824
|
canonical = {
|
|
@@ -1275,7 +1826,7 @@ def build_pack(root: Path, specs: list[SourceSpec], *, budget_bytes: int, root_a
|
|
|
1275
1826
|
"root": display_root(root),
|
|
1276
1827
|
"budget_bytes": budget_bytes,
|
|
1277
1828
|
"sources": canonical_specs,
|
|
1278
|
-
"pack_sha256":
|
|
1829
|
+
"pack_sha256": pack_digest,
|
|
1279
1830
|
"omission_summary": sorted({str(item.get("reason")) for item in omitted_sorted}),
|
|
1280
1831
|
}
|
|
1281
1832
|
pack_id = hashlib.sha256(json.dumps(canonical, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()[:20]
|
|
@@ -1287,6 +1838,7 @@ def build_pack(root: Path, specs: list[SourceSpec], *, budget_bytes: int, root_a
|
|
|
1287
1838
|
"budget_bytes": budget_bytes,
|
|
1288
1839
|
"pack_bytes": pack_bytes,
|
|
1289
1840
|
"pack": pack,
|
|
1841
|
+
"content_address": content_address(pack_digest, pack_bytes),
|
|
1290
1842
|
"token_proxy": {"measurement": "estimated", "method": f"chars_div_{TOKEN_PROXY_CHARS_PER_TOKEN}", "pack": token_proxy(pack)},
|
|
1291
1843
|
"sources": {"total": len(specs), "included": len(included) - partial_count, "partial": partial_count, "omitted": len(omitted_sorted)},
|
|
1292
1844
|
"included_sources": included,
|
|
@@ -1295,6 +1847,15 @@ def build_pack(root: Path, specs: list[SourceSpec], *, budget_bytes: int, root_a
|
|
|
1295
1847
|
"artifact": {"stored": False, "path": None, "bytes": 0, "capped": False, "cap_bytes": MAX_RECEIPT_BYTES},
|
|
1296
1848
|
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
1297
1849
|
}
|
|
1850
|
+
if sketch_duplicate_veto:
|
|
1851
|
+
result["sketch_duplicate_veto"] = {"comparison_cap_reached": comparison_cap_reached}
|
|
1852
|
+
if delta_from_pack_id is not None:
|
|
1853
|
+
result["rolling_delta"] = rolling_delta_from_receipt(
|
|
1854
|
+
root,
|
|
1855
|
+
pack,
|
|
1856
|
+
delta_from_pack_id,
|
|
1857
|
+
result["content_address"]["id"],
|
|
1858
|
+
)
|
|
1298
1859
|
if store_artifact:
|
|
1299
1860
|
artifact = store_receipt(root, result)
|
|
1300
1861
|
result["artifact"] = artifact
|
|
@@ -3267,7 +3828,15 @@ def auto_pack(root: Path, args: argparse.Namespace, *, root_arg: str) -> tuple[d
|
|
|
3267
3828
|
manifest = suggest_payload["manifest"]
|
|
3268
3829
|
specs = manifest_to_source_specs(manifest)
|
|
3269
3830
|
budget = bounded_int(args.budget_bytes, DEFAULT_BUDGET_BYTES, MIN_BUDGET_BYTES, MAX_BUDGET_BYTES)
|
|
3270
|
-
build_payload = build_pack(
|
|
3831
|
+
build_payload = build_pack(
|
|
3832
|
+
root,
|
|
3833
|
+
specs,
|
|
3834
|
+
budget_bytes=budget,
|
|
3835
|
+
root_arg=root_arg,
|
|
3836
|
+
store_artifact=False,
|
|
3837
|
+
delta_from_pack_id=args.delta_from_pack_id,
|
|
3838
|
+
sketch_duplicate_veto=getattr(args, "sketch_duplicate_veto", False),
|
|
3839
|
+
)
|
|
3271
3840
|
if not args.no_artifact:
|
|
3272
3841
|
receipt_rel = Path(PACK_DIR) / f"{build_payload['pack_id']}.json"
|
|
3273
3842
|
if manifest_rel is not None:
|
|
@@ -3415,9 +3984,15 @@ def print_suggest_text(payload: dict[str, Any]) -> None:
|
|
|
3415
3984
|
|
|
3416
3985
|
|
|
3417
3986
|
def print_auto_text(payload: dict[str, Any]) -> None:
|
|
3987
|
+
build_payload = payload.get("build", {}) if isinstance(payload.get("build"), dict) else {}
|
|
3988
|
+
sketch_duplicate = build_payload.get("sketch_duplicate_veto")
|
|
3989
|
+
sketch_suffix = ""
|
|
3990
|
+
if isinstance(sketch_duplicate, dict):
|
|
3991
|
+
cap_reached = str(bool(sketch_duplicate.get("comparison_cap_reached"))).lower()
|
|
3992
|
+
sketch_suffix = f" sketch_comparison_cap_reached={cap_reached}"
|
|
3418
3993
|
print(
|
|
3419
3994
|
f"context-guard-pack auto: {payload['sources']['suggested']} suggested source(s), "
|
|
3420
|
-
f"pack {payload['pack_bytes']}/{payload['budget_bytes']} bytes"
|
|
3995
|
+
f"pack {payload['pack_bytes']}/{payload['budget_bytes']} bytes{sketch_suffix}"
|
|
3421
3996
|
)
|
|
3422
3997
|
explain = payload.get("explain")
|
|
3423
3998
|
if isinstance(explain, dict):
|
|
@@ -3477,6 +4052,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3477
4052
|
build.add_argument("--budget-bytes", type=int, default=DEFAULT_BUDGET_BYTES)
|
|
3478
4053
|
build.add_argument("--json", action="store_true", help="emit JSON payload")
|
|
3479
4054
|
build.add_argument("--no-artifact", action="store_true", help="do not write .context-guard/packs receipt")
|
|
4055
|
+
build.add_argument(
|
|
4056
|
+
"--sketch-duplicate-veto",
|
|
4057
|
+
action="store_true",
|
|
4058
|
+
help=(
|
|
4059
|
+
"omit later rank-stable sanitized exact/sketch-set duplicates; use a fixed 100,000 verified-pair cap, "
|
|
4060
|
+
"then fail open; when enabled report sketch_comparison_cap_reached=true|false in text and "
|
|
4061
|
+
"sketch_duplicate_veto.comparison_cap_reached in JSON"
|
|
4062
|
+
),
|
|
4063
|
+
)
|
|
4064
|
+
build.add_argument(
|
|
4065
|
+
"--delta-from-pack-id",
|
|
4066
|
+
type=pack_id_arg,
|
|
4067
|
+
metavar="PACK_ID",
|
|
4068
|
+
help=(
|
|
4069
|
+
"compare against one private local pack receipt using bounded rolling diagnostics; "
|
|
4070
|
+
"visible only in --json output or a stored receipt (--no-artifact requires --json)"
|
|
4071
|
+
),
|
|
4072
|
+
)
|
|
3480
4073
|
slice_cmd = sub.add_parser("slice", help="retrieve an exact sanitized file slice")
|
|
3481
4074
|
slice_cmd.add_argument("--root", default=".", help="project root; must not be a symlink")
|
|
3482
4075
|
slice_cmd.add_argument("--path", required=True, help="relative file path under root")
|
|
@@ -3512,6 +4105,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3512
4105
|
auto.add_argument("--pack-out", help="write the built Markdown pack to this relative path under root")
|
|
3513
4106
|
auto.add_argument("--json", action="store_true", help="emit JSON payload")
|
|
3514
4107
|
auto.add_argument("--no-artifact", action="store_true", help="do not write .context-guard/packs receipt")
|
|
4108
|
+
auto.add_argument(
|
|
4109
|
+
"--sketch-duplicate-veto",
|
|
4110
|
+
action="store_true",
|
|
4111
|
+
help=(
|
|
4112
|
+
"omit later rank-stable sanitized exact/sketch-set duplicates; use a fixed 100,000 verified-pair cap, "
|
|
4113
|
+
"then fail open; when enabled report sketch_comparison_cap_reached=true|false in text and "
|
|
4114
|
+
"sketch_duplicate_veto.comparison_cap_reached in JSON"
|
|
4115
|
+
),
|
|
4116
|
+
)
|
|
4117
|
+
auto.add_argument(
|
|
4118
|
+
"--delta-from-pack-id",
|
|
4119
|
+
type=pack_id_arg,
|
|
4120
|
+
metavar="PACK_ID",
|
|
4121
|
+
help=(
|
|
4122
|
+
"compare against one private local pack receipt using bounded rolling diagnostics; "
|
|
4123
|
+
"visible only in --json output or a stored receipt (--no-artifact requires --json)"
|
|
4124
|
+
),
|
|
4125
|
+
)
|
|
3515
4126
|
auto.add_argument("--explain", action="store_true", help="include deterministic local selection/build explanation metadata")
|
|
3516
4127
|
auto.add_argument("--adaptive-k", action="store_true", help="include local score/budget top-k advisory metadata without changing the manifest or pack")
|
|
3517
4128
|
auto.add_argument("--adaptive-k-policy", choices=ADAPTIVE_K_POLICIES, default="balanced", help="local adaptive-k recommendation policy used when --adaptive-k is set")
|
|
@@ -3531,15 +4142,29 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
3531
4142
|
if not specs:
|
|
3532
4143
|
raise PackError("provide --manifest or --source")
|
|
3533
4144
|
budget = bounded_int(args.budget_bytes, DEFAULT_BUDGET_BYTES, MIN_BUDGET_BYTES, MAX_BUDGET_BYTES)
|
|
3534
|
-
result = build_pack(
|
|
4145
|
+
result = build_pack(
|
|
4146
|
+
root,
|
|
4147
|
+
specs,
|
|
4148
|
+
budget_bytes=budget,
|
|
4149
|
+
root_arg=str(args.root),
|
|
4150
|
+
store_artifact=not args.no_artifact,
|
|
4151
|
+
delta_from_pack_id=args.delta_from_pack_id,
|
|
4152
|
+
sketch_duplicate_veto=args.sketch_duplicate_veto,
|
|
4153
|
+
)
|
|
3535
4154
|
if args.json:
|
|
3536
4155
|
json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
|
|
3537
4156
|
sys.stdout.write("\n")
|
|
3538
4157
|
else:
|
|
3539
4158
|
sys.stdout.write(str(result["pack"]))
|
|
4159
|
+
sketch_suffix = ""
|
|
4160
|
+
sketch_duplicate = result.get("sketch_duplicate_veto")
|
|
4161
|
+
if isinstance(sketch_duplicate, dict):
|
|
4162
|
+
cap_reached = str(bool(sketch_duplicate.get("comparison_cap_reached"))).lower()
|
|
4163
|
+
sketch_suffix = f" sketch_comparison_cap_reached={cap_reached}"
|
|
3540
4164
|
print(
|
|
3541
4165
|
f"[context-guard-pack] pack_id={result['pack_id']} bytes={result['pack_bytes']}/{result['budget_bytes']} "
|
|
3542
|
-
f"included={result['sources']['included']} partial={result['sources']['partial']} omitted={result['sources']['omitted']}"
|
|
4166
|
+
f"included={result['sources']['included']} partial={result['sources']['partial']} omitted={result['sources']['omitted']}"
|
|
4167
|
+
f"{sketch_suffix}",
|
|
3543
4168
|
file=sys.stderr,
|
|
3544
4169
|
)
|
|
3545
4170
|
return 0
|