@ictechgy/context-guard 0.4.4 → 0.4.6

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.ko.md +15 -2
  3. package/README.md +15 -4
  4. package/context-guard-kit/README.md +2 -2
  5. package/context-guard-kit/benchmark_runner.py +244 -6
  6. package/context-guard-kit/claude_transcript_cost_audit.py +171 -1
  7. package/context-guard-kit/context_pack.py +82 -35
  8. package/context-guard-kit/cost_guard.py +457 -45
  9. package/docs/benchmark-fixtures/learned-compression-baseline-context-pack.prompt.example.md +19 -0
  10. package/docs/benchmark-fixtures/learned-compression-candidate-digest.prompt.example.md +21 -0
  11. package/docs/benchmark-fixtures/learned-compression.tasks.example.json +5 -1
  12. package/docs/benchmark-fixtures/output-transform-baseline-raw-output.prompt.example.md +20 -0
  13. package/docs/benchmark-fixtures/output-transform-digest-receipt.prompt.example.md +23 -0
  14. package/docs/benchmark-fixtures/output-transform.tasks.example.json +28 -0
  15. package/docs/benchmark-fixtures/output-transform.variants.example.json +10 -0
  16. package/docs/benchmark-fixtures/visual-ocr-cropped-ocr.prompt.example.md +22 -0
  17. package/docs/benchmark-fixtures/visual-ocr-full-visual.prompt.example.md +19 -0
  18. package/docs/benchmark-fixtures/visual-ocr.tasks.example.json +5 -1
  19. package/docs/benchmark-workflow-examples.md +6 -2
  20. package/docs/benchmark-workflows/self-hosted-metrics-ledger.example.jsonl +1 -0
  21. package/docs/experimental-benchmark-fixtures.md +17 -6
  22. package/docs/mac-visibility-feasibility-schema.md +62 -0
  23. package/docs/mac-visibility-feasibility.example.json +130 -0
  24. package/package.json +5 -1
  25. package/packaging/homebrew/context-guard.rb.template +1 -1
  26. package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
  27. package/plugins/context-guard/README.ko.md +1 -1
  28. package/plugins/context-guard/README.md +1 -1
  29. package/plugins/context-guard/bin/context-guard-audit +171 -1
  30. package/plugins/context-guard/bin/context-guard-bench +244 -6
  31. package/plugins/context-guard/bin/context-guard-cost +457 -45
  32. package/plugins/context-guard/bin/context-guard-pack +82 -35
@@ -46,7 +46,8 @@ COST_KEYS = ("total_cost_usd", "cost_usd", "costUSD")
46
46
  MODEL_KEYS = ("model", "model_id", "modelId")
47
47
  QUERY_SOURCE_KEYS = ("query_source", "querySource")
48
48
  TIMESTAMP_KEYS = ("timestamp", "created_at", "createdAt", "time", "ts")
49
- FEASIBILITY_SCHEMA_VERSION = "contextguard.metric-feasibility.v1.2"
49
+ FEASIBILITY_SCHEMA_VERSION = "contextguard.metric-feasibility.v1.3"
50
+ MAC_VISIBILITY_SCHEMA_VERSION = "contextguard.mac-visibility.v1"
50
51
  FEASIBILITY_PRODUCER = "context-guard-audit"
51
52
  CACHE_DIAGNOSTICS_SCHEMA_VERSION = "contextguard.cache-diagnostics.v1"
52
53
  CACHE_LAYOUT_ADVICE_SCHEMA_VERSION = "contextguard.cache-layout-advice.v1"
@@ -1635,6 +1636,168 @@ def build_metric_caveats(summary: UsageSummary) -> list[str]:
1635
1636
  return caveats
1636
1637
 
1637
1638
 
