mirror of
https://github.com/CharlesWiltgen/Axiom.git
synced 2026-09-20 19:58:20 +08:00
fix(axiom-performance,hooks): restore visionOS availability; harden the temp-root guard
Corrects two things the earlier commits in this area got wrong, and closes the false positive they left open. CrashReportExtension visionOS availability.e5f1018bdropped the visionOS claim on the strength of developer.apple.com symbol-page badges; the SDK contradicts them. Against the installed Xcode 27 SDK, 'xcrun --sdk xros swiftc -typecheck -target arm64e-apple-xros27.0' compiles clean, xros26.0 reports "only available in visionOS 27.0 or newer" (a version gate, not an exclusion), while tvOS and watchOS report "unavailable" outright and Mac Catalyst has no module at all. The .swiftinterface carries @available(iOS 27.0, macOS 27.0, *) with @available(tvOS, unavailable) and @available(watchOS, unavailable), and no visionOS clause. Apple's pages disagree with one another, so the SDK leads: skill text, the version-support row and the docs page are restored with the reasoning inline. Temp-root guard. Three ways the guard added byba04043bcould still misjudge a project: - a TMPDIR-less process got /tmp from tempfile.gettempdir(), leaving the macOS per-user scratch root unneutralized and the original false positive alive for any launcher that scrubs the environment — roots now also come from the filesystem on darwin (containers and their T/ dirs); - a relative TMPDIR resolved against the detector's own cwd — the project being judged — turning a real Apple project into a "temp root" and silently disabling Axiom; only absolute values are accepted; - the scan-root guard ran before the repo-root exemption, so a repo rooted at a temp root (devcontainer or CI exporting TMPDIR to the workspace, or a clone in /tmp) read as non-Apple; the exemption now wins. Mirrored into axiom-pi/src/session.ts with the same three tests; the parity matrix gained a repo-rooted-at-a-temp-root case and TMPDIR control. Verified: detector 54/54, axiom-pi 69/69 + typecheck, npm test PASS, test:unit exit 0, check:cursor clean, docs build clean.
This commit is contained in:
@@ -14,7 +14,9 @@ no match statements, no runtime PEP 604 unions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
# Presence of any of these in a directory marks it an Apple project. ".swift"
|
||||
@@ -51,17 +53,44 @@ TEMP_ROOT_NAMES = ("/tmp", "/var/tmp", "/private/tmp")
|
||||
|
||||
|
||||
def _system_temp_roots() -> frozenset[str]:
|
||||
"""System temp directories in both abspath and realpath form."""
|
||||
"""System temp directories in both abspath and realpath form.
|
||||
|
||||
Three sources, because each alone leaves a hole:
|
||||
|
||||
- the fixed names (/tmp and friends) cover launchers that scrub the env;
|
||||
- TMPDIR covers a relocated temp dir, but ONLY when absolute: a relative value
|
||||
resolves against this process's cwd, which for session-start and
|
||||
user-prompt-submit IS the project being judged, and accepting it would
|
||||
silently disable Axiom for a real Apple project;
|
||||
- on macOS the per-user scratch roots come from the filesystem, because
|
||||
Foundation/confstr tools write there whether or not this process inherited
|
||||
TMPDIR. Containers count too — the walk ascends past T/ into the container,
|
||||
which is shared, user-writable scratch in its own right.
|
||||
"""
|
||||
roots = set(TEMP_ROOT_NAMES)
|
||||
try:
|
||||
roots.add(tempfile.gettempdir())
|
||||
except Exception:
|
||||
pass # gettempdir is documented not to raise, but never fail the gate
|
||||
env = os.environ.get("TMPDIR")
|
||||
if env:
|
||||
if env and os.path.isabs(env):
|
||||
roots.add(env)
|
||||
if sys.platform == "darwin":
|
||||
for pattern in (
|
||||
"/var/folders/*/*",
|
||||
"/var/folders/*/*/T",
|
||||
"/private/var/folders/*/*",
|
||||
"/private/var/folders/*/*/T",
|
||||
):
|
||||
try:
|
||||
roots.update(glob.glob(pattern))
|
||||
except Exception:
|
||||
pass
|
||||
return frozenset(
|
||||
form for root in roots for form in (os.path.abspath(root), os.path.realpath(root))
|
||||
form
|
||||
for root in roots
|
||||
if os.path.isabs(root)
|
||||
for form in (os.path.abspath(root), os.path.realpath(root))
|
||||
)
|
||||
|
||||
|
||||
@@ -221,7 +250,14 @@ def is_apple_project(start: str) -> bool:
|
||||
prev = cur
|
||||
levels += 1
|
||||
cur = parent
|
||||
if scan_root in temp_roots or _is_vacuous_scan_root(scan_root, home, found_repo_root):
|
||||
# A temp root that is ALSO a repo root keeps the repo-boundary exemption:
|
||||
# a devcontainer/CI exporting TMPDIR to the workspace, or a clone into
|
||||
# /tmp, is a real project, and refusing it here would be the cardinal sin
|
||||
# that exemption exists to prevent. A temp root that is not a repo root is
|
||||
# still refused, so a stray marker at the shared root stays non-evidence.
|
||||
if (scan_root in temp_roots and not found_repo_root) or _is_vacuous_scan_root(
|
||||
scan_root, home, found_repo_root
|
||||
):
|
||||
return False
|
||||
return _downward_has_marker(scan_root)
|
||||
except Exception:
|
||||
|
||||
@@ -380,6 +380,41 @@ class TestIsAppleProject(unittest.TestCase):
|
||||
touch(os.path.join(work, "Package.swift"))
|
||||
self.assertTrue(pd.is_apple_project(work))
|
||||
|
||||
def test_repo_rooted_at_a_temp_root_is_still_detected(self):
|
||||
# A repo whose ROOT is a temp root (devcontainer/CI exporting TMPDIR to the
|
||||
# workspace, or a clone into /tmp) must keep the repo-root exemption: the
|
||||
# guard must not refuse before found_repo_root gets its say.
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
os.mkdir(os.path.join(d, ".git"))
|
||||
touch(os.path.join(d, "ios", "App.xcodeproj", "x"))
|
||||
with mock.patch.dict(os.environ, {"TMPDIR": d}):
|
||||
self.assertTrue(pd.is_apple_project(d))
|
||||
|
||||
def test_per_user_temp_root_survives_a_missing_TMPDIR(self):
|
||||
# The per-user scratch root is only derivable from TMPDIR when the process
|
||||
# inherited it; with it absent, the roots must still come from the
|
||||
# filesystem, or the original false positive returns for any launcher that
|
||||
# scrubs the environment.
|
||||
live = os.path.realpath(tempfile.gettempdir())
|
||||
if "/var/folders/" not in live:
|
||||
self.skipTest("macOS per-user scratch dirs only")
|
||||
with mock.patch.dict(os.environ):
|
||||
os.environ.pop("TMPDIR", None)
|
||||
tempfile.tempdir = None # drop the cached resolution
|
||||
try:
|
||||
roots = pd._system_temp_roots()
|
||||
finally:
|
||||
tempfile.tempdir = None
|
||||
self.assertIn(live, roots)
|
||||
|
||||
def test_relative_TMPDIR_is_not_treated_as_a_temp_root(self):
|
||||
# A relative TMPDIR resolves against the detector's cwd, which for
|
||||
# session-start/user-prompt-submit IS the project being judged. Accepting it
|
||||
# would silently disable Axiom for a real Apple project.
|
||||
with mock.patch.dict(os.environ, {"TMPDIR": "."}):
|
||||
roots = pd._system_temp_roots()
|
||||
self.assertNotIn(os.path.abspath("."), roots)
|
||||
|
||||
|
||||
class TestResolveContextDecision(unittest.TestCase):
|
||||
def test_never_skips_even_in_apple_dir(self):
|
||||
|
||||
@@ -46,7 +46,7 @@ For memory debugging including jetsam, see `axiom-performance (skills/memory-deb
|
||||
| Per-state metrics (StateReporting framework) | `OS27` (the StateReporting framework itself spans all platforms) |
|
||||
| Metal frame rate metric, launch-task tracking | `OS27` |
|
||||
| Memory exception diagnostics | `iOS27` |
|
||||
| Crash reporter extensions (CrashReportExtension framework) | `OS27` (iOS 27/iPadOS 27/macOS 27 only — not Mac Catalyst, tvOS, watchOS, or visionOS) |
|
||||
| Crash reporter extensions (CrashReportExtension framework) | `OS27` (iOS 27/iPadOS 27/macOS 27/visionOS 27 — not Mac Catalyst, tvOS, or watchOS; visionOS inherits, see Part 10) |
|
||||
|
||||
## Part 1: The New Swift API `OS27`
|
||||
|
||||
@@ -871,7 +871,9 @@ The Xcode 27 Organizer adds a redesigned Overview, Storage and animation-hitches
|
||||
|
||||
## Part 10: CrashReportExtension — Crash Reporter Extensions `OS27`
|
||||
|
||||
A NEW framework (iOS 27, iPadOS 27, macOS 27) for shipping a crash reporter as an app extension. Unavailable on Mac Catalyst (and to iOS apps running on Apple silicon Macs), tvOS, watchOS, and visionOS. Where MetricKit delivers crash *diagnostics* on the app's next run (Part 1), a crash reporter extension is invoked by the system when a crash report is ready to be processed, in its own process separate from the crashed app — the extension point for third-party crash reporters. You can persist the report or send it to a server you control.
|
||||
A NEW framework (iOS 27, iPadOS 27, macOS 27, visionOS 27) for shipping a crash reporter as an app extension. Unavailable on Mac Catalyst (and to iOS apps running on Apple silicon Macs), tvOS, and watchOS. Where MetricKit delivers crash *diagnostics* on the app's next run (Part 1), a crash reporter extension is invoked by the system when a crash report is ready to be processed, in its own process separate from the crashed app — the extension point for third-party crash reporters. You can persist the report or send it to a server you control.
|
||||
|
||||
> **visionOS inherits; it is not excluded.** The framework's `.swiftinterface` carries `@available(iOS 27.0, macOS 27.0, *)` alongside `@available(tvOS, unavailable)` and `@available(watchOS, unavailable)`, with no visionOS clause — and `xcrun --sdk xros swiftc -typecheck -target arm64e-apple-xros27.0` compiles clean while `xros26.0` reports "only available in visionOS 27.0 or newer", a version gate rather than an exclusion. developer.apple.com renders `visionOS: -` on these symbols; the compiler disagrees, and on availability the SDK wins.
|
||||
|
||||
### Extension Setup
|
||||
|
||||
|
||||
Generated
+40
-4
@@ -14,7 +14,9 @@ no match statements, no runtime PEP 604 unions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
# Presence of any of these in a directory marks it an Apple project. ".swift"
|
||||
@@ -51,17 +53,44 @@ TEMP_ROOT_NAMES = ("/tmp", "/var/tmp", "/private/tmp")
|
||||
|
||||
|
||||
def _system_temp_roots() -> frozenset[str]:
|
||||
"""System temp directories in both abspath and realpath form."""
|
||||
"""System temp directories in both abspath and realpath form.
|
||||
|
||||
Three sources, because each alone leaves a hole:
|
||||
|
||||
- the fixed names (/tmp and friends) cover launchers that scrub the env;
|
||||
- TMPDIR covers a relocated temp dir, but ONLY when absolute: a relative value
|
||||
resolves against this process's cwd, which for session-start and
|
||||
user-prompt-submit IS the project being judged, and accepting it would
|
||||
silently disable Axiom for a real Apple project;
|
||||
- on macOS the per-user scratch roots come from the filesystem, because
|
||||
Foundation/confstr tools write there whether or not this process inherited
|
||||
TMPDIR. Containers count too — the walk ascends past T/ into the container,
|
||||
which is shared, user-writable scratch in its own right.
|
||||
"""
|
||||
roots = set(TEMP_ROOT_NAMES)
|
||||
try:
|
||||
roots.add(tempfile.gettempdir())
|
||||
except Exception:
|
||||
pass # gettempdir is documented not to raise, but never fail the gate
|
||||
env = os.environ.get("TMPDIR")
|
||||
if env:
|
||||
if env and os.path.isabs(env):
|
||||
roots.add(env)
|
||||
if sys.platform == "darwin":
|
||||
for pattern in (
|
||||
"/var/folders/*/*",
|
||||
"/var/folders/*/*/T",
|
||||
"/private/var/folders/*/*",
|
||||
"/private/var/folders/*/*/T",
|
||||
):
|
||||
try:
|
||||
roots.update(glob.glob(pattern))
|
||||
except Exception:
|
||||
pass
|
||||
return frozenset(
|
||||
form for root in roots for form in (os.path.abspath(root), os.path.realpath(root))
|
||||
form
|
||||
for root in roots
|
||||
if os.path.isabs(root)
|
||||
for form in (os.path.abspath(root), os.path.realpath(root))
|
||||
)
|
||||
|
||||
|
||||
@@ -221,7 +250,14 @@ def is_apple_project(start: str) -> bool:
|
||||
prev = cur
|
||||
levels += 1
|
||||
cur = parent
|
||||
if scan_root in temp_roots or _is_vacuous_scan_root(scan_root, home, found_repo_root):
|
||||
# A temp root that is ALSO a repo root keeps the repo-boundary exemption:
|
||||
# a devcontainer/CI exporting TMPDIR to the workspace, or a clone into
|
||||
# /tmp, is a real project, and refusing it here would be the cardinal sin
|
||||
# that exemption exists to prevent. A temp root that is not a repo root is
|
||||
# still refused, so a stray marker at the shared root stays non-evidence.
|
||||
if (scan_root in temp_roots and not found_repo_root) or _is_vacuous_scan_root(
|
||||
scan_root, home, found_repo_root
|
||||
):
|
||||
return False
|
||||
return _downward_has_marker(scan_root)
|
||||
except Exception:
|
||||
|
||||
@@ -46,7 +46,7 @@ For memory debugging including jetsam, see `axiom-performance (skills/memory-deb
|
||||
| Per-state metrics (StateReporting framework) | `OS27` (the StateReporting framework itself spans all platforms) |
|
||||
| Metal frame rate metric, launch-task tracking | `OS27` |
|
||||
| Memory exception diagnostics | `iOS27` |
|
||||
| Crash reporter extensions (CrashReportExtension framework) | `OS27` (iOS 27/iPadOS 27/macOS 27 only — not Mac Catalyst, tvOS, watchOS, or visionOS) |
|
||||
| Crash reporter extensions (CrashReportExtension framework) | `OS27` (iOS 27/iPadOS 27/macOS 27/visionOS 27 — not Mac Catalyst, tvOS, or watchOS; visionOS inherits, see Part 10) |
|
||||
|
||||
## Part 1: The New Swift API `OS27`
|
||||
|
||||
@@ -871,7 +871,9 @@ The Xcode 27 Organizer adds a redesigned Overview, Storage and animation-hitches
|
||||
|
||||
## Part 10: CrashReportExtension — Crash Reporter Extensions `OS27`
|
||||
|
||||
A NEW framework (iOS 27, iPadOS 27, macOS 27) for shipping a crash reporter as an app extension. Unavailable on Mac Catalyst (and to iOS apps running on Apple silicon Macs), tvOS, watchOS, and visionOS. Where MetricKit delivers crash *diagnostics* on the app's next run (Part 1), a crash reporter extension is invoked by the system when a crash report is ready to be processed, in its own process separate from the crashed app — the extension point for third-party crash reporters. You can persist the report or send it to a server you control.
|
||||
A NEW framework (iOS 27, iPadOS 27, macOS 27, visionOS 27) for shipping a crash reporter as an app extension. Unavailable on Mac Catalyst (and to iOS apps running on Apple silicon Macs), tvOS, and watchOS. Where MetricKit delivers crash *diagnostics* on the app's next run (Part 1), a crash reporter extension is invoked by the system when a crash report is ready to be processed, in its own process separate from the crashed app — the extension point for third-party crash reporters. You can persist the report or send it to a server you control.
|
||||
|
||||
> **visionOS inherits; it is not excluded.** The framework's `.swiftinterface` carries `@available(iOS 27.0, macOS 27.0, *)` alongside `@available(tvOS, unavailable)` and `@available(watchOS, unavailable)`, with no visionOS clause — and `xcrun --sdk xros swiftc -typecheck -target arm64e-apple-xros27.0` compiles clean while `xros26.0` reports "only available in visionOS 27.0 or newer", a version gate rather than an exclusion. developer.apple.com renders `visionOS: -` on these symbols; the compiler disagrees, and on availability the SDK wins.
|
||||
|
||||
### Extension Setup
|
||||
|
||||
|
||||
+5
-5
@@ -343,8 +343,8 @@
|
||||
},
|
||||
{
|
||||
"path": "scripts/project_detect.py",
|
||||
"sha256": "c4cd1310c95915b32dbffe571d1fe5f5e4a282d3c167934c572ae2880d405184",
|
||||
"bytes": 11429
|
||||
"sha256": "1a625e741889969b0cf7aee190e2b2e29a594923d528d97166d0dc414ef7fab3",
|
||||
"bytes": 13039
|
||||
},
|
||||
{
|
||||
"path": "scripts/subagent-start.py",
|
||||
@@ -1383,8 +1383,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-performance/skills/metrickit-ref.md",
|
||||
"sha256": "01dd7046e0e3b32e48cb3d7af94a0724dcc9ab279265ae0c4018f5498fc1f466",
|
||||
"bytes": 38607
|
||||
"sha256": "c8eeff8dbdab1275b08ddd86637f18be6a56f504098b71da40cd0ff7dbcf1e05",
|
||||
"bytes": 39189
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-performance/skills/objc-block-retain-cycles.md",
|
||||
@@ -1879,7 +1879,7 @@
|
||||
],
|
||||
"totals": {
|
||||
"files": 375,
|
||||
"bytes": 7371587
|
||||
"bytes": 7373779
|
||||
},
|
||||
"excludedMirrors": 30,
|
||||
"classes": {
|
||||
|
||||
Generated
+40
-4
@@ -14,7 +14,9 @@ no match statements, no runtime PEP 604 unions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
# Presence of any of these in a directory marks it an Apple project. ".swift"
|
||||
@@ -51,17 +53,44 @@ TEMP_ROOT_NAMES = ("/tmp", "/var/tmp", "/private/tmp")
|
||||
|
||||
|
||||
def _system_temp_roots() -> frozenset[str]:
|
||||
"""System temp directories in both abspath and realpath form."""
|
||||
"""System temp directories in both abspath and realpath form.
|
||||
|
||||
Three sources, because each alone leaves a hole:
|
||||
|
||||
- the fixed names (/tmp and friends) cover launchers that scrub the env;
|
||||
- TMPDIR covers a relocated temp dir, but ONLY when absolute: a relative value
|
||||
resolves against this process's cwd, which for session-start and
|
||||
user-prompt-submit IS the project being judged, and accepting it would
|
||||
silently disable Axiom for a real Apple project;
|
||||
- on macOS the per-user scratch roots come from the filesystem, because
|
||||
Foundation/confstr tools write there whether or not this process inherited
|
||||
TMPDIR. Containers count too — the walk ascends past T/ into the container,
|
||||
which is shared, user-writable scratch in its own right.
|
||||
"""
|
||||
roots = set(TEMP_ROOT_NAMES)
|
||||
try:
|
||||
roots.add(tempfile.gettempdir())
|
||||
except Exception:
|
||||
pass # gettempdir is documented not to raise, but never fail the gate
|
||||
env = os.environ.get("TMPDIR")
|
||||
if env:
|
||||
if env and os.path.isabs(env):
|
||||
roots.add(env)
|
||||
if sys.platform == "darwin":
|
||||
for pattern in (
|
||||
"/var/folders/*/*",
|
||||
"/var/folders/*/*/T",
|
||||
"/private/var/folders/*/*",
|
||||
"/private/var/folders/*/*/T",
|
||||
):
|
||||
try:
|
||||
roots.update(glob.glob(pattern))
|
||||
except Exception:
|
||||
pass
|
||||
return frozenset(
|
||||
form for root in roots for form in (os.path.abspath(root), os.path.realpath(root))
|
||||
form
|
||||
for root in roots
|
||||
if os.path.isabs(root)
|
||||
for form in (os.path.abspath(root), os.path.realpath(root))
|
||||
)
|
||||
|
||||
|
||||
@@ -221,7 +250,14 @@ def is_apple_project(start: str) -> bool:
|
||||
prev = cur
|
||||
levels += 1
|
||||
cur = parent
|
||||
if scan_root in temp_roots or _is_vacuous_scan_root(scan_root, home, found_repo_root):
|
||||
# A temp root that is ALSO a repo root keeps the repo-boundary exemption:
|
||||
# a devcontainer/CI exporting TMPDIR to the workspace, or a clone into
|
||||
# /tmp, is a real project, and refusing it here would be the cardinal sin
|
||||
# that exemption exists to prevent. A temp root that is not a repo root is
|
||||
# still refused, so a stray marker at the shared root stays non-evidence.
|
||||
if (scan_root in temp_roots and not found_repo_root) or _is_vacuous_scan_root(
|
||||
scan_root, home, found_repo_root
|
||||
):
|
||||
return False
|
||||
return _downward_has_marker(scan_root)
|
||||
except Exception:
|
||||
|
||||
@@ -50,7 +50,7 @@ For memory debugging including jetsam, see `axiom-performance (skills/memory-deb
|
||||
| Per-state metrics (StateReporting framework) | `OS27` (the StateReporting framework itself spans all platforms) |
|
||||
| Metal frame rate metric, launch-task tracking | `OS27` |
|
||||
| Memory exception diagnostics | `iOS27` |
|
||||
| Crash reporter extensions (CrashReportExtension framework) | `OS27` (iOS 27/iPadOS 27/macOS 27 only — not Mac Catalyst, tvOS, watchOS, or visionOS) |
|
||||
| Crash reporter extensions (CrashReportExtension framework) | `OS27` (iOS 27/iPadOS 27/macOS 27/visionOS 27 — not Mac Catalyst, tvOS, or watchOS; visionOS inherits, see Part 10) |
|
||||
|
||||
## Part 1: The New Swift API `OS27`
|
||||
|
||||
@@ -875,7 +875,9 @@ The Xcode 27 Organizer adds a redesigned Overview, Storage and animation-hitches
|
||||
|
||||
## Part 10: CrashReportExtension — Crash Reporter Extensions `OS27`
|
||||
|
||||
A NEW framework (iOS 27, iPadOS 27, macOS 27) for shipping a crash reporter as an app extension. Unavailable on Mac Catalyst (and to iOS apps running on Apple silicon Macs), tvOS, watchOS, and visionOS. Where MetricKit delivers crash *diagnostics* on the app's next run (Part 1), a crash reporter extension is invoked by the system when a crash report is ready to be processed, in its own process separate from the crashed app — the extension point for third-party crash reporters. You can persist the report or send it to a server you control.
|
||||
A NEW framework (iOS 27, iPadOS 27, macOS 27, visionOS 27) for shipping a crash reporter as an app extension. Unavailable on Mac Catalyst (and to iOS apps running on Apple silicon Macs), tvOS, and watchOS. Where MetricKit delivers crash *diagnostics* on the app's next run (Part 1), a crash reporter extension is invoked by the system when a crash report is ready to be processed, in its own process separate from the crashed app — the extension point for third-party crash reporters. You can persist the report or send it to a server you control.
|
||||
|
||||
> **visionOS inherits; it is not excluded.** The framework's `.swiftinterface` carries `@available(iOS 27.0, macOS 27.0, *)` alongside `@available(tvOS, unavailable)` and `@available(watchOS, unavailable)`, with no visionOS clause — and `xcrun --sdk xros swiftc -typecheck -target arm64e-apple-xros27.0` compiles clean while `xros26.0` reports "only available in visionOS 27.0 or newer", a version gate rather than an exclusion. developer.apple.com renders `visionOS: -` on these symbols; the compiler disagrees, and on availability the SDK wins.
|
||||
|
||||
### Extension Setup
|
||||
|
||||
|
||||
Vendored
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"totalBytes": 13498195,
|
||||
"totalBytes": 13499416,
|
||||
"skills": {
|
||||
"count": 303,
|
||||
"bytes": 7380339
|
||||
"bytes": 7380925
|
||||
},
|
||||
"commands": {
|
||||
"count": 17,
|
||||
@@ -13,7 +13,7 @@
|
||||
"bytes": 677829
|
||||
},
|
||||
"searchIndex": {
|
||||
"bytes": 5392009
|
||||
"bytes": 5392644
|
||||
},
|
||||
"generatedAt": "2026-09-15T15:35:12.257Z"
|
||||
"generatedAt": "2026-09-15T16:10:03.566Z"
|
||||
}
|
||||
Vendored
+135
-54
File diff suppressed because one or more lines are too long
@@ -12,6 +12,7 @@ import {
|
||||
isAppleProject,
|
||||
isVacuousScanRoot,
|
||||
resolveContextDecision,
|
||||
systemTempRoots,
|
||||
} from "./session.ts";
|
||||
|
||||
describe("formatDate", () => {
|
||||
@@ -185,6 +186,48 @@ describe("isAppleProject / resolveContextDecision", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("still detects a repo rooted at a temp root", () => {
|
||||
// A devcontainer/CI exporting TMPDIR to the workspace, or a clone into /tmp,
|
||||
// keeps the repo-boundary exemption — refusing it would be the cardinal sin.
|
||||
const repo = fs.mkdtempSync(path.join(os.tmpdir(), "axiom-temp-repo-"));
|
||||
const prior = process.env.TMPDIR;
|
||||
try {
|
||||
fs.mkdirSync(path.join(repo, ".git"));
|
||||
fs.mkdirSync(path.join(repo, "ios", "App.xcodeproj"), { recursive: true });
|
||||
process.env.TMPDIR = repo;
|
||||
expect(isAppleProject(repo)).toBe(true);
|
||||
} finally {
|
||||
if (prior === undefined) delete process.env.TMPDIR;
|
||||
else process.env.TMPDIR = prior;
|
||||
fs.rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("derives the macOS per-user scratch root without TMPDIR", () => {
|
||||
const live = fs.realpathSync(os.tmpdir());
|
||||
if (!live.includes("/var/folders/")) return; // macOS per-user scratch only
|
||||
const prior = process.env.TMPDIR;
|
||||
try {
|
||||
delete process.env.TMPDIR;
|
||||
expect(systemTempRoots().has(live)).toBe(true);
|
||||
} finally {
|
||||
if (prior !== undefined) process.env.TMPDIR = prior;
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores a relative TMPDIR", () => {
|
||||
// A relative TMPDIR resolves against the project being judged; honoring it
|
||||
// would silently disable Axiom for a real Apple project.
|
||||
const prior = process.env.TMPDIR;
|
||||
try {
|
||||
process.env.TMPDIR = ".";
|
||||
expect(systemTempRoots().has(path.resolve("."))).toBe(false);
|
||||
} finally {
|
||||
if (prior === undefined) delete process.env.TMPDIR;
|
||||
else process.env.TMPDIR = prior;
|
||||
}
|
||||
});
|
||||
|
||||
// Mirrors TestIsVacuousScanRoot in project_detect_test.py. Tested directly
|
||||
// because a genuinely shallow path (/app) cannot be built under a temp dir, so
|
||||
// an end-to-end test silently never reaches the depth rule at all.
|
||||
@@ -342,12 +385,12 @@ describe("project detection parity with project_detect.py", () => {
|
||||
"hooks",
|
||||
);
|
||||
|
||||
type Case = { name: string; cwd: string; home: string | null };
|
||||
type Case = { name: string; cwd: string; home: string | null; tmpdir: string | null };
|
||||
|
||||
/** Verdicts from the shipped Python detector — one subprocess for the matrix. */
|
||||
function pythonVerdicts(cases: readonly Case[]): boolean[] {
|
||||
const script = [
|
||||
"import json, os, sys",
|
||||
"import json, os, sys, tempfile",
|
||||
`sys.path.insert(0, ${JSON.stringify(HOOKS_DIR)})`,
|
||||
"import project_detect",
|
||||
"out = []",
|
||||
@@ -356,6 +399,11 @@ describe("project detection parity with project_detect.py", () => {
|
||||
" os.environ['HOME'] = case['home']",
|
||||
" else:",
|
||||
" os.environ.pop('HOME', None)",
|
||||
" if case['tmpdir']:",
|
||||
" os.environ['TMPDIR'] = case['tmpdir']",
|
||||
" else:",
|
||||
" os.environ.pop('TMPDIR', None)",
|
||||
" tempfile.tempdir = None # re-resolve after the env change",
|
||||
" out.append(project_detect.is_apple_project(case['cwd']))",
|
||||
"print(json.dumps(out))",
|
||||
].join("\n");
|
||||
@@ -373,18 +421,23 @@ describe("project detection parity with project_detect.py", () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** The same verdicts from this module, with each case's HOME applied. */
|
||||
/** The same verdicts from this module, with each case's HOME and TMPDIR applied. */
|
||||
function tsVerdicts(cases: readonly Case[]): boolean[] {
|
||||
const prior = process.env.HOME;
|
||||
const priorHome = process.env.HOME;
|
||||
const priorTmpdir = process.env.TMPDIR;
|
||||
try {
|
||||
return cases.map((entry) => {
|
||||
if (entry.home === null) delete process.env.HOME;
|
||||
else process.env.HOME = entry.home;
|
||||
if (entry.tmpdir === null) delete process.env.TMPDIR;
|
||||
else process.env.TMPDIR = entry.tmpdir;
|
||||
return isAppleProject(entry.cwd);
|
||||
});
|
||||
} finally {
|
||||
if (prior === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = prior;
|
||||
if (priorHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = priorHome;
|
||||
if (priorTmpdir === undefined) delete process.env.TMPDIR;
|
||||
else process.env.TMPDIR = priorTmpdir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,6 +463,9 @@ describe("project detection parity with project_detect.py", () => {
|
||||
const tempProject = fs.mkdtempSync(path.join(os.tmpdir(), "axiom-parity-project-"));
|
||||
fs.writeFileSync(path.join(tempProject, "Package.swift"), "");
|
||||
const tempPlain = fs.mkdtempSync(path.join(os.tmpdir(), "axiom-parity-plain-"));
|
||||
const tempRepo = fs.mkdtempSync(path.join(os.tmpdir(), "axiom-parity-repo-"));
|
||||
fs.mkdirSync(path.join(tempRepo, ".git"));
|
||||
fs.mkdirSync(path.join(tempRepo, "ios", "App.xcodeproj"), { recursive: true });
|
||||
const tempProbe = path.join(os.tmpdir(), `axiom-parity-probe-${process.pid}.swift`);
|
||||
fs.writeFileSync(tempProbe, "");
|
||||
|
||||
@@ -432,24 +488,26 @@ describe("project detection parity with project_detect.py", () => {
|
||||
|
||||
expectParity(
|
||||
[
|
||||
{ name: "plain dir", cwd: plain, home: null },
|
||||
{ name: "marker at cwd", cwd: atCwd, home: null },
|
||||
{ name: "marker in ancestor", cwd: ancestor, home: null },
|
||||
{ name: "marker-free git repo", cwd: gitRepo, home: null },
|
||||
{ name: "marker above the git root", cwd: repoInside, home: null },
|
||||
{ name: "non-git dir under a .swiftpm home", cwd: underHome, home },
|
||||
{ name: "visible .swiftpm package", cwd: playgrounds, home: null },
|
||||
{ name: "project inside the temp root", cwd: tempProject, home: null },
|
||||
{ name: "plain temp dir under a polluted temp root", cwd: tempPlain, home: null },
|
||||
{ name: "the temp root itself", cwd: os.tmpdir(), home: null },
|
||||
{ name: "missing path (fail-open)", cwd: path.join(scratch, "nope"), home: null },
|
||||
{ name: "plain dir", cwd: plain, home: null, tmpdir: null },
|
||||
{ name: "marker at cwd", cwd: atCwd, home: null, tmpdir: null },
|
||||
{ name: "marker in ancestor", cwd: ancestor, home: null, tmpdir: null },
|
||||
{ name: "marker-free git repo", cwd: gitRepo, home: null, tmpdir: null },
|
||||
{ name: "marker above the git root", cwd: repoInside, home: null, tmpdir: null },
|
||||
{ name: "non-git dir under a .swiftpm home", cwd: underHome, home, tmpdir: null },
|
||||
{ name: "visible .swiftpm package", cwd: playgrounds, home: null, tmpdir: null },
|
||||
{ name: "project inside the temp root", cwd: tempProject, home: null, tmpdir: null },
|
||||
{ name: "plain temp dir under a polluted temp root", cwd: tempPlain, home: null, tmpdir: null },
|
||||
{ name: "the temp root itself", cwd: os.tmpdir(), home: null, tmpdir: null },
|
||||
{ name: "missing path (fail-open)", cwd: path.join(scratch, "nope"), home: null, tmpdir: null },
|
||||
{ name: "repo rooted at a temp root", cwd: tempRepo, home: null, tmpdir: tempRepo },
|
||||
],
|
||||
[false, true, true, false, false, false, true, true, false, false, true],
|
||||
[false, true, true, false, false, false, true, true, false, false, true, true],
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(scratch, { recursive: true, force: true });
|
||||
fs.rmSync(tempProject, { recursive: true, force: true });
|
||||
fs.rmSync(tempPlain, { recursive: true, force: true });
|
||||
fs.rmSync(tempRepo, { recursive: true, force: true });
|
||||
fs.rmSync(tempProbe, { force: true });
|
||||
}
|
||||
});
|
||||
@@ -458,7 +516,7 @@ describe("project detection parity with project_detect.py", () => {
|
||||
const big = fs.mkdtempSync(path.join(os.tmpdir(), "axiom-parity-big-"));
|
||||
try {
|
||||
for (let i = 0; i < 10_050; i++) fs.writeFileSync(path.join(big, `f${i}`), "");
|
||||
expectParity([{ name: "oversized tree", cwd: big, home: null }], [true]);
|
||||
expectParity([{ name: "oversized tree", cwd: big, home: null, tmpdir: null }], [true]);
|
||||
} finally {
|
||||
fs.rmSync(big, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
+56
-6
@@ -132,15 +132,59 @@ const TEMP_ROOTS: Record<string, true> = {
|
||||
"/private/tmp": true,
|
||||
};
|
||||
|
||||
/** System temp directories in both resolved and realpath form. */
|
||||
function systemTempRoots(): Set<string> {
|
||||
// Record for the static names; the membership set is computed because $TMPDIR
|
||||
// and realpath resolution are environment-dependent.
|
||||
/**
|
||||
* System temp directories in both resolved and realpath form.
|
||||
*
|
||||
* Three sources, matching `_system_temp_roots` in project_detect.py: the fixed
|
||||
* names, an ABSOLUTE $TMPDIR (a relative one resolves against the project being
|
||||
* judged and would silently disable Axiom), and — on macOS — the per-user
|
||||
* scratch roots read from the filesystem, because Foundation tools write there
|
||||
* whether or not this process inherited TMPDIR. Containers count too: the walk
|
||||
* ascends past T/ into the shared container.
|
||||
*/
|
||||
export function systemTempRoots(): Set<string> {
|
||||
const candidates = new Set<string>(Object.keys(TEMP_ROOTS));
|
||||
candidates.add(os.tmpdir());
|
||||
if (process.env.TMPDIR) candidates.add(process.env.TMPDIR);
|
||||
const env = process.env.TMPDIR;
|
||||
if (env && path.isAbsolute(env)) candidates.add(env);
|
||||
if (process.platform === "darwin") {
|
||||
// Same two levels the Python helper globs: the per-user container and its T/
|
||||
// dir. Owners (/var/folders/_s) are NOT roots — matching Python here matters,
|
||||
// because the parity gate compares the two verdict-by-verdict.
|
||||
for (const base of ["/var/folders", "/private/var/folders"]) {
|
||||
let owners: string[];
|
||||
try {
|
||||
owners = fs.readdirSync(base);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const owner of owners) {
|
||||
if (owner.startsWith(".")) continue;
|
||||
const ownerDir = path.join(base, owner);
|
||||
let containers: string[];
|
||||
try {
|
||||
containers = fs.readdirSync(ownerDir);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const name of containers) {
|
||||
if (name.startsWith(".")) continue;
|
||||
const container = path.join(ownerDir, name);
|
||||
candidates.add(container);
|
||||
try {
|
||||
if (fs.readdirSync(container).includes("T")) {
|
||||
candidates.add(path.join(container, "T"));
|
||||
}
|
||||
} catch {
|
||||
// Unreadable container — the container itself is already added.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const forms = new Set<string>();
|
||||
for (const root of candidates) {
|
||||
if (!path.isAbsolute(root)) continue;
|
||||
forms.add(path.resolve(root));
|
||||
try {
|
||||
forms.add(fs.realpathSync(root));
|
||||
@@ -268,7 +312,13 @@ export function isAppleProject(start: string): boolean {
|
||||
levels++;
|
||||
cur = parent;
|
||||
}
|
||||
if (tempRoots.has(scanRoot) || isVacuousScanRoot(scanRoot, home, foundRepoRoot)) return false;
|
||||
// A temp root that is ALSO a repo root keeps the repo-boundary exemption — a
|
||||
// devcontainer/CI exporting TMPDIR to the workspace, or a clone into /tmp, is
|
||||
// a real project. A temp root that is not a repo root is still refused, so a
|
||||
// stray marker at the shared root stays non-evidence.
|
||||
if ((tempRoots.has(scanRoot) && !foundRepoRoot) || isVacuousScanRoot(scanRoot, home, foundRepoRoot)) {
|
||||
return false;
|
||||
}
|
||||
return downwardHasMarker(scanRoot);
|
||||
} catch {
|
||||
return true;
|
||||
|
||||
@@ -39,7 +39,7 @@ Questions you can ask Claude that will draw from this reference:
|
||||
|
||||
- The new Swift API (27): MetricManager setup, MetricReport interval entries, the full MetricResult metric inventory (including Metal frame rate and storage metrics, and `HitchTimeMetric` with its `HitchTimeRatio` unit — the Swift API has no scroll-specific hitch metric), launch-task tracking, typed diagnostics with termination categories, and memory exception diagnostics
|
||||
- Per-state metrics: StateReporting domains, state transitions, the `@ReportableMetadata` macro, and state-grouped report encoding
|
||||
- Crash reporter extensions (27, iOS/iPadOS/macOS only): the CrashReportExtension framework — CrashedProcess, in-extension symbolication, binary image inventory, and the extension-point setup
|
||||
- Crash reporter extensions (27, iOS/iPadOS/macOS/visionOS): the CrashReportExtension framework — CrashedProcess, in-extension symbolication, binary image inventory, and the extension-point setup
|
||||
- Migration map from the soft-deprecated MX* API to the 27 API
|
||||
- MXMetricManagerSubscriber setup and registration timing (legacy)
|
||||
- MXMetricPayload: CPU, memory, launch time histograms, disk I/O, network, scroll hitches, signpost metrics
|
||||
|
||||
Reference in New Issue
Block a user