@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.
@@ -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(root: Path, specs: list[SourceSpec], *, budget_bytes: int, root_arg: str, store_artifact: bool) -> dict[str, Any]:
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
- canonical_specs.append({"path": source.display_path, "priority": spec.priority, "lines": identity_lines, "status": "candidate"})
1241
- resolved.sort(key=lambda item: (-item.spec.priority, item.spec.input_index, item.display_path))
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
- redacted_lines = sum(source.redacted_lines for source in resolved)
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": sha256_text(pack),
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(root, specs, budget_bytes=budget, root_arg=root_arg, store_artifact=False)
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(root, specs, budget_bytes=budget, root_arg=str(args.root), store_artifact=not args.no_artifact)
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