1639
+ def _mac_card(
1640
+ card_id: str,
1641
+ title: str,
1642
+ status: str,
1643
+ binding_paths: list[str],
1644
+ *,
1645
+ required_observation: str | None = None,
1646
+ ) -> dict[str, Any]:
1647
+ card: dict[str, Any] = {
1648
+ "id": card_id,
1649
+ "title": title,
1650
+ "status": status,
1651
+ "binding_paths": binding_paths,
1652
+ }
1653
+ if required_observation:
1654
+ card["required_observation"] = required_observation
1655
+ return card
1656
+
1657
+
1658
+ def build_mac_visibility_contract(
1659
+ *,
1660
+ availability: dict[str, Any],
1661
+ integrity: dict[str, Any],
1662
+ cache_layout_advice: dict[str, Any],
1663
+ ) -> dict[str, Any]:
1664
+ """Build the pre-GUI macOS visibility binding contract.
1665
+
1666
+ This is intentionally a thin index over already-emitted stable feasibility
1667
+ fields. It does not recompute metrics, read diagnostic summary data, or infer
1668
+ live context/headroom from historical transcript totals.
1669
+ """
1670
+ token_status = str((availability.get("tokens") or {}).get("status", "missing"))
1671
+ scan_status = str(integrity.get("status", "partial"))
1672
+ if token_status == "available" and scan_status == "complete":
1673
+ readiness_status = "ready"
1674
+ readiness_reason = "Transcript token totals are available and the scan completed within configured limits."
1675
+ elif token_status in {"available", "partial"}:
1676
+ readiness_status = "partial"
1677
+ readiness_reason = "Some stable fields can be shown, but scan integrity or metric availability is partial."
1678
+ else:
1679
+ readiness_status = "missing"
1680
+ readiness_reason = "Token totals are missing from the transcript scan; show setup or unavailable state."
1681
+
1682
+ context_status = str((availability.get("context") or {}).get("status", "missing"))
1683
+ headroom_status = str((availability.get("headroom") or {}).get("status", "missing"))
1684
+ cache_status = str((availability.get("cache") or {}).get("status", "missing"))
1685
+ cost_status = str((availability.get("cost") or {}).get("status", "missing"))
1686
+ advice_status = str(cache_layout_advice.get("status", "missing"))
1687
+
1688
+ missing_live_observations: list[dict[str, Any]] = []
1689
+ if context_status == "missing":
1690
+ missing_live_observations.append({
1691
+ "id": "live_context_window",
1692
+ "required_observation": "live_statusline_snapshot",
1693
+ "affects": ["context_availability", "metric_availability.context"],
1694
+ "reason": "Historical transcript scans do not include live Claude Code context_window data.",
1695
+ })
1696
+ if headroom_status == "missing":
1697
+ missing_live_observations.append({
1698
+ "id": "live_headroom",
1699
+ "required_observation": "live_statusline_snapshot",
1700
+ "affects": ["headroom_availability", "cache_diagnostics.headroom_diagnostics"],
1701
+ "reason": "Historical transcript totals are not remaining-token or live headroom observations.",
1702
+ })
1703
+
1704
+ return {
1705
+ "schema_version": MAC_VISIBILITY_SCHEMA_VERSION,
1706
+ "surface_kind": "local_macos_visibility_contract",
1707
+ "readiness": {
1708
+ "status": readiness_status,
1709
+ "reason": readiness_reason,
1710
+ },
1711
+ "bind_to_top_level_fields": [
1712
+ "source_kind",
1713
+ "source_freshness",
1714
+ "scan_integrity",
1715
+ "metric_availability",
1716
+ "metric_caveats",
1717
+ "redaction_mode",
1718
+ "context_availability",
1719
+ "headroom_availability",
1720
+ "cache_friendliness",
1721
+ "cache_diagnostics",
1722
+ "cache_layout_advice",
1723
+ "totals",
1724
+ ],
1725
+ "diagnostic_only_fields": ["summary"],
1726
+ "primary_cards": [
1727
+ _mac_card(
1728
+ "source_freshness",
1729
+ "Source freshness",
1730
+ "available",
1731
+ ["source_kind", "source_freshness.status", "source_freshness.generated_at"],
1732
+ ),
1733
+ _mac_card(
1734
+ "scan_integrity",
1735
+ "Scan integrity",
1736
+ scan_status,
1737
+ [
1738
+ "scan_integrity.status",
1739
+ "scan_integrity.files_scanned",
1740
+ "scan_integrity.records_scanned",
1741
+ "scan_integrity.skipped_files",
1742
+ "scan_integrity.skipped_records",
1743
+ ],
1744
+ ),
1745
+ _mac_card(
1746
+ "token_totals",
1747
+ "Token totals",
1748
+ token_status,
1749
+ [
1750
+ "totals.total_tokens",
1751
+ "totals.tokens.input",
1752
+ "totals.tokens.output",
1753
+ "totals.tokens.cache_read",
1754
+ "totals.tokens.cache_creation",
1755
+ ],
1756
+ ),
1757
+ _mac_card(
1758
+ "cache_reuse",
1759
+ "Cache-read share and reuse ratio",
1760
+ cache_status,
1761
+ ["totals.cache_read_share", "totals.cache_reuse_ratio", "metric_availability.cache"],
1762
+ ),
1763
+ _mac_card(
1764
+ "observed_cost",
1765
+ "Observed transcript cost",
1766
+ cost_status,
1767
+ ["totals.cost_usd_observed", "metric_availability.cost"],
1768
+ ),
1769
+ _mac_card(
1770
+ "context_availability",
1771
+ "Context availability",
1772
+ context_status,
1773
+ ["context_availability", "metric_availability.context"],
1774
+ required_observation="live_statusline_snapshot" if context_status == "missing" else None,
1775
+ ),
1776
+ _mac_card(
1777
+ "headroom_availability",
1778
+ "Headroom availability",
1779
+ headroom_status,
1780
+ ["headroom_availability", "cache_diagnostics.headroom_diagnostics"],
1781
+ required_observation="live_statusline_snapshot" if headroom_status == "missing" else None,
1782
+ ),
1783
+ _mac_card(
1784
+ "cache_layout_advice",
1785
+ "Cache layout advice",
1786
+ advice_status,
1787
+ ["cache_layout_advice", "cache_friendliness", "cache_diagnostics.dynamic_prefix_breakers"],
1788
+ ),
1789
+ ],
1790
+ "missing_live_observations": missing_live_observations,
1791
+ "claim_boundaries": [
1792
+ "Local transcript observations are not invoice-grade billing records.",
1793
+ "Provider cache fields are telemetry, not ContextGuard-caused token reduction and do not prove provider cache hits.",
1794
+ "Historical transcript totals do not infer live context headroom or remaining tokens.",
1795
+ "This contract does not guarantee token or cost savings.",
1796
+ ],
1797
+ "redaction_required": True,
1798
+ }
1799
+
1800
+
1638
1801
  def feasibility_json(
1639
1802
  summary: UsageSummary,
1640
1803
  top: int = 15,
@@ -1652,6 +1815,11 @@ def feasibility_json(
1652
1815
  cache_friendliness = cache_friendliness_for_summary(summary)
1653
1816
  cache_diagnostics = cache_diagnostics_for_summary(summary)
1654
1817
  cache_layout_advice = cache_layout_advice_for_summary(summary)
1818
+ mac_visibility = build_mac_visibility_contract(
1819
+ availability=availability,
1820
+ integrity=integrity,
1821
+ cache_layout_advice=cache_layout_advice,
1822
+ )
1655
1823
  return {
1656
1824
  "schema_version": FEASIBILITY_SCHEMA_VERSION,
1657
1825
  "producer": FEASIBILITY_PRODUCER,
@@ -1672,6 +1840,7 @@ def feasibility_json(
1672
1840
  "cache_friendliness",
1673
1841
  "cache_diagnostics",
1674
1842
  "cache_layout_advice",
1843
+ "mac_visibility",
1675
1844
  "totals",
1676
1845
  ],
1677
1846
  "diagnostic_fields": ["summary"],
@@ -1701,6 +1870,7 @@ def feasibility_json(
1701
1870
  "cache_friendliness": cache_friendliness,
1702
1871
  "cache_diagnostics": cache_diagnostics,
1703
1872
  "cache_layout_advice": cache_layout_advice,
1873
+ "mac_visibility": mac_visibility,
1704
1874
  "totals": {
1705
1875
  "total_tokens": stable_total_tokens,
1706
1876
  "tokens": stable_tokens,
@@ -729,29 +729,95 @@ def ensure_private_pack_dir(root: Path) -> tuple[Path | None, int | None, str |
729
729
  pass
730
730
 
731
731
 
732
- def write_private_json_at(dir_fd: int, filename: str, data: dict[str, Any]) -> None:
732
+ def atomic_write_ops_supported() -> bool:
733
+ return (
734
+ os.open in os.supports_dir_fd
735
+ and os.rename in os.supports_dir_fd
736
+ and os.unlink in os.supports_dir_fd
737
+ )
738
+
739
+
740
+ def fsync_dir_fd(dir_fd: int) -> None:
741
+ os.fsync(dir_fd)
742
+
743
+
744
+ def validate_existing_output_target_at(dir_fd: int, filename: str, option_name: str) -> None:
745
+ flags = os.O_WRONLY
746
+ if hasattr(os, "O_NOFOLLOW"):
747
+ flags |= os.O_NOFOLLOW
748
+ if hasattr(os, "O_CLOEXEC"):
749
+ flags |= os.O_CLOEXEC
750
+ if hasattr(os, "O_NONBLOCK"):
751
+ flags |= os.O_NONBLOCK
752
+ file_fd = -1
753
+ try:
754
+ file_fd = os.open(filename, flags, dir_fd=dir_fd)
755
+ st = os.fstat(file_fd)
756
+ if not stat.S_ISREG(st.st_mode):
757
+ raise PackError(f"invalid {option_name}: unsafe_path")
758
+ except FileNotFoundError:
759
+ return
760
+ except IsADirectoryError as exc:
761
+ raise PackError(f"invalid {option_name}: unsafe_path") from exc
762
+ except OSError as exc:
763
+ raise PackError(f"invalid {option_name}: {exc.strerror or exc.__class__.__name__}") from exc
764
+ finally:
765
+ if file_fd >= 0:
766
+ try:
767
+ os.close(file_fd)
768
+ except OSError:
769
+ pass
770
+
771
+
772
+ def write_text_atomic_at(dir_fd: int, filename: str, content: str, *, mode: int, option_name: str) -> None:
733
773
  if "/" in filename or filename in {"", ".", ".."}:
734
- raise PackError("unsafe_artifact_path")
735
- flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
774
+ raise PackError(f"invalid {option_name}: unsafe_path")
775
+ if not atomic_write_ops_supported():
776
+ raise PackError(f"invalid {option_name}: atomic_write_unsupported")
777
+ validate_existing_output_target_at(dir_fd, filename, option_name)
778
+ digest = hashlib.sha256(f"{filename}:{os.getpid()}:{time.time_ns()}".encode("utf-8", "replace")).hexdigest()[:16]
779
+ temp_name = f".context-guard-pack-{digest}.tmp"
780
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
736
781
  if hasattr(os, "O_NOFOLLOW"):
737
782
  flags |= os.O_NOFOLLOW
738
783
  if hasattr(os, "O_CLOEXEC"):
739
784
  flags |= os.O_CLOEXEC
740
- fd = os.open(filename, flags, 0o600, dir_fd=dir_fd)
785
+ fd = -1
786
+ temp_created = False
741
787
  try:
742
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
743
- json.dump(data, handle, ensure_ascii=False, indent=2, sort_keys=True)
744
- handle.write("\n")
745
- except Exception:
788
+ fd = os.open(temp_name, flags, mode, dir_fd=dir_fd)
789
+ temp_created = True
790
+ with os.fdopen(fd, "w", encoding="utf-8", newline="") as handle:
791
+ fd = -1
792
+ handle.write(content)
793
+ handle.flush()
794
+ os.fsync(handle.fileno())
795
+ fsync_dir_fd(dir_fd)
796
+ os.rename(temp_name, filename, src_dir_fd=dir_fd, dst_dir_fd=dir_fd)
797
+ temp_created = False
746
798
  try:
747
- os.close(fd)
748
- except OSError:
799
+ os.chmod(filename, mode, dir_fd=dir_fd, follow_symlinks=False)
800
+ except (OSError, TypeError, NotImplementedError):
749
801
  pass
750
- raise
751
- try:
752
- os.chmod(filename, 0o600, dir_fd=dir_fd, follow_symlinks=False)
753
- except (OSError, TypeError, NotImplementedError):
754
- pass
802
+ fsync_dir_fd(dir_fd)
803
+ finally:
804
+ if fd >= 0:
805
+ try:
806
+ os.close(fd)
807
+ except OSError:
808
+ pass
809
+ if temp_created:
810
+ try:
811
+ os.unlink(temp_name, dir_fd=dir_fd)
812
+ except OSError:
813
+ pass
814
+
815
+
816
+ def write_private_json_at(dir_fd: int, filename: str, data: dict[str, Any]) -> None:
817
+ if "/" in filename or filename in {"", ".", ".."}:
818
+ raise PackError("unsafe_artifact_path")
819
+ content = json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
820
+ write_text_atomic_at(dir_fd, filename, content, mode=0o600, option_name="artifact receipt")
755
821
 
756
822
 
757
823
  def finalize_receipt_size(receipt: dict[str, Any]) -> int:
@@ -1453,27 +1519,13 @@ def write_text_under_root(root: Path, raw_path: str, content: str, option_name:
1453
1519
  parent_parts = rel.parts[:-1]
1454
1520
  filename = rel.parts[-1]
1455
1521
  current_fd: int | None = None
1456
- file_fd = -1
1457
1522
  try:
1458
1523
  current_fd = open_dir_no_follow(root)
1459
1524
  for part in parent_parts:
1460
1525
  next_fd = open_dir_no_follow(part, dir_fd=current_fd)
1461
1526
  os.close(current_fd)
1462
1527
  current_fd = next_fd
1463
- flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
1464
- if hasattr(os, "O_NOFOLLOW"):
1465
- flags |= os.O_NOFOLLOW
1466
- if hasattr(os, "O_CLOEXEC"):
1467
- flags |= os.O_CLOEXEC
1468
- if hasattr(os, "O_NONBLOCK"):
1469
- flags |= os.O_NONBLOCK
1470
- file_fd = os.open(filename, flags, 0o600, dir_fd=current_fd)
1471
- st = os.fstat(file_fd)
1472
- if not stat.S_ISREG(st.st_mode):
1473
- raise PackError(f"invalid {option_name}: unsafe_path")
1474
- with os.fdopen(file_fd, "w", encoding="utf-8") as handle:
1475
- file_fd = -1
1476
- handle.write(content)
1528
+ write_text_atomic_at(current_fd, filename, content, mode=0o600, option_name=option_name)
1477
1529
  except PackError:
1478
1530
  raise
1479
1531
  except FileNotFoundError as exc:
@@ -1481,11 +1533,6 @@ def write_text_under_root(root: Path, raw_path: str, content: str, option_name:
1481
1533
  except OSError as exc:
1482
1534
  raise PackError(f"invalid {option_name}: {exc.strerror or exc.__class__.__name__}") from exc
1483
1535
  finally:
1484
- if file_fd >= 0:
1485
- try:
1486
- os.close(file_fd)
1487
- except OSError:
1488
- pass
1489
1536
  if current_fd is not None:
1490
1537
  try:
1491
1538
  os.close(current_fd)