claude-dev-env 1.49.0 → 1.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +72 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +36 -0
- package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +35 -0
- package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +35 -0
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +38 -0
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +38 -0
- package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +38 -0
- package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +40 -0
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +38 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +46 -0
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +59 -0
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +45 -0
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +44 -0
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +45 -0
- package/audit-rubrics/prompts/category-a-api-contracts.md +384 -0
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +401 -0
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +420 -0
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +414 -0
- package/audit-rubrics/prompts/category-e-dead-code.md +420 -0
- package/audit-rubrics/prompts/category-f-silent-failures.md +420 -0
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +383 -0
- package/audit-rubrics/prompts/category-h-security-boundaries.md +423 -0
- package/audit-rubrics/prompts/category-i-concurrency.md +429 -0
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +463 -0
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +328 -0
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +128 -0
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +129 -0
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +132 -0
- package/audit-rubrics/source-material-section-types.md +51 -0
- package/package.json +2 -1
- package/skills/bugteam/reference/teardown-publish-permissions.md +7 -2
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
Audit [REPO/ARTIFACT] [TARGET_ID] for **Category B only** (selector / query / engine compatibility). Skip A, C–K. Sub-bucket forced-exhaustion mode: Category B is decomposed into 7 sub-buckets below. Each sub-bucket REQUIRES at least one Shape A finding OR exactly one Shape B proof-of-absence with **at least 3 adversarial probes** specific to that sub-bucket. A sub-bucket returning neither is a protocol gap.
|
|
2
|
+
|
|
3
|
+
[ARTIFACT METADATA: repo, ref/SHA, PR or commit range, file count, language matrix, declared engine/runtime/browser/DB targets — fill before running.]
|
|
4
|
+
ID prefix: `find`.
|
|
5
|
+
|
|
6
|
+
## Source material ([N] files/sections, all lines in scope)
|
|
7
|
+
|
|
8
|
+
[INLINE THE FULL ARTIFACT HERE — see ../source-material-section-types.md for chunking guidance.]
|
|
9
|
+
|
|
10
|
+
## Sub-buckets
|
|
11
|
+
|
|
12
|
+
**B1. CSS / DOM selector vs target browser engine**
|
|
13
|
+
- Every CSS selector in the diff — verify pseudo-class support (`:has()`, `:is()`, `:where()`, `:focus-visible`, `:focus-within`) against every browser engine in the declared support matrix; flag any selector that requires an engine version newer than the declared minimum.
|
|
14
|
+
- Every attribute selector, `::part()`, `::slotted()`, and shadow-DOM piercing pattern — verify the target engine actually exposes the matching DOM (e.g. shadow boundaries, scoped styles).
|
|
15
|
+
- Every selector fed to `document.querySelector` / `querySelectorAll` / jQuery `$()` / Selenium `By.css_selector` / Playwright `page.locator` — verify the **runtime** selector engine matches the **target browser** engine; a Node-side jsdom selector parse can succeed where the real browser fails (or vice versa).
|
|
16
|
+
- Every CSS feature query (`@supports`), media query level, and container query — verify availability across the declared engine matrix and that fallback paths exist for engines that do not parse the syntax.
|
|
17
|
+
- Every test-runner DOM assertion (snapshot, `outerHTML`/`innerHTML` equality, computed-style read) — verify the runner uses the same engine as the production target, or document the divergence.
|
|
18
|
+
- Cross-engine quirks: WebKit-only `-webkit-` prefixes, Gecko-only `-moz-`, IE/Edge legacy filters; flag any production reliance on a single-engine prefix without a standard fallback.
|
|
19
|
+
|
|
20
|
+
**B2. SQL syntax vs database version**
|
|
21
|
+
- Every SQL string literal, ORM-generated query, and migration — verify CTE (`WITH … AS`) support against the declared minimum DB version (MySQL ≥ 8.0, MariaDB ≥ 10.2, SQLite ≥ 3.8.3, etc.).
|
|
22
|
+
- Every window function (`OVER`, `PARTITION BY`, `ROWS BETWEEN`), `LATERAL` join, `FILTER` clause, and `MERGE`/`UPSERT` syntax — verify the target dialect and version supports it.
|
|
23
|
+
- Every JSON operator (`->`, `->>`, `@>`, `?`, `jsonb_path_query`, `JSON_VALUE`, `JSON_EXTRACT`) — verify dialect-specific syntax matches the declared engine.
|
|
24
|
+
- Every full-text search clause (`MATCH … AGAINST`, `to_tsvector`, `CONTAINS`, `FREETEXT`) and every spatial / geometry function — verify engine and version availability.
|
|
25
|
+
- Every dialect-specific function (`GROUP_CONCAT` vs `STRING_AGG` vs `LISTAGG`, `IFNULL` vs `NVL` vs `COALESCE`, `LIMIT n` vs `TOP n` vs `FETCH FIRST n ROWS`).
|
|
26
|
+
- Every migration's reversibility and online-DDL safety on the target version (ALGORITHM/LOCK hints, transactional DDL availability).
|
|
27
|
+
|
|
28
|
+
**B3. Regex syntax vs engine flavor**
|
|
29
|
+
- Every regex in the diff — verify lookbehind support (POSIX ERE has none; Python `re` requires fixed-width lookbehind; Python `regex`, PCRE, Perl, .NET, JS V8 ≥ 2018 allow variable-width).
|
|
30
|
+
- Every named group — verify `(?P<name>…)` (Python) vs `(?<name>…)` (PCRE/JS/.NET) vs `(?'name'…)` (.NET only) matches the engine in use.
|
|
31
|
+
- Every backreference, recursion (`(?R)`, `(?0)`), conditional (`(?(1)yes|no)`), atomic group (`(?>…)`), possessive quantifier (`*+`, `++`) — verify engine support.
|
|
32
|
+
- Every Unicode character class (`\p{L}`, `\p{Script=Greek}`) and Unicode flag (`u`, `re.UNICODE`) — verify the engine has Unicode tables compiled in and the flag is honored.
|
|
33
|
+
- Every regex passed across boundaries (Python → JS via JSON, server → client) — verify the consumer engine accepts the same flavor; flag any flavor-translation gap.
|
|
34
|
+
- Every f-string- or template-built regex — verify interpolated values are escaped (`re.escape`, `RegExp.escape` proposal, manual literal-quote helpers) so user-supplied input cannot inject metacharacters.
|
|
35
|
+
|
|
36
|
+
**B4. Shell / CLI / cmdlet syntax vs runtime version**
|
|
37
|
+
- Every PowerShell cmdlet — verify availability across the declared edition matrix (Windows PowerShell 5.1 ↔ PowerShell 7+ ↔ PowerShell on Linux/macOS) and version-specific parameter sets.
|
|
38
|
+
- Every shebang (`#!/usr/bin/env pwsh`, `#!/bin/bash`, `#!/usr/bin/env python3`) vs the actual interpreter resolved at runtime — flag mismatches between declared and invoked interpreter (e.g. shebang says `pwsh` but Python `subprocess.run(["powershell", …])` resolves to PS 5.1 on Windows).
|
|
39
|
+
- Every parameter set in a `param(...)` block — verify `[CmdletBinding(DefaultParameterSetName=…)]` is set when ambiguity is possible; missing default = `Parameter set cannot be resolved` at runtime.
|
|
40
|
+
- Every cmdlet flag combination — verify both flags belong to the same parameter set per Microsoft docs (e.g. `-RepetitionInterval` is an `-Once` parameter, NOT `-Daily`).
|
|
41
|
+
- Every bash-ism — `[[ ... ]]` (bash 3+ only, not POSIX `sh`), arrays, process substitution `<(…)`, `${var,,}` lower-case expansion (bash 4+), associative arrays — verify against the declared minimum shell.
|
|
42
|
+
- Every CLI flag deprecation across versions (`gh` flag changes, `git` porcelain v2, `docker` deprecated commands) — verify the declared minimum tool version still accepts the syntax in use.
|
|
43
|
+
- Every `Get-Command`, `command -v`, `which` lookup — verify error-handling parity (`-ErrorAction SilentlyContinue` on the failing path, `|| true`, `2>/dev/null`) so a missing dependency degrades gracefully across versions.
|
|
44
|
+
|
|
45
|
+
**B5. JSON path / XPath / structural query vs library**
|
|
46
|
+
- Every JSONPath expression — verify the target library's flavor (`jq` vs Python `jsonpath-ng` vs `jsonpath-rw` vs JS `jsonpath-plus` vs Goessner reference). Filter syntax (`[?(@.foo)]`), recursive descent (`..`), and slicing differ.
|
|
47
|
+
- Every XPath expression — verify XPath 1.0 vs 2.0 vs 3.0 functions (`matches()`, `tokenize()`, `string-join()` are 2.0+); flag any 2.0+ function fed to a 1.0-only engine like `lxml.etree`'s default.
|
|
48
|
+
- Every JSON Pointer (`/foo/bar/0`), JSON Patch op, and JMESPath query — verify library version and edge cases (escaped `~` and `/`).
|
|
49
|
+
- Every YAML/TOML structural query (`yq`, `tomlq`) — verify version flavor (`yq` v3 Python ↔ v4 Go are incompatible).
|
|
50
|
+
- Every cross-library round-trip: `jq` filter handed to a Python jsonpath consumer, or XPath compiled in lxml then serialized for libxslt — verify each consumer accepts the source flavor.
|
|
51
|
+
|
|
52
|
+
**B6. Search query DSL vs engine**
|
|
53
|
+
- Every Lucene/Elasticsearch query string — verify field syntax (`field:value`), required/excluded operators (`+`, `-`), fuzzy (`term~2`), proximity (`"a b"~5`), and wildcard rules (`*`, `?`) match the engine version's parser.
|
|
54
|
+
- Every Elasticsearch query DSL object (`match`, `bool`, `should`, `must`, `filter`, `term`, `terms`) — verify removed/renamed clauses across major versions (e.g. `query_string` defaults, `term` vs `match` for `text` fields, mapping-type removal in ES 7+).
|
|
55
|
+
- Every Zoekt / Sourcegraph / OpenSearch / Solr query — verify dialect-specific operators and that the deployment has the relevant features enabled (e.g. ES `query_string` may be disabled for security).
|
|
56
|
+
- Every escaping rule for special characters in the DSL (`+ - && || ! ( ) { } [ ] ^ " ~ * ? : \ /`) — verify the producer escapes them before handing to the engine; flag any user-supplied input concatenated raw.
|
|
57
|
+
- Every analyzer assumption (whitespace, standard, keyword, ngram) — verify the index mapping matches what the query string assumes.
|
|
58
|
+
|
|
59
|
+
**B7. ORM vs raw SQL semantic differences**
|
|
60
|
+
- Every ORM filter expression — verify SQLAlchemy `.filter()` (Core/ORM expression) vs `.filter_by()` (kwargs only, equality only) usage; flag any `.filter_by()` passed boolean operators it does not support.
|
|
61
|
+
- Every Django Q expression, F expression, `Subquery`, `OuterRef`, `Exists` — verify the ORM version supports the construct (e.g. `FilteredRelation` is Django 2.0+, `Window` is 2.0+).
|
|
62
|
+
- Every lazy vs eager loading decision (`select_related`, `prefetch_related`, SQLAlchemy `joinedload`/`selectinload`/`subqueryload`) — verify N+1 risk and that the chosen strategy matches the dialect's join semantics.
|
|
63
|
+
- Every transaction context manager (`with session.begin():`, `@transaction.atomic`, `db.session.commit()`) — verify isolation level, autocommit behavior, and savepoint support match the declared DB version.
|
|
64
|
+
- Every raw SQL escape hatch (`session.execute(text(...))`, `cursor.execute()`, `RawSQL`) — verify it bypasses the ORM's dialect-rewriting and ensure the literal SQL still satisfies B2 (engine version) constraints.
|
|
65
|
+
- Every model field type (`JSONField`, `ArrayField`, `HStoreField`) — verify the backing DB version supports the underlying column type and operators.
|
|
66
|
+
|
|
67
|
+
## Cross-bucket questions to answer at the end
|
|
68
|
+
|
|
69
|
+
Q1: Are there any compatibility constraints that span two sub-buckets that single-bucket analysis would miss? (E.g. a query string that crosses B3 regex flavor + B6 search DSL escaping; a script that crosses B4 cmdlet syntax + B3 format-string interpolation; an ORM expression that crosses B7 lazy-load semantics + B2 dialect SQL.)
|
|
70
|
+
|
|
71
|
+
Q2: What's the worst engine-incompatibility hazard introduced by this artifact? Cite file:line. Rank by (a) likelihood the deployment hits the incompatible engine, (b) severity when it does, (c) detectability before production.
|
|
72
|
+
|
|
73
|
+
Q3: Where would a future engine/library upgrade most likely break a selector, query, cmdlet, or interpolated pattern in this artifact? Name the most fragile lines and the upgrade path that would break them (browser version bump, DB major upgrade, PowerShell edition change, ORM major version, search-engine major version).
|
|
74
|
+
|
|
75
|
+
## Output
|
|
76
|
+
|
|
77
|
+
Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket B1–B7, produce Shape A (≥1 finding) or Shape B (proof-of-absence with ≥3 distinct adversarial probes). Cross-bucket Q1–Q3 answers after the per-sub-bucket walk. Adversarial second pass (P1 quota): "assume your first pass missed at least 3 P1 incompatibility bugs across these 7 sub-buckets — find them." Open Questions section for ambiguities, undeclared engine targets, or cases where the declared support matrix is incomplete. Read-only. No edits, no commits.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Worked example: jl-cmd/claude-code-config PR #394
|
|
82
|
+
|
|
83
|
+
Audit jl-cmd/claude-code-config PR #394 for **Category B only** (selector / query / engine compatibility). Skip A, C–K. Sub-bucket forced-exhaustion mode: Category B is decomposed into 7 sub-buckets below. Each sub-bucket REQUIRES at least one Shape A finding OR exactly one Shape B proof-of-absence with **at least 3 adversarial probes** specific to that sub-bucket. A sub-bucket returning neither is a protocol gap.
|
|
84
|
+
|
|
85
|
+
PR: feat(scripts): add sweep-empty-dirs utility and scheduled-task installer
|
|
86
|
+
Head SHA: 62c9c169ee7a44824e5da25c4cf8b74fdca08a53
|
|
87
|
+
ID prefix: `find`.
|
|
88
|
+
|
|
89
|
+
````
|
|
90
|
+
## Diff (4 new files, all lines in scope)
|
|
91
|
+
|
|
92
|
+
### packages/claude-dev-env/scripts/sweep_empty_dirs.py
|
|
93
|
+
```python
|
|
94
|
+
#!/usr/bin/env python3
|
|
95
|
+
"""Delete empty directories older than 2 minutes under a given root."""
|
|
96
|
+
|
|
97
|
+
import argparse
|
|
98
|
+
import os
|
|
99
|
+
import sys
|
|
100
|
+
import time
|
|
101
|
+
|
|
102
|
+
from config.sweep_config import DEFAULT_AGE_SECONDS
|
|
103
|
+
from config.sweep_config import DEFAULT_POLL_INTERVAL
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _log_walk_error(os_error: OSError) -> None:
|
|
107
|
+
print(f"warning: cannot scan {os_error.filename} — {os_error.strerror}", file=sys.stderr)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def sweep(root: str, min_age_seconds: int) -> list[str]:
|
|
111
|
+
"""Remove empty directories under *root* older than *min_age_seconds*."""
|
|
112
|
+
|
|
113
|
+
now = time.time()
|
|
114
|
+
removed: list[str] = []
|
|
115
|
+
|
|
116
|
+
for each_directory_path, _, _ in os.walk(
|
|
117
|
+
root, onerror=_log_walk_error, topdown=False
|
|
118
|
+
):
|
|
119
|
+
try:
|
|
120
|
+
created = os.path.getctime(each_directory_path)
|
|
121
|
+
except OSError:
|
|
122
|
+
continue
|
|
123
|
+
if now - created >= min_age_seconds:
|
|
124
|
+
try:
|
|
125
|
+
os.rmdir(each_directory_path)
|
|
126
|
+
print(f"deleted: {each_directory_path}")
|
|
127
|
+
removed.append(each_directory_path)
|
|
128
|
+
except OSError:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
return removed
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
135
|
+
parser = argparse.ArgumentParser(description="Delete empty directories older than a given age.")
|
|
136
|
+
parser.add_argument("root", help="Root directory to scan")
|
|
137
|
+
parser.add_argument("--age", type=int, default=DEFAULT_AGE_SECONDS,
|
|
138
|
+
help=f"Minimum age in seconds (default: {DEFAULT_AGE_SECONDS} = 2 minutes)")
|
|
139
|
+
parser.add_argument("--once", action="store_true",
|
|
140
|
+
help="Single pass and exit instead of watching in a loop")
|
|
141
|
+
parser.add_argument("--interval", type=int, default=DEFAULT_POLL_INTERVAL,
|
|
142
|
+
help=f"Poll interval in seconds when looping (default: {DEFAULT_POLL_INTERVAL})")
|
|
143
|
+
return parser
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main() -> None:
|
|
147
|
+
parser = _build_parser()
|
|
148
|
+
arguments = parser.parse_args()
|
|
149
|
+
|
|
150
|
+
if not os.path.isdir(arguments.root):
|
|
151
|
+
print(f"error: not a directory: {arguments.root}", file=sys.stderr)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
if arguments.once:
|
|
155
|
+
sweep(arguments.root, arguments.age)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
print(f"watching {arguments.root} every {arguments.interval}s (age threshold: {arguments.age}s)")
|
|
159
|
+
try:
|
|
160
|
+
while True:
|
|
161
|
+
sweep(arguments.root, arguments.age)
|
|
162
|
+
time.sleep(arguments.interval)
|
|
163
|
+
except KeyboardInterrupt:
|
|
164
|
+
print("\nstopped.")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
main()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### packages/claude-dev-env/scripts/config/sweep_config.py
|
|
172
|
+
```python
|
|
173
|
+
"""Centralized timing configuration for sweep_empty_dirs."""
|
|
174
|
+
|
|
175
|
+
DEFAULT_AGE_SECONDS: int = 120
|
|
176
|
+
DEFAULT_POLL_INTERVAL: int = 30
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### packages/claude-dev-env/scripts/tests/test_sweep_empty_dirs.py
|
|
180
|
+
```python
|
|
181
|
+
"""Tests for sweep_empty_dirs.py"""
|
|
182
|
+
|
|
183
|
+
from __future__ import annotations
|
|
184
|
+
|
|
185
|
+
import datetime
|
|
186
|
+
import os
|
|
187
|
+
import subprocess
|
|
188
|
+
import sys
|
|
189
|
+
import tempfile
|
|
190
|
+
import time
|
|
191
|
+
from pathlib import Path
|
|
192
|
+
|
|
193
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
194
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
195
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
196
|
+
|
|
197
|
+
from sweep_empty_dirs import sweep # noqa: E402
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _set_creation_time_windows(path: str, timestamp: float) -> None:
|
|
201
|
+
dt = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
|
202
|
+
date_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
203
|
+
subprocess.run(
|
|
204
|
+
["powershell", "-Command",
|
|
205
|
+
f"(Get-Item '{path}').CreationTimeUtc = [DateTime]'{date_str}'"],
|
|
206
|
+
check=True, capture_output=True,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_deletes_empty_dir_older_than_threshold() -> None:
|
|
211
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
212
|
+
empty_dir = os.path.join(tmp, "old_empty")
|
|
213
|
+
os.mkdir(empty_dir)
|
|
214
|
+
_set_creation_time_windows(empty_dir, time.time() - 300)
|
|
215
|
+
removed = sweep(tmp, min_age_seconds=120)
|
|
216
|
+
assert empty_dir in removed
|
|
217
|
+
assert not os.path.isdir(empty_dir)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_skips_empty_dir_newer_than_threshold() -> None:
|
|
221
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
222
|
+
fresh_dir = os.path.join(tmp, "fresh_empty")
|
|
223
|
+
os.mkdir(fresh_dir)
|
|
224
|
+
removed = sweep(tmp, min_age_seconds=120)
|
|
225
|
+
assert fresh_dir not in removed
|
|
226
|
+
assert os.path.isdir(fresh_dir)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_deletes_nested_empty_dirs() -> None:
|
|
230
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
231
|
+
leaf = os.path.join(tmp, "parent", "child", "leaf")
|
|
232
|
+
os.makedirs(leaf)
|
|
233
|
+
_set_creation_time_windows(os.path.join(tmp, "parent"), time.time() - 300)
|
|
234
|
+
_set_creation_time_windows(os.path.join(tmp, "parent", "child"), time.time() - 300)
|
|
235
|
+
_set_creation_time_windows(leaf, time.time() - 300)
|
|
236
|
+
removed = sweep(tmp, min_age_seconds=120)
|
|
237
|
+
assert leaf in removed
|
|
238
|
+
assert os.path.join(tmp, "parent", "child") in removed
|
|
239
|
+
assert os.path.join(tmp, "parent") in removed
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_empty_root_does_not_crash() -> None:
|
|
243
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
244
|
+
_set_creation_time_windows(tmp, time.time() - 300)
|
|
245
|
+
sweep(tmp, min_age_seconds=120)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_skips_nonempty_dir() -> None:
|
|
249
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
250
|
+
nonempty_dir = os.path.join(tmp, "has_stuff")
|
|
251
|
+
os.mkdir(nonempty_dir)
|
|
252
|
+
Path(nonempty_dir, "keepme.txt").write_text("hello")
|
|
253
|
+
removed = sweep(tmp, min_age_seconds=0)
|
|
254
|
+
assert nonempty_dir not in removed
|
|
255
|
+
assert os.path.isdir(nonempty_dir)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### packages/claude-dev-env/scripts/Install-SweepEmptyDirs.ps1
|
|
259
|
+
```powershell
|
|
260
|
+
#!/usr/bin/env pwsh
|
|
261
|
+
param(
|
|
262
|
+
[Parameter(ParameterSetName = "install")]
|
|
263
|
+
[string]$Target,
|
|
264
|
+
|
|
265
|
+
[Parameter(ParameterSetName = "install")]
|
|
266
|
+
[int]$IntervalMinutes = 5,
|
|
267
|
+
|
|
268
|
+
[Parameter(ParameterSetName = "install")]
|
|
269
|
+
[int]$AgeSeconds = 120,
|
|
270
|
+
|
|
271
|
+
[Parameter(ParameterSetName = "remove")]
|
|
272
|
+
[switch]$Remove,
|
|
273
|
+
|
|
274
|
+
[Parameter(ParameterSetName = "status")]
|
|
275
|
+
[switch]$Status
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
$TaskName = "SweepEmptyDirs"
|
|
279
|
+
|
|
280
|
+
if ($Status) {
|
|
281
|
+
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
282
|
+
if (-not $task) {
|
|
283
|
+
Write-Host "STATUS: $TaskName is not registered."
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
Write-Host "STATUS: $TaskName is registered."
|
|
287
|
+
Write-Host " State: $($task.State)"
|
|
288
|
+
Write-Host " Actions:"
|
|
289
|
+
foreach ($action in $task.Actions) {
|
|
290
|
+
Write-Host " $($action.Execute) $($action.Arguments)"
|
|
291
|
+
}
|
|
292
|
+
Write-Host " Triggers:"
|
|
293
|
+
foreach ($trigger in $task.Triggers) {
|
|
294
|
+
Write-Host " $($trigger.Repetition.Interval) (starting $($trigger.StartBoundary))"
|
|
295
|
+
}
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if ($Remove) {
|
|
300
|
+
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
301
|
+
Write-Host "$TaskName removed."
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
$ScriptDir = Split-Path -Parent $PSCommandPath
|
|
306
|
+
$ScriptPath = Join-Path $ScriptDir "sweep_empty_dirs.py"
|
|
307
|
+
|
|
308
|
+
if (-not (Test-Path $ScriptPath)) {
|
|
309
|
+
Write-Error "sweep_empty_dirs.py not found at: $ScriptPath"
|
|
310
|
+
exit 1
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (-not $Target) {
|
|
314
|
+
Write-Error "Parameter -Target is required (the directory to watch)."
|
|
315
|
+
exit 1
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (-not (Test-Path $Target)) {
|
|
319
|
+
Write-Error "Target directory does not exist: $Target"
|
|
320
|
+
exit 1
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
$_py = Get-Command py -ErrorAction SilentlyContinue
|
|
324
|
+
$PythonPath = if ($_py) { $_py.Source } else { (Get-Command python).Source }
|
|
325
|
+
if (-not $PythonPath) {
|
|
326
|
+
Write-Error "Cannot find Python (py or python) on PATH."
|
|
327
|
+
exit 1
|
|
328
|
+
}
|
|
329
|
+
$Action = New-ScheduledTaskAction -Execute $PythonPath -Argument "$ScriptPath --once --age $AgeSeconds ""$Target"""
|
|
330
|
+
$Trigger = New-ScheduledTaskTrigger -Daily -At "00:00" -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes)
|
|
331
|
+
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
|
332
|
+
|
|
333
|
+
Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Force | Out-Null
|
|
334
|
+
Write-Host "$TaskName registered — runs every ${IntervalMinutes}min against '$Target' (age ≥ ${AgeSeconds}s)."
|
|
335
|
+
```
|
|
336
|
+
````
|
|
337
|
+
|
|
338
|
+
## Sub-buckets (each requires Shape A finding OR Shape B with ≥3 adversarial probes)
|
|
339
|
+
|
|
340
|
+
**B1. CSS / DOM selector vs target browser engine**
|
|
341
|
+
- The four PR #394 files contain no HTML, no CSS, no JavaScript, no DOM-rendering or shadow-DOM code, no browser-test runner.
|
|
342
|
+
- Shape B proof-of-absence expected. Adversarial probes must each verify a distinct DOM/CSS dimension:
|
|
343
|
+
- Probe B1.a: confirm zero CSS selectors anywhere in the four files — no `:has()`, `:is()`, `:where()`, no attribute selectors, no `::part()`, no `::slotted()`. Walk every string literal in `sweep_empty_dirs.py`, `sweep_config.py`, `test_sweep_empty_dirs.py`, `Install-SweepEmptyDirs.ps1`.
|
|
344
|
+
- Probe B1.b: confirm zero references to `document.querySelector` / jQuery `$()` / `getElementById` / shadow-root `attachShadow` / Selenium `By.css_selector` / Playwright `page.locator`. The Python test harness uses `subprocess.run`, not a browser driver.
|
|
345
|
+
- Probe B1.c: confirm zero rendered-output assertions — no HTML snapshot fixtures, no `outerHTML`/`innerHTML` comparisons, no DOM-tree equality checks. The test file at `tests/test_sweep_empty_dirs.py` lines 35-76 only asserts on filesystem state (`os.path.isdir`, list membership of `removed`).
|
|
346
|
+
|
|
347
|
+
**B2. SQL syntax vs database version**
|
|
348
|
+
- The four PR #394 files contain no database access, no ORM, no migrations, no SQL string literals.
|
|
349
|
+
- Shape B proof-of-absence expected. Adversarial probes must each verify a distinct DB dimension:
|
|
350
|
+
- Probe B2.a: confirm `sweep_empty_dirs.py` imports only `argparse`, `os`, `sys`, `time` plus two names from `config.sweep_config` (lines 4-10). No `sqlite3`, no `psycopg2`, no SQLAlchemy, no Django ORM.
|
|
351
|
+
- Probe B2.b: confirm zero SQL keyword tokens (`SELECT`, `INSERT`, `UPDATE`, `DELETE`, `WITH`, `JOIN`, `MERGE`, `WINDOW`) appear in any string literal across all four files. No CTEs, no window functions, no JSON operators (`->`, `->>`, `@>`).
|
|
352
|
+
- Probe B2.c: confirm no migration directory or schema file is added by the PR — the four added files are `sweep_empty_dirs.py`, `config/sweep_config.py`, `tests/test_sweep_empty_dirs.py`, `Install-SweepEmptyDirs.ps1`. None is a `*.sql`, `migrations/*.py`, `alembic/`, or `prisma/schema.prisma`.
|
|
353
|
+
|
|
354
|
+
**B3. Regex syntax / format-string flavor vs engine — Python f-string → PowerShell**
|
|
355
|
+
- The test helper `_set_creation_time_windows` at `tests/test_sweep_empty_dirs.py` lines 20-27 (def at line 20, body lines 21-27) builds a PowerShell command via Python f-string interpolation. The critical line is `tests/test_sweep_empty_dirs.py:25` — `f"(Get-Item '{path}').CreationTimeUtc = [DateTime]'{date_str}'"` — and the surrounding `subprocess.run(["powershell", "-Command", ...], check=True, capture_output=True)` call at lines 23-27. The interpolated `path` and `date_str` values pass through Python f-string substitution → argv list → Windows `CreateProcess` argv → `powershell.exe` `-Command` parser → PowerShell single-quoted string-literal parser → `[DateTime]` cast.
|
|
356
|
+
- Adversarial probe B3.a (single-quote injection): PowerShell single-quoted string literals (`'…'`) do not honor backslash escapes, but `''` is the embedded-single-quote escape. If `path` contains `'` (e.g. `won't`), the `f"...'{path}'..."` substitution at `tests/test_sweep_empty_dirs.py:25` produces a PowerShell command with an unbalanced quote — `(Get-Item 'won't').CreationTimeUtc = ...` — which terminates the literal early and leaves `t')` as a parse error. The Python helper does not call any escape function. Verify whether this breaks `test_deletes_empty_dir_older_than_threshold` (line 30) when run from a tmp path containing an apostrophe (Windows usernames with apostrophes, e.g. `O'Brien`, are legal).
|
|
357
|
+
- Adversarial probe B3.b (`$(...)` subexpression hazard): PowerShell single-quoted strings DO NOT expand `$(...)`, `$variable`, or backtick escapes — they are inert. But the OUTER PowerShell `-Command` payload is a double-quoted-equivalent context where the entire Python f-string sits as a single argv entry. Verify that the `[DateTime]` literal subsequently invoked at `tests/test_sweep_empty_dirs.py:25` does not allow an embedded `$(...)` to escape — i.e. confirm that even if `date_str` were attacker-influenced, the `[DateTime]'…'` cast parses within single quotes only and never re-evaluates the contents through PowerShell's expression engine.
|
|
358
|
+
- Adversarial probe B3.c (backtick escape): backticks (`` ` ``) are PowerShell's escape character inside double-quoted strings but inert inside single-quoted strings. The f-string at `tests/test_sweep_empty_dirs.py:25` wraps `{path}` in single quotes, so backticks in `path` should pass through literally — verify against PowerShell 5.1 and PowerShell 7+, both invoked via `["powershell", ...]` (which on Windows resolves to PS 5.1 by default — see B4).
|
|
359
|
+
- Adversarial probe B3.d (`[DateTime]` parse format): `dt.strftime("%Y-%m-%d %H:%M:%S")` at `tests/test_sweep_empty_dirs.py:22` produces e.g. `2026-05-08 14:23:45`. PowerShell's `[DateTime]'<string>'` cast parses via `DateTime.Parse`, which is **culture-sensitive** by default. On a Windows machine where `Get-Culture` is e.g. `de-DE` (`yyyy-MM-dd` is unambiguous, but the time separator `:` vs. month-day reordering matters for some locales), `DateTime.Parse` may misinterpret the string or throw `FormatException`. The Python helper does not pin a culture (no `[CultureInfo]::InvariantCulture` use). Verify whether the same parse succeeds on PS 5.1 and PS 7+ across at least `en-US`, `de-DE`, `ja-JP`.
|
|
360
|
+
|
|
361
|
+
**B4. Shell / CLI / cmdlet syntax vs runtime version — ScheduledTasks module + powershell.exe vs pwsh**
|
|
362
|
+
- `Install-SweepEmptyDirs.ps1` declares the interpreter via shebang `#!/usr/bin/env pwsh` at line 1 (PowerShell 7+), but invokes the `ScheduledTasks` module: `Get-ScheduledTask` at `Install-SweepEmptyDirs.ps1:22`, `Unregister-ScheduledTask` at line 41, `New-ScheduledTaskAction` at line 70, `New-ScheduledTaskTrigger` at line 71, `New-ScheduledTaskSettingsSet` at line 72, `Register-ScheduledTask` at line 74. The `ScheduledTasks` module is a Windows-native CDXML module (introduced in Windows 8 / Server 2012, PS 3.0); on PS 7+ it is reachable on Windows via the WindowsPowerShell Compatibility shim auto-load, but absent entirely on Linux/macOS PS 7+.
|
|
363
|
+
- Probe B4.a (shebang vs platform): `#!/usr/bin/env pwsh` at `Install-SweepEmptyDirs.ps1:1` says "run me on PowerShell 7+", but the cmdlets at lines 22, 41, 70-74 only resolve on Windows. Verify whether the script declares a `#Requires -RunAsAdministrator` / `#Requires -Version` / `#Requires -PSEdition Desktop` / OS guard. It does not. Cross-check Microsoft docs: https://learn.microsoft.com/powershell/scripting/lang-spec/chapter-13#1310-the-requires-statement.
|
|
364
|
+
- Probe B4.b (`-Daily` parameter set + `-RepetitionInterval`): `New-ScheduledTaskTrigger -Daily -At "00:00" -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes)` at `Install-SweepEmptyDirs.ps1:71`. Per https://learn.microsoft.com/powershell/module/scheduledtasks/new-scheduledtasktrigger, `-RepetitionInterval` belongs to the `-Once` parameter set, NOT the `-Daily` parameter set. Confirm whether the cmdlet errors with `Parameter set cannot be resolved using the specified named parameters` on both PS 5.1 (ScheduledTasks v1.0.0.0 shipped with Windows 8.1 / Server 2012 R2) and PS 7+ (same module via Windows Compatibility).
|
|
365
|
+
- Probe B4.c (`New-ScheduledTaskSettingsSet` switches): `-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable` at `Install-SweepEmptyDirs.ps1:72`. Verify all three switches exist in the ScheduledTasks v1.0.0.0 module baseline (Windows 8.1 / Server 2012 R2). Microsoft docs: https://learn.microsoft.com/powershell/module/scheduledtasks/new-scheduledtasksettingsset.
|
|
366
|
+
- Probe B4.d (`powershell` vs `pwsh` argv resolution): the Python test helper at `tests/test_sweep_empty_dirs.py:24` hardcodes `["powershell", "-Command", …]`. On Windows, `powershell` resolves to **Windows PowerShell 5.1** (`%WinDir%\System32\WindowsPowerShell\v1.0\powershell.exe`), NOT to PowerShell 7+ (`pwsh.exe`). The shebang on the installer says PS 7+, but the test helper runs PS 5.1. Two different runtimes for one PR. Verify the `[DateTime]'<string>'` cast (B3.d) and `Get-Item` semantics behave identically across both. Cross-check https://learn.microsoft.com/powershell/scripting/install/migrating-from-windows-powershell-51-to-powershell-7.
|
|
367
|
+
- Probe B4.e (parameter-set ambiguity with no default): the `param(...)` block at `Install-SweepEmptyDirs.ps1:2-17` declares three parameter sets (`install`, `remove`, `status`) but the script lacks `[CmdletBinding(DefaultParameterSetName=…)]`. With no default set, a no-argument invocation triggers `Parameter set cannot be resolved` on PS 5.1 and PS 7+. The exit code and error stream shape differ subtly across the two versions — verify whether the user's downstream automation can disambiguate.
|
|
368
|
+
- Probe B4.f (`Get-Command` -ErrorAction asymmetry): `$_py = Get-Command py -ErrorAction SilentlyContinue` at `Install-SweepEmptyDirs.ps1:64` swallows the not-found error, but `(Get-Command python).Source` at line 65 has no `-ErrorAction SilentlyContinue`. If `python` is not on PATH, the line throws a terminating error before the `if (-not $PythonPath)` check at line 66 can fire. Verify the version-specific shape of the resulting error stream — PS 5.1 emits a `CommandNotFoundException`, PS 7+ emits the same with a slightly different formatted prefix; downstream automation that grep-matches the error text breaks across versions.
|
|
369
|
+
|
|
370
|
+
**B5. JSON path / XPath / structural query vs library**
|
|
371
|
+
- The four PR #394 files contain no JSON path, no XPath, no JsonPointer, no structural query expressions.
|
|
372
|
+
- Shape B proof-of-absence expected. Adversarial probes must each verify a distinct structural-query dimension:
|
|
373
|
+
- Probe B5.a: confirm no `jq` invocations anywhere — neither `subprocess.run(["jq", ...])` in Python nor `jq` cmdlets nor inline `--jq` flags in PowerShell. The Python `subprocess.run` at `tests/test_sweep_empty_dirs.py:23-27` invokes `powershell`, not `jq`.
|
|
374
|
+
- Probe B5.b: confirm no `import jsonpath_ng`, `import lxml`, `import xml.etree`, no `from xml import dom`, no `xpath.compile(...)` calls. `sweep_empty_dirs.py` imports only `argparse`, `os`, `sys`, `time` (lines 4-7) plus the config module.
|
|
375
|
+
- Probe B5.c: confirm no JSON-pointer (`/foo/bar`) string literals, no JsonPath-style `$.foo[?(@.bar)]` patterns, no XPath `/html/body//div[@class='x']` patterns in any string in the four files. Walk every f-string and string literal.
|
|
376
|
+
|
|
377
|
+
**B6. Search query DSL vs engine**
|
|
378
|
+
- The four PR #394 files contain no search-engine queries, no Lucene/Elasticsearch/Zoekt/OpenSearch DSL.
|
|
379
|
+
- Shape B proof-of-absence expected. Adversarial probes must each verify a distinct search-DSL dimension:
|
|
380
|
+
- Probe B6.a: confirm no HTTP calls to `/_search`, `/_msearch`, `/_count`, `/_analyze` endpoints — `sweep_empty_dirs.py` does not import `requests`, `urllib`, `httpx`, `aiohttp`. Pure stdlib + local config.
|
|
381
|
+
- Probe B6.b: confirm no Lucene-syntax fragments — no `field:value`, no `+required -excluded`, no fuzzy `term~2`, no proximity `"a b"~5`. The only colon-bearing literals in the diff are PowerShell hash separators (`$($action.Execute) $($action.Arguments)` at `Install-SweepEmptyDirs.ps1:31`) and the time literal `"00:00"` at line 71 — neither is a search-DSL fragment.
|
|
382
|
+
- Probe B6.c: confirm no `match`/`bool`/`should`/`must`/`filter` clause objects appearing as Python dict literals or JSON. No Elasticsearch DSL anywhere.
|
|
383
|
+
|
|
384
|
+
**B7. ORM vs raw SQL semantic differences**
|
|
385
|
+
- The four PR #394 files contain no ORM usage, no raw SQL, no transaction context, no DB session.
|
|
386
|
+
- Shape B proof-of-absence expected. Adversarial probes must each verify a distinct ORM dimension:
|
|
387
|
+
- Probe B7.a: confirm no SQLAlchemy / Django ORM / Peewee / Tortoise / Pony imports anywhere — `sweep_empty_dirs.py` lines 4-10 are limited to stdlib `argparse, os, sys, time` plus the local `config.sweep_config` module.
|
|
388
|
+
- Probe B7.b: confirm no `.filter()`, `.filter_by()`, `Q()`, `F()`, `Subquery(...)`, `select()`, `insert()` ORM-style calls in any file. The closest expression-tree call is `os.walk(root, onerror=_log_walk_error, topdown=False)` at `sweep_empty_dirs.py:23-25` — that is a stdlib filesystem walk, not an ORM query.
|
|
389
|
+
- Probe B7.c: confirm no transaction context manager (`with session.begin():`, `@transaction.atomic`, `db.session.commit()`) and no lazy-vs-eager-load distinction anywhere. The only context managers in the diff are `tempfile.TemporaryDirectory()` calls in test bodies at `tests/test_sweep_empty_dirs.py:31, 41, 50, 63, 69`.
|
|
390
|
+
|
|
391
|
+
## Cross-bucket questions to answer at the end
|
|
392
|
+
|
|
393
|
+
Q1: Are there any compatibility constraints that span two sub-buckets that single-bucket analysis would miss? Specifically, does the f-string-built PowerShell command at `tests/test_sweep_empty_dirs.py:25` cross B3 (Python f-string interpolation safety + PowerShell `[DateTime]` parse-format flavor) and B4 (PS 5.1 vs PS 7+ runtime, hard-coded `powershell` argv at line 24) such that the same line is exposed to both axes? Does the trigger-cmdlet call at `Install-SweepEmptyDirs.ps1:71` cross B4 (parameter-set validity for `-Daily` + `-RepetitionInterval`) and an A-category contract (already audited separately) such that a Category B finding would silently subsume a Category A one?
|
|
394
|
+
|
|
395
|
+
Q2: What's the worst engine-incompatibility hazard introduced by this PR? Cite file:line. Candidates: (a) the `-Daily -RepetitionInterval` parameter-set mismatch at `Install-SweepEmptyDirs.ps1:71` per Microsoft docs; (b) the `[DateTime]'<string>'` culture-sensitive parse at `tests/test_sweep_empty_dirs.py:25`; (c) the `powershell` vs `pwsh` runtime split between `tests/test_sweep_empty_dirs.py:24` and `Install-SweepEmptyDirs.ps1:1`; (d) the bare `(Get-Command python).Source` at `Install-SweepEmptyDirs.ps1:65` lacking `-ErrorAction SilentlyContinue`.
|
|
396
|
+
|
|
397
|
+
Q3: Where would a future engine/library upgrade most likely break a cmdlet, command line, or interpolated pattern in this diff? Name the line(s) most fragile. Candidates to evaluate: (a) the `ScheduledTasks` module being relocated or deprecated in a future Windows / PS 7+ release (lines 22, 41, 70-74 of `Install-SweepEmptyDirs.ps1`); (b) the f-string + `[DateTime]'…'` cast at `tests/test_sweep_empty_dirs.py:25` if a future PS release tightens culture parsing; (c) the hard-coded `["powershell", ...]` argv at `tests/test_sweep_empty_dirs.py:24` if Windows 12 ships `pwsh` as the default and removes Windows PowerShell 5.1.
|
|
398
|
+
|
|
399
|
+
## Output
|
|
400
|
+
|
|
401
|
+
Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket B1–B7, produce Shape A or Shape B (with ≥3 probes). Cross-bucket Q1–Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 P1 incompatibility bugs across these 7 sub-buckets — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
|