macverify 1.0.0__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.
Files changed (49) hide show
  1. macverify-1.0.0/LICENSE +21 -0
  2. macverify-1.0.0/PKG-INFO +198 -0
  3. macverify-1.0.0/README.md +149 -0
  4. macverify-1.0.0/macverify/__init__.py +39 -0
  5. macverify-1.0.0/macverify/__main__.py +4 -0
  6. macverify-1.0.0/macverify/aicommon.py +112 -0
  7. macverify-1.0.0/macverify/cli.py +231 -0
  8. macverify-1.0.0/macverify/collectors/__init__.py +0 -0
  9. macverify-1.0.0/macverify/collectors/ai_assistants.py +208 -0
  10. macverify-1.0.0/macverify/collectors/claude_code.py +926 -0
  11. macverify-1.0.0/macverify/collectors/containers.py +154 -0
  12. macverify-1.0.0/macverify/collectors/github_copilot.py +343 -0
  13. macverify-1.0.0/macverify/collectors/hardware.py +309 -0
  14. macverify-1.0.0/macverify/collectors/identity.py +270 -0
  15. macverify-1.0.0/macverify/collectors/network.py +298 -0
  16. macverify-1.0.0/macverify/collectors/openai_codex.py +411 -0
  17. macverify-1.0.0/macverify/collectors/packages.py +305 -0
  18. macverify-1.0.0/macverify/collectors/permissions.py +163 -0
  19. macverify-1.0.0/macverify/collectors/secrets.py +302 -0
  20. macverify-1.0.0/macverify/collectors/security.py +284 -0
  21. macverify-1.0.0/macverify/collectors/services.py +181 -0
  22. macverify-1.0.0/macverify/collectors/shell_env.py +354 -0
  23. macverify-1.0.0/macverify/collectors/storage.py +263 -0
  24. macverify-1.0.0/macverify/collectors/toolchain.py +474 -0
  25. macverify-1.0.0/macverify/compat.py +201 -0
  26. macverify-1.0.0/macverify/context.py +19 -0
  27. macverify-1.0.0/macverify/findings.py +38 -0
  28. macverify-1.0.0/macverify/fsutil.py +216 -0
  29. macverify-1.0.0/macverify/i18n.py +244 -0
  30. macverify-1.0.0/macverify/quickfix.py +280 -0
  31. macverify-1.0.0/macverify/registry.py +40 -0
  32. macverify-1.0.0/macverify/report_html.py +1422 -0
  33. macverify-1.0.0/macverify/report_md.py +175 -0
  34. macverify-1.0.0/macverify/runner.py +52 -0
  35. macverify-1.0.0/macverify/scope.py +150 -0
  36. macverify-1.0.0/macverify/shell.py +136 -0
  37. macverify-1.0.0/macverify/sysinfo.py +128 -0
  38. macverify-1.0.0/macverify.egg-info/PKG-INFO +198 -0
  39. macverify-1.0.0/macverify.egg-info/SOURCES.txt +47 -0
  40. macverify-1.0.0/macverify.egg-info/dependency_links.txt +1 -0
  41. macverify-1.0.0/macverify.egg-info/entry_points.txt +2 -0
  42. macverify-1.0.0/macverify.egg-info/top_level.txt +1 -0
  43. macverify-1.0.0/pyproject.toml +43 -0
  44. macverify-1.0.0/setup.cfg +4 -0
  45. macverify-1.0.0/tests/test_cli.py +153 -0
  46. macverify-1.0.0/tests/test_degradation.py +94 -0
  47. macverify-1.0.0/tests/test_guarantees.py +267 -0
  48. macverify-1.0.0/tests/test_quickfix.py +151 -0
  49. macverify-1.0.0/tests/test_redaction.py +137 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shane Dixon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: macverify
3
+ Version: 1.0.0
4
+ Summary: Read-only, offline inventory of a macOS user space and its AI assistant configuration
5
+ Author: Shane Dixon
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Shane Dixon
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: Homepage, https://github.com/ShanedixonGit/macverify
28
+ Project-URL: Source, https://github.com/ShanedixonGit/macverify
29
+ Project-URL: Issues, https://github.com/ShanedixonGit/macverify/issues
30
+ Keywords: macos,audit,inventory,read-only,offline,claude-code,copilot,codex
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Environment :: Console
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: Intended Audience :: System Administrators
35
+ Classifier: Operating System :: MacOS :: MacOS X
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3 :: Only
38
+ Classifier: Programming Language :: Python :: 3.9
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Topic :: System :: Systems Administration
44
+ Classifier: Topic :: Utilities
45
+ Requires-Python: >=3.9
46
+ Description-Content-Type: text/markdown
47
+ License-File: LICENSE
48
+ Dynamic: license-file
49
+
50
+ <picture>
51
+ <source media="(prefers-color-scheme: dark)" srcset=".github/brand/wordmark-dark.svg">
52
+ <img alt="macverify" src=".github/brand/wordmark-light.svg" width="340">
53
+ </picture>
54
+
55
+ [![CI](https://github.com/ShanedixonGit/macverify/actions/workflows/ci.yml/badge.svg)](https://github.com/ShanedixonGit/macverify/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
56
+
57
+ > **macverify 1.0.0** — macOS 11+, Python 3.9+, zero dependencies, fully offline.
58
+
59
+ A read-only, offline inventory of a Mac: what is installed, what runs at login,
60
+ what listens on the network, which protections are on, where credential-shaped
61
+ values sit in plain files, and how your AI assistants (Claude Code, GitHub
62
+ Copilot, OpenAI Codex) are configured and what they cost to load. It writes an
63
+ HTML report, a JSON dataset and a list of remediation commands, then stops.
64
+
65
+ ## What this is NOT
66
+
67
+ **macverify is not antivirus. It is not malware detection. It is not real-time
68
+ protection. It does not scan for known threats.** It has no signature database,
69
+ computes no file hashes, consults no reputation service, and makes no network
70
+ request, so nothing it sees is ever compared against a threat feed.
71
+
72
+ It takes a point-in-time, read-only inventory of your toolchain, config hygiene
73
+ and AI-assistant setup, and flags things worth reviewing yourself. A launchd job
74
+ that exists, is signed and starts at login looks the same from here whether it
75
+ is a printer helper or a backdoor.
76
+
77
+ Run a real scanner alongside it: [Malwarebytes](https://www.malwarebytes.com/mac),
78
+ [KnockKnock](https://objective-see.org/products/knockknock.html) or
79
+ [LuLu](https://objective-see.org/products/lulu.html). Scan first — removing
80
+ adware changes what this report is built from.
81
+
82
+ ## Installation
83
+
84
+ ```sh
85
+ pipx install macverify
86
+ ```
87
+
88
+ Or with pip, if you do not use pipx:
89
+
90
+ ```sh
91
+ pip install --user macverify
92
+ ```
93
+
94
+ No build step, no dependencies. To install nothing at all, clone and run in
95
+ place:
96
+
97
+ ```sh
98
+ git clone https://github.com/ShanedixonGit/macverify.git
99
+ cd macverify
100
+ python3 -m macverify
101
+ ```
102
+
103
+ ### Verify it before you run it
104
+
105
+ This tool reads your machine, so do not take its word for it. From a clone,
106
+ these must all return nothing:
107
+
108
+ ```sh
109
+ grep -rn "shell=True\|os\.system\|os\.popen" --include='*.py' .
110
+ grep -rnE "\b(eval|exec)[[:space:]]*\(|pickle" --include='*.py' .
111
+ grep -rn "urllib\|requests\.\|http\.client\|urlopen\|httpx" --include='*.py' .
112
+ ```
113
+
114
+ No shell execution, no dynamic evaluation, no network client. The same claims
115
+ are enforced as tests, which CI re-runs on every push:
116
+
117
+ ```sh
118
+ /usr/bin/python3 -m compileall -q macverify
119
+ /usr/bin/python3 -m unittest discover -s tests -t .
120
+ ```
121
+
122
+ [SECURITY.md](SECURITY.md) lists each claim and how it is checked.
123
+
124
+ ## Usage
125
+
126
+ ```sh
127
+ macverify --check # what this Mac can be audited for, then exit
128
+ macverify # the full run
129
+ ```
130
+
131
+ `--check` collects nothing, and is the safe way to see what a run would cover.
132
+ A full run takes 30-60 seconds, writing to `~/.macverify/reports` (`0700`):
133
+
134
+ ```
135
+ -h, --help show this help message and exit
136
+ --version print the installed version and exit
137
+ --only DOMAIN run only this domain (repeatable)
138
+ --skip DOMAIN skip this domain (repeatable)
139
+ --json-only write the JSON dataset only
140
+ --html-only write the HTML report only
141
+ --out DIR output directory (default: ~/.macverify/reports)
142
+ --timeout S per-command timeout in seconds (default: 8)
143
+ --lang {en,es} report label language (default: en)
144
+ --project PATH extra project root to inspect for AI assistant config
145
+ (repeatable)
146
+ --verbose print per-domain progress to stderr
147
+ --list-domains list domain names and exit
148
+ --quick-fixes print the quick-fix plan to stdout as well as writing the
149
+ reports
150
+ --check report what this machine can be audited for, then exit
151
+ without collecting
152
+ ```
153
+
154
+ ```sh
155
+ macverify --quick-fixes
156
+ macverify --only security --only network
157
+ ```
158
+
159
+ A run writes `audit_<timestamp>.html` (self-contained), `audit_<timestamp>.json`
160
+ (full dataset), `remediation.md` and `ai_assistant_findings.json`. See
161
+ [examples/](examples/) for the JSON shape and how to pull findings out of it.
162
+
163
+ ## What it reads, and what it never does
164
+
165
+ Sixteen domains: `toolchain`, `packages`, `shell_env`, `hardware`, `storage`,
166
+ `services`, `containers`, `network`, `security`, `identity`, `secrets`,
167
+ `permissions`, `claude_code`, `github_copilot`, `openai_codex`, `ai_assistants`.
168
+ Missing tools are reported as unavailable, never as an error.
169
+
170
+ It never makes a network request. It never calls `sudo` — `sudo`, `su`, `doas`,
171
+ `sudoedit`, `security` and `systemsetup` are refused by its command runner, so
172
+ no code path can elevate. It never modifies system state; fixes are printed,
173
+ never executed. It never captures a secret value: the secrets domain records the
174
+ file, line, detector and a masked prefix, never the value. It never reads AI
175
+ session transcripts, only their file metadata.
176
+
177
+ Reports are sensitive: an `audit_*.json` names your host, user, applications,
178
+ SSH key paths, privacy grants and anything credential-shaped. Read one before
179
+ you send it anywhere.
180
+
181
+ ## Let an AI assistant triage the report
182
+
183
+ [`SKILL.md`](SKILL.md) is a self-contained skill that runs the audit, reads the
184
+ dataset and returns a short ranked list of fixes rather than eighty raw
185
+ findings. For Claude Code, copy it to `~/.claude/skills/macverify-review/`; for
186
+ Codex, append it to your `AGENTS.md`. It never runs a destructive command
187
+ without asking, and never runs `sudo` on your behalf.
188
+
189
+ ## Requirements
190
+
191
+ macOS 11+ and Python 3.9+; every Mac since macOS 12.3 ships a suitable
192
+ `python3`. No third-party packages, at install time or runtime.
193
+
194
+ ## License
195
+
196
+ MIT. See [LICENSE](LICENSE). Also [CONTRIBUTING.md](CONTRIBUTING.md),
197
+ [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md) and
198
+ [design_notes.md](design_notes.md).
@@ -0,0 +1,149 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset=".github/brand/wordmark-dark.svg">
3
+ <img alt="macverify" src=".github/brand/wordmark-light.svg" width="340">
4
+ </picture>
5
+
6
+ [![CI](https://github.com/ShanedixonGit/macverify/actions/workflows/ci.yml/badge.svg)](https://github.com/ShanedixonGit/macverify/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ > **macverify 1.0.0** — macOS 11+, Python 3.9+, zero dependencies, fully offline.
9
+
10
+ A read-only, offline inventory of a Mac: what is installed, what runs at login,
11
+ what listens on the network, which protections are on, where credential-shaped
12
+ values sit in plain files, and how your AI assistants (Claude Code, GitHub
13
+ Copilot, OpenAI Codex) are configured and what they cost to load. It writes an
14
+ HTML report, a JSON dataset and a list of remediation commands, then stops.
15
+
16
+ ## What this is NOT
17
+
18
+ **macverify is not antivirus. It is not malware detection. It is not real-time
19
+ protection. It does not scan for known threats.** It has no signature database,
20
+ computes no file hashes, consults no reputation service, and makes no network
21
+ request, so nothing it sees is ever compared against a threat feed.
22
+
23
+ It takes a point-in-time, read-only inventory of your toolchain, config hygiene
24
+ and AI-assistant setup, and flags things worth reviewing yourself. A launchd job
25
+ that exists, is signed and starts at login looks the same from here whether it
26
+ is a printer helper or a backdoor.
27
+
28
+ Run a real scanner alongside it: [Malwarebytes](https://www.malwarebytes.com/mac),
29
+ [KnockKnock](https://objective-see.org/products/knockknock.html) or
30
+ [LuLu](https://objective-see.org/products/lulu.html). Scan first — removing
31
+ adware changes what this report is built from.
32
+
33
+ ## Installation
34
+
35
+ ```sh
36
+ pipx install macverify
37
+ ```
38
+
39
+ Or with pip, if you do not use pipx:
40
+
41
+ ```sh
42
+ pip install --user macverify
43
+ ```
44
+
45
+ No build step, no dependencies. To install nothing at all, clone and run in
46
+ place:
47
+
48
+ ```sh
49
+ git clone https://github.com/ShanedixonGit/macverify.git
50
+ cd macverify
51
+ python3 -m macverify
52
+ ```
53
+
54
+ ### Verify it before you run it
55
+
56
+ This tool reads your machine, so do not take its word for it. From a clone,
57
+ these must all return nothing:
58
+
59
+ ```sh
60
+ grep -rn "shell=True\|os\.system\|os\.popen" --include='*.py' .
61
+ grep -rnE "\b(eval|exec)[[:space:]]*\(|pickle" --include='*.py' .
62
+ grep -rn "urllib\|requests\.\|http\.client\|urlopen\|httpx" --include='*.py' .
63
+ ```
64
+
65
+ No shell execution, no dynamic evaluation, no network client. The same claims
66
+ are enforced as tests, which CI re-runs on every push:
67
+
68
+ ```sh
69
+ /usr/bin/python3 -m compileall -q macverify
70
+ /usr/bin/python3 -m unittest discover -s tests -t .
71
+ ```
72
+
73
+ [SECURITY.md](SECURITY.md) lists each claim and how it is checked.
74
+
75
+ ## Usage
76
+
77
+ ```sh
78
+ macverify --check # what this Mac can be audited for, then exit
79
+ macverify # the full run
80
+ ```
81
+
82
+ `--check` collects nothing, and is the safe way to see what a run would cover.
83
+ A full run takes 30-60 seconds, writing to `~/.macverify/reports` (`0700`):
84
+
85
+ ```
86
+ -h, --help show this help message and exit
87
+ --version print the installed version and exit
88
+ --only DOMAIN run only this domain (repeatable)
89
+ --skip DOMAIN skip this domain (repeatable)
90
+ --json-only write the JSON dataset only
91
+ --html-only write the HTML report only
92
+ --out DIR output directory (default: ~/.macverify/reports)
93
+ --timeout S per-command timeout in seconds (default: 8)
94
+ --lang {en,es} report label language (default: en)
95
+ --project PATH extra project root to inspect for AI assistant config
96
+ (repeatable)
97
+ --verbose print per-domain progress to stderr
98
+ --list-domains list domain names and exit
99
+ --quick-fixes print the quick-fix plan to stdout as well as writing the
100
+ reports
101
+ --check report what this machine can be audited for, then exit
102
+ without collecting
103
+ ```
104
+
105
+ ```sh
106
+ macverify --quick-fixes
107
+ macverify --only security --only network
108
+ ```
109
+
110
+ A run writes `audit_<timestamp>.html` (self-contained), `audit_<timestamp>.json`
111
+ (full dataset), `remediation.md` and `ai_assistant_findings.json`. See
112
+ [examples/](examples/) for the JSON shape and how to pull findings out of it.
113
+
114
+ ## What it reads, and what it never does
115
+
116
+ Sixteen domains: `toolchain`, `packages`, `shell_env`, `hardware`, `storage`,
117
+ `services`, `containers`, `network`, `security`, `identity`, `secrets`,
118
+ `permissions`, `claude_code`, `github_copilot`, `openai_codex`, `ai_assistants`.
119
+ Missing tools are reported as unavailable, never as an error.
120
+
121
+ It never makes a network request. It never calls `sudo` — `sudo`, `su`, `doas`,
122
+ `sudoedit`, `security` and `systemsetup` are refused by its command runner, so
123
+ no code path can elevate. It never modifies system state; fixes are printed,
124
+ never executed. It never captures a secret value: the secrets domain records the
125
+ file, line, detector and a masked prefix, never the value. It never reads AI
126
+ session transcripts, only their file metadata.
127
+
128
+ Reports are sensitive: an `audit_*.json` names your host, user, applications,
129
+ SSH key paths, privacy grants and anything credential-shaped. Read one before
130
+ you send it anywhere.
131
+
132
+ ## Let an AI assistant triage the report
133
+
134
+ [`SKILL.md`](SKILL.md) is a self-contained skill that runs the audit, reads the
135
+ dataset and returns a short ranked list of fixes rather than eighty raw
136
+ findings. For Claude Code, copy it to `~/.claude/skills/macverify-review/`; for
137
+ Codex, append it to your `AGENTS.md`. It never runs a destructive command
138
+ without asking, and never runs `sudo` on your behalf.
139
+
140
+ ## Requirements
141
+
142
+ macOS 11+ and Python 3.9+; every Mac since macOS 12.3 ships a suitable
143
+ `python3`. No third-party packages, at install time or runtime.
144
+
145
+ ## License
146
+
147
+ MIT. See [LICENSE](LICENSE). Also [CONTRIBUTING.md](CONTRIBUTING.md),
148
+ [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md) and
149
+ [design_notes.md](design_notes.md).
@@ -0,0 +1,39 @@
1
+ import os
2
+ import re
3
+
4
+ DISTRIBUTION = "macverify"
5
+
6
+ UNKNOWN_VERSION = "0+unknown"
7
+
8
+
9
+ def _installed_version():
10
+ try:
11
+ from importlib.metadata import PackageNotFoundError, version
12
+ except ImportError:
13
+ return None
14
+ try:
15
+ return version(DISTRIBUTION)
16
+ except PackageNotFoundError:
17
+ return None
18
+ except Exception:
19
+ return None
20
+
21
+
22
+ def _source_version():
23
+ path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "pyproject.toml")
24
+ try:
25
+ with open(path, "r", encoding="utf-8") as handle:
26
+ for line in handle:
27
+ match = re.match(r'^\s*version\s*=\s*["\']([^"\']+)["\']\s*$', line)
28
+ if match:
29
+ return match.group(1)
30
+ except (OSError, ValueError):
31
+ return None
32
+ return None
33
+
34
+
35
+ def _resolve_version():
36
+ return _installed_version() or _source_version() or UNKNOWN_VERSION
37
+
38
+
39
+ __version__ = _resolve_version()
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,112 @@
1
+ import os
2
+ import re
3
+
4
+ from . import fsutil
5
+
6
+ STOPWORDS = {
7
+ "the", "a", "an", "and", "or", "for", "with", "when", "use", "using", "used", "this", "that", "to", "of", "in",
8
+ "on", "by", "is", "are", "be", "it", "its", "as", "at", "from", "into", "you", "your", "should", "can", "will",
9
+ "any", "all", "not", "but", "if", "then", "than", "also", "via", "per", "up", "out", "over", "before", "after",
10
+ "skill", "skills", "agent", "agents", "command", "commands", "claude", "code", "user", "users", "need", "needs",
11
+ "have", "has", "was", "were", "there", "their", "them", "they", "what", "which", "who", "how", "why", "more",
12
+ }
13
+
14
+ NOT_APPLICABLE = "n/a"
15
+
16
+ MAX_ANCESTOR_DEPTH = 12
17
+
18
+
19
+ def tokens(text):
20
+ words = re.findall(r"[a-z][a-z0-9_-]{2,}", (text or "").lower())
21
+ return {word for word in words if word not in STOPWORDS}
22
+
23
+
24
+ def similarity(left, right):
25
+ if not left or not right:
26
+ return 0.0, []
27
+ shared = left & right
28
+ union = left | right
29
+ if not union:
30
+ return 0.0, []
31
+ return round(len(shared) / float(len(union)), 3), sorted(shared)
32
+
33
+
34
+ def cost(always_bytes, on_demand_bytes):
35
+ return {
36
+ "always_loaded_bytes": always_bytes,
37
+ "always_loaded_tokens_estimate": always_bytes // 4,
38
+ "on_demand_bytes": on_demand_bytes,
39
+ "on_demand_tokens_estimate": on_demand_bytes // 4,
40
+ }
41
+
42
+
43
+ def no_cost(reason):
44
+ return {
45
+ "always_loaded_bytes": None,
46
+ "always_loaded_tokens_estimate": NOT_APPLICABLE,
47
+ "on_demand_bytes": None,
48
+ "on_demand_tokens_estimate": NOT_APPLICABLE,
49
+ "measurement_note": reason,
50
+ }
51
+
52
+
53
+ def headings(body, limit=40):
54
+ outline = []
55
+ for line in (body or "").splitlines():
56
+ match = re.match(r"^(#{1,6})\s+(.+?)\s*#*$", line)
57
+ if match:
58
+ outline.append({"level": len(match.group(1)), "text": match.group(2)[:120]})
59
+ if len(outline) >= limit:
60
+ break
61
+ return outline
62
+
63
+
64
+ def ancestors(start, home, include_home=True):
65
+ chain = []
66
+ current = os.path.abspath(start)
67
+ while True:
68
+ chain.append(current)
69
+ parent = os.path.dirname(current)
70
+ if parent == current or len(chain) >= MAX_ANCESTOR_DEPTH:
71
+ break
72
+ if current == home:
73
+ break
74
+ current = parent
75
+ if include_home and home not in chain and fsutil.exists(home):
76
+ chain.append(home)
77
+ return chain
78
+
79
+
80
+ def project_roots(ctx):
81
+ roots = []
82
+ for root in [ctx.cwd] + list(ctx.projects):
83
+ absolute = os.path.abspath(os.path.expanduser(root))
84
+ if absolute not in roots:
85
+ roots.append(absolute)
86
+ return roots
87
+
88
+
89
+ def search_roots(ctx):
90
+ home = fsutil.home()
91
+ seen = []
92
+ for root in project_roots(ctx):
93
+ for candidate in ancestors(root, home):
94
+ if candidate not in seen:
95
+ seen.append(candidate)
96
+ return seen
97
+
98
+
99
+ def quote_lines(text, terms, limit=2, width=160):
100
+ if not text or not terms:
101
+ return []
102
+ wanted = set(terms)
103
+ picked = []
104
+ for line in text.splitlines():
105
+ stripped = line.strip().lstrip("#").strip()
106
+ if len(stripped) < 20:
107
+ continue
108
+ if tokens(stripped) & wanted:
109
+ picked.append(stripped[:width])
110
+ if len(picked) >= limit:
111
+ break
112
+ return picked