claude-dev-env 8.0.0 → 8.2.0
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/.agents/agents/clean-coder.md +4 -4
- package/.agents/agents/code-quality-agent.md +4 -4
- package/.agents/agents/plan-packet-validator.md +2 -2
- package/.agents/agents/session-advisor.md +1 -1
- package/.agents/skills/cert-classification-rule/SKILL.md +3 -3
- package/.agents/skills/e-code-review/SKILL.md +2 -2
- package/.agents/skills/e-code-review/reference/fix.md +1 -1
- package/.agents/skills/e-code-review/reference/loop.md +1 -1
- package/.agents/skills/e-code-review/reference/medium.md +4 -4
- package/.agents/skills/e-code-review/reference/xhigh.md +5 -5
- package/.agents/skills/everything-search/SKILL.md +3 -3
- package/.agents/skills/issue-tracker/SKILL.md +1 -1
- package/.agents/skills/orchestrator/SKILL.md +3 -3
- package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +1 -1
- package/.agents/skills/orchestrator/reference/executor-consult-block.md +1 -1
- package/.agents/skills/orchestrator-refresh/SKILL.md +2 -2
- package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +96 -0
- package/.agents/skills/plugin-eval-standalone-skill/reference/graders-and-commands.md +108 -0
- package/.agents/skills/privacy-hygiene/SKILL.md +5 -6
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +5 -6
- package/.agents/skills/repairing-hook-boundaries/SKILL.md +1 -1
- package/.agents/skills/syncing-submodules/SKILL.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +9 -7
- package/.agents/skills/team-advisor/reference/advisor-docs-review.md +28 -1
- package/.agents/skills/usage-pause/SKILL.md +3 -3
- package/_shared/advisor/advisor-protocol.md +7 -3
- package/_shared/advisor/reference/advisor-block.md +8 -1
- package/audit-rubrics/audit-categories.json +0 -4
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +2 -2
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -2
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +1 -1
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +5 -5
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +5 -5
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +3 -3
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +3 -3
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +3 -3
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +1 -1
- package/audit-rubrics/prompts/category-e-dead-code.md +5 -5
- package/audit-rubrics/prompts/category-f-silent-failures.md +12 -12
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +4 -4
- package/audit-rubrics/prompts/category-i-concurrency.md +6 -6
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +6 -29
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +1 -1
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +7 -7
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +3 -3
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +2 -2
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/ever-shipped-skills.test.mjs +4 -0
- package/docs/BDD_SCENARIO_QUALITY.md +2 -2
- package/docs/CODE_RULES.md +7 -7
- package/docs/TEST_QUALITY.md +1 -1
- package/docs/nas-ssh-invocation.md +1 -1
- package/docs/references/advisor-tool.md +2 -2
- package/docs/references/prose-style-enforcement.md +1 -1
- package/docs/references/team-advisor-skill.md +2 -2
- package/docs/references/weak-executor-advisor.md +1 -1
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/package.json +1 -1
- package/rules/anti-corollary-tests.md +3 -1
- package/rules/archiving-agent-config.md +67 -0
- package/rules/asd-ste100-language.md +1 -1
- package/rules/ci-owns-the-gate.md +15 -0
- package/rules/cleanup-temp-files.md +1 -1
- package/rules/code-standards.md +2 -2
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/explore-thoroughly.md +2 -2
- package/rules/failure-blast-radius.md +9 -8
- package/rules/git-workflow.md +7 -4
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/nas-ssh-invocation.md +1 -1
- package/rules/research-mode.md +3 -1
- package/rules/retired-hook-prose.md +10 -2
- package/rules/testing.md +3 -3
- package/rules/verify-before-asking.md +3 -0
- package/rules/verify-runtime-state.md +2 -2
- package/scripts/active_capability_references.py +74 -19
- package/scripts/banned_prose_words.py +159 -0
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +12 -10
- package/scripts/dev_env_scripts_constants/banned_prose_word_constants.py +95 -0
- package/scripts/policy_lint/adapter_retired_hook_prose.py +17 -7
- package/scripts/policy_lint/adapters.py +6 -6
- package/scripts/policy_lint/config/constants.py +8 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_active_capability_references.py +100 -33
- package/scripts/tests/test_banned_prose_words.py +103 -0
- package/scripts/tests/test_codex_compat_materializer.py +3 -3
- package/scripts/tests/test_engine.py +1 -1
- package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +46 -0
- package/scripts/tests/test_rules.py +12 -1
- package/system-prompts/software-engineer.xml +7 -7
|
@@ -25,7 +25,7 @@ Repeat for every section in scope.
|
|
|
25
25
|
**F1. Catch-all except clauses**
|
|
26
26
|
- Locate every bare `except:`, `except Exception:`, `except BaseException:` (and language-equivalents: PowerShell `try { } catch { }` with no type filter, JS `catch (e) {}`, Go `_ = err`, Rust `let _ = result`).
|
|
27
27
|
- For each, classify the body: `pass` / `continue` / log-only / re-raise. Only re-raise (or a documented benign-case-with-trace) is compliant.
|
|
28
|
-
- For the swallowing variants, enumerate which exception subclasses the protected call
|
|
28
|
+
- For the swallowing variants, enumerate which exception subclasses the protected call raises and identify which are failures vs benign known cases (e.g., for `os.rmdir`: `FileNotFoundError`, `PermissionError`, `NotADirectoryError`, `OSError(ENOTEMPTY)`, `OSError(EBUSY)` — only `ENOTEMPTY` is benign).
|
|
29
29
|
- Check for asymmetry: do two error handlers in the same module disagree on whether to log? A handler that logs to stderr next to one that silently swallows is a Category F smell even when each is locally defensible.
|
|
30
30
|
- Check for double-swallow: does an outer callback (e.g. `os.walk(..., onerror=...)`) already report errors that an inner `except` then swallows again?
|
|
31
31
|
|
|
@@ -37,7 +37,7 @@ Repeat for every section in scope.
|
|
|
37
37
|
|
|
38
38
|
**F3. Default fallback values masking failure**
|
|
39
39
|
- `dict.get(key, default)` where the absence of the key is itself a bug.
|
|
40
|
-
- Stale-payload-key shape: for each external-input dict (`*_payload`, event, request, parsed-JSON body), list every string-literal key read from it. Check each key against the contract the module's docstring and its other reads set up — a payload whose other reads bind to same-named variables and whose docstring names a key set is an established contract. Flag a lone read of a key outside that contract: it resolves to the `.get` default on every
|
|
40
|
+
- Stale-payload-key shape: for each external-input dict (`*_payload`, event, request, parsed-JSON body), list every string-literal key read from it. Check each key against the contract the module's docstring and its other reads set up — a payload whose other reads bind to same-named variables and whose docstring names a key set is an established contract. Flag a lone read of a key outside that contract: it resolves to the `.get` default on every payload and silently records an empty value into the field it feeds. A key consumed inline with a meaningful default (`resolve(payload.get("cwd", "."))`) is legitimate; the flag is the dropped key whose value the consuming field needs but never receives.
|
|
41
41
|
- `or default` short-circuits hiding `None` returns from fallible calls.
|
|
42
42
|
- `getattr(obj, attr, default)` masking `AttributeError` from the wrong object type.
|
|
43
43
|
- argparse `default=...` for values that should fail-loud when absent.
|
|
@@ -65,7 +65,7 @@ Repeat for every section in scope.
|
|
|
65
65
|
- `os.write`, `os.read`, `socket.send`, `socket.recv` whose short-write/short-read return values are discarded.
|
|
66
66
|
- File-handle methods (`f.write`) whose returned byte count is not checked when partial writes are possible (Windows pipes, network sockets).
|
|
67
67
|
- PowerShell pipelines piped to `| Out-Null` that may be hiding a non-terminating warning.
|
|
68
|
-
- Functions that return a status object alongside a side effect — verify the side effect's success is the
|
|
68
|
+
- Functions that return a status object alongside a side effect — verify the side effect's success is the contract and the discarded return is informational only.
|
|
69
69
|
- Exception-only contracts (e.g. `os.rmdir` returns `None`, raises on failure) belong under F1, not F6 — note the cross-reference rather than double-booking.
|
|
70
70
|
|
|
71
71
|
**F7. PowerShell error-suppression patterns**
|
|
@@ -74,12 +74,12 @@ Repeat for every section in scope.
|
|
|
74
74
|
- `2>$null`, `*>$null`, `$ErrorActionPreference = 'SilentlyContinue'` mutations.
|
|
75
75
|
- `$?` not consulted after a native-binary call where exit code matters.
|
|
76
76
|
- The opposite hazard: a fallback path that drops `-ErrorAction SilentlyContinue` from a `Get-Command` (or similar) call where the `$null` was the entire contract — produces a terminating error on the path that was supposed to be silent-then-handled-by-the-next-line.
|
|
77
|
-
- Semantic-success-but-functional-failure: `Get-Command` finds the Microsoft Store stub `python.exe` whose `.Source` resolves but whose execution opens the Store. The cmdlet did not error, but the resolved interpreter cannot run code. Verify whether scripts validate that resolved paths
|
|
78
|
-
- Mismatched user-facing messages: a `-Remove` / `-Uninstall` flow that prints "X removed." regardless of whether anything was
|
|
77
|
+
- Semantic-success-but-functional-failure: `Get-Command` finds the Microsoft Store stub `python.exe` whose `.Source` resolves but whose execution opens the Store. The cmdlet did not error, but the resolved interpreter cannot run code. Verify whether scripts validate that resolved paths do their job.
|
|
78
|
+
- Mismatched user-facing messages: a `-Remove` / `-Uninstall` flow that prints "X removed." regardless of whether anything was unregistered.
|
|
79
79
|
|
|
80
80
|
**F8. Test-level swallowing**
|
|
81
81
|
- `try/except` / `try/catch` inside a test body that catches and logs instead of asserting. Tests must fail loudly when their target raises.
|
|
82
|
-
- `pytest.warns` used where `pytest.raises` was the
|
|
82
|
+
- `pytest.warns` used where `pytest.raises` was the contract.
|
|
83
83
|
- Tests asserting only on the success path with no negative-space coverage of the swallowed-error branch.
|
|
84
84
|
- Tests whose name encodes an assertion ("does not crash") but whose body asserts nothing — the test passes incidentally and would also pass if the function silently failed every operation.
|
|
85
85
|
- Coverage gaps for known F1 / F2 / F6 swallowing branches: if the audited code has `except OSError: pass`, is there a test that exercises that branch and verifies the post-conditions (e.g. directory still present, no log line emitted)?
|
|
@@ -88,12 +88,12 @@ Repeat for every section in scope.
|
|
|
88
88
|
**F9. Gate-validator self-defeat via parse-failure swallow**
|
|
89
89
|
- Locate every `ast.parse` / `tokenize` / `json.loads` / `yaml.safe_load` call inside gate / validator / hook code that parses the audited input as code or structured data.
|
|
90
90
|
- For each, classify the catch branch: does it return `[]` / `None` / `True` (a clean signal) on parse failure? A clean-signal default masks the gate being broken — the check fires zero findings whenever the parse fails.
|
|
91
|
-
- Trace what the dispatcher
|
|
92
|
-
- Adversarial probes: (a) feed the check a syntactically incomplete fragment and confirm whether the catch branch returns a clean signal; (b) compare the content surface the test fixtures use against the content surface the dispatcher supplies at runtime; (c) verify the catch branch distinguishes "input is
|
|
91
|
+
- Trace what the dispatcher feeds the check: when the dispatcher passes a partial fragment (e.g., the Edit tool's `new_string` rather than the full file content), the parse fails on every Edit and the check is dead in production while its tests pass against full-file fixtures.
|
|
92
|
+
- Adversarial probes: (a) feed the check a syntactically incomplete fragment and confirm whether the catch branch returns a clean signal; (b) compare the content surface the test fixtures use against the content surface the dispatcher supplies at runtime; (c) verify the catch branch distinguishes "input is clean" from "input could not be parsed" — a single clean-signal return for both is an F9 finding.
|
|
93
93
|
|
|
94
94
|
**F10. Guard helper returns success-default on unverifiable input**
|
|
95
95
|
- Locate every guard helper / predicate / classifier that decides whether to allow or block, and inspect the path taken when it cannot validate the input shape (missing positional args, wrong arity, unexpected `None`, malformed payload).
|
|
96
|
-
- Flag any guard that short-circuits to `True` / `Ok` / the success sentinel on unverifiable input. Guards must default-deny when they cannot verify; default-allow on unverifiable input lets
|
|
96
|
+
- Flag any guard that short-circuits to `True` / `Ok` / the success sentinel on unverifiable input. Guards must default-deny when they cannot verify; default-allow on unverifiable input lets violations through downstream.
|
|
97
97
|
- Adversarial probes: (a) construct an input that fails the guard's shape check and confirm whether the guard returns allow or deny; (b) supply `None` / missing args / wrong arity and trace the return; (c) verify the guard's "cannot verify" branch is distinct from its "verified clean" branch — collapsing both into a success return is an F10 finding.
|
|
98
98
|
|
|
99
99
|
## Cross-bucket questions to answer at the end
|
|
@@ -367,7 +367,7 @@ Write-Host "$TaskName registered — runs every ${IntervalMinutes}min against '$
|
|
|
367
367
|
|
|
368
368
|
**F1. Catch-all except clauses**
|
|
369
369
|
- `sweep_empty_dirs.py` line 28-29: `except OSError: continue` absorbs `os.path.getctime` failures inside the `os.walk` bottom-up loop. Verify this is the documented "file vanished mid-walk" case and not masking a permission denial that leaves stale empty dirs forever.
|
|
370
|
-
- `sweep_empty_dirs.py` line 35-36: `except OSError: pass` absorbs `os.rmdir` failures. The intent comment in the rubric says "silently skips non-empty dirs". Enumerate the OSError subclasses `os.rmdir`
|
|
370
|
+
- `sweep_empty_dirs.py` line 35-36: `except OSError: pass` absorbs `os.rmdir` failures. The intent comment in the rubric says "silently skips non-empty dirs". Enumerate the OSError subclasses `os.rmdir` raises: `FileNotFoundError`, `PermissionError`, `NotADirectoryError`, `OSError(ENOTEMPTY)`, `OSError(EBUSY)`. The non-empty case (ENOTEMPTY) is the *only* benign one — every other subclass is a failure being swallowed without logging.
|
|
371
371
|
- Critical asymmetry: `_log_walk_error` (line 14-15) DOES log walk-level errors to stderr, but the inner `except OSError: pass` at line 35-36 logs nothing on the rmdir path. The PR ships two error handlers that disagree on whether errors deserve a stderr trace.
|
|
372
372
|
- The walk-level handler `_log_walk_error` is wired through `os.walk(root, onerror=_log_walk_error, topdown=False)` at lines 22-24. Verify whether the `except OSError: continue` at line 28-29 ever fires for an error `os.walk`'s `onerror` already reported, producing a double-swallow with the second handler being silent.
|
|
373
373
|
- `main()` at lines 65-83 has no top-level `try/except` around `sweep(...)`, so unhandled errors inside the loop body (other than KeyboardInterrupt at line 81) would crash the watcher. Verify whether any silent-failure within `sweep()` could leave the watcher running but no longer making progress (e.g., sweep returns empty list every cycle because every rmdir fails).
|
|
@@ -410,9 +410,9 @@ Write-Host "$TaskName registered — runs every ${IntervalMinutes}min against '$
|
|
|
410
410
|
- `Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue` at line 22 of installer — null is captured into `$task`, then explicitly checked by `if (-not $task)` at line 23. The null absorption is handled. Compliant.
|
|
411
411
|
- `Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue` at line 42 of installer — the `-ErrorAction SilentlyContinue` makes the absence-of-task case non-terminating. Verify intent: should `-Remove` against a never-installed task be a success (current) or a clearly-flagged no-op? The `Write-Host "$TaskName removed."` at line 43 will print regardless, mis-stating the outcome.
|
|
412
412
|
- ⚠️ `Get-Command py -ErrorAction SilentlyContinue` at line 65 immediately followed by `.Source` access at line 66: `$_py = Get-Command py -ErrorAction SilentlyContinue` returns `$null` when `py` is absent; the line 66 expression `$_py.Source` would throw on `$null`, BUT the `if ($_py)` guard short-circuits to the `else` branch first. The `py`-side path is safe.
|
|
413
|
-
- ⚠️⚠️ The fallback at line 66 is `(Get-Command python).Source` — *missing* `-ErrorAction SilentlyContinue`. When neither `py` nor `python` is on PATH, `Get-Command python` raises a terminating `CommandNotFoundException`, which propagates past the `if (-not $PythonPath)` guard at line 67 entirely. The intended diagnostic message at line 68 (`"Cannot find Python (py or python) on PATH."`) is never reached. This is the OPPOSITE F7 hazard: the script is *loud* on a failure path that was *intended to be silent-then-handled-by-line-67*. The asymmetry between line 65 (suppressed) and line 66's else-branch (not suppressed) is a
|
|
413
|
+
- ⚠️⚠️ The fallback at line 66 is `(Get-Command python).Source` — *missing* `-ErrorAction SilentlyContinue`. When neither `py` nor `python` is on PATH, `Get-Command python` raises a terminating `CommandNotFoundException`, which propagates past the `if (-not $PythonPath)` guard at line 67 entirely. The intended diagnostic message at line 68 (`"Cannot find Python (py or python) on PATH."`) is never reached. This is the OPPOSITE F7 hazard: the script is *loud* on a failure path that was *intended to be silent-then-handled-by-line-67*. The asymmetry between line 65 (suppressed) and line 66's else-branch (not suppressed) is a bug.
|
|
414
414
|
- No `2>$null`, `*>$null`, or `$ErrorActionPreference` mutation in this script. No `$?` consultation either, but each cmdlet either has explicit error handling or uses default error action.
|
|
415
|
-
- Adversarial probe — does `Get-Command python` find the Microsoft Store stub on a fresh Windows 11 install? The stub's `.Source` resolves to `%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe`, which when invoked opens the Store rather than running Python. The installer would register a scheduled task whose `-Execute` path opens the Microsoft Store every IntervalMinutes — a silent semantic failure even though `Get-Command` did not error. Verify whether the installer needs to validate that the resolved interpreter
|
|
415
|
+
- Adversarial probe — does `Get-Command python` find the Microsoft Store stub on a fresh Windows 11 install? The stub's `.Source` resolves to `%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe`, which when invoked opens the Store rather than running Python. The installer would register a scheduled task whose `-Execute` path opens the Microsoft Store every IntervalMinutes — a silent semantic failure even though `Get-Command` did not error. Verify whether the installer needs to validate that the resolved interpreter executes Python.
|
|
416
416
|
|
|
417
417
|
**F8. Test-level swallowing**
|
|
418
418
|
- `test_sweep_empty_dirs.py` has no try/except inside any test body. All tests use direct asserts (lines 38-39, 47-49, 64-67, 76-78). No `pytest.warns`, no `pytest.raises` either — failure paths are not exercised at all.
|
|
@@ -89,7 +89,7 @@ ID prefix: `find`.
|
|
|
89
89
|
- Python side: `DEFAULT_AGE_SECONDS: int = 120` and `DEFAULT_POLL_INTERVAL: int = 30` at lines 3-4 of `sweep_config.py`. Python `int` is arbitrary precision — no 32-bit ceiling. `argparse` `type=int` (lines 44, 48 of `sweep_empty_dirs.py`) likewise produces a Python int. `time.sleep(arguments.interval)` at line 69 accepts any non-negative number; no overflow risk on the value itself.
|
|
90
90
|
- PowerShell side: `[int]$IntervalMinutes = 5` (line 7) and `[int]$AgeSeconds = 120` (line 10) of `Install-SweepEmptyDirs.ps1`. `[int]` in PowerShell is `System.Int32` — range `-2147483648` to `2147483647`. Both defaults are 4-5 orders of magnitude below the ceiling. The user-overridable values would have to exceed 2^31-1 (≈ 68 years in seconds, ≈ 4084 years in minutes) to overflow.
|
|
91
91
|
- `New-TimeSpan -Minutes $IntervalMinutes` at line 71 of `Install-SweepEmptyDirs.ps1`: `TimeSpan` is internally `Int64` ticks (100ns each). 5 minutes × 60 × 10⁷ = 3×10⁹ ticks — well within `Int64` range. Even 4084-year overflow on `[int]$IntervalMinutes` would be caught at the `[int]` cast, not at the `TimeSpan` construction.
|
|
92
|
-
- Cross-language defaults: PowerShell `[int]$AgeSeconds = 120` (line 10) is passed through argv as the bare token `120` to Python's `argparse type=int`. Python parses it back to an arbitrary-precision int. No precision drift at this magnitude. **But:** the two defaults (PS line 10 and Python sweep_config.py line 3) are independently hardcoded with no shared source of truth — drift in a future edit is the
|
|
92
|
+
- Cross-language defaults: PowerShell `[int]$AgeSeconds = 120` (line 10) is passed through argv as the bare token `120` to Python's `argparse type=int`. Python parses it back to an arbitrary-precision int. No precision drift at this magnitude. **But:** the two defaults (PS line 10 and Python sweep_config.py line 3) are independently hardcoded with no shared source of truth — drift in a future edit is the G4 hazard, not arithmetic overflow today.
|
|
93
93
|
- Shape A candidate (P2): `[int]$AgeSeconds = 120` and `DEFAULT_AGE_SECONDS: int = 120` are duplicated literals across two files with no validation that they match. A future edit to one without the other produces a silent default drift. Cite `Install-SweepEmptyDirs.ps1:10` and `sweep_config.py:3`.
|
|
94
94
|
- Adversarial probes: (a) what happens if the user invokes `Install-SweepEmptyDirs.ps1 -AgeSeconds 2147483648`? PowerShell's `[int]` cast would throw at parameter binding — verify whether the script handles this or crashes uninformatively. (b) `New-TimeSpan -Minutes` accepts `[int32]` per Microsoft docs — is `$IntervalMinutes = 0` accepted, and does `-RepetitionInterval (TimeSpan.Zero)` register a degenerate task? (c) `time.sleep(arguments.interval)` at line 69 with `arguments.interval = 0` — does the busy loop spin without yielding to the OS?
|
|
95
95
|
|
|
@@ -98,7 +98,7 @@ ID prefix: `find`.
|
|
|
98
98
|
- `created = os.path.getctime(each_directory_path)` at line 27 returns `float`.
|
|
99
99
|
- `if now - created >= min_age_seconds:` at line 30 — float minus float compared against `int` (`min_age_seconds: int`). Python promotes the int to float for comparison. The comparison is `>=`, not `==`, so IEEE-754 epsilon noise does not produce stale-result equality bugs of the `0.1 + 0.2 != 0.3` kind.
|
|
100
100
|
- The float subtraction `now - created` carries ~15-16 significant decimal digits. For Unix timestamps in 2026 (~1.78×10⁹), epsilon-magnitude noise is on the order of 10⁻⁷ seconds — irrelevant against a 120-second threshold.
|
|
101
|
-
- Shape B probes: (a) for very small `min_age_seconds` (e.g., `min_age_seconds=0` as in `test_skips_nonempty_dir` at line 73), the comparison `now - created >= 0` is dominated by `getctime` filesystem-resolution rounding (FAT32 = 2-second granularity, NTFS = 100ns) — does this matter for the test's intent? (b) does `time.time()` ever return a value equal to `os.path.getctime` for a directory just created, producing `0.0 >= 0` = True and immediate deletion? (c) is the float subtraction monotonic under wall-clock adjustment (NTP slew, manual clock change) — could `now < created` produce a negative result that compares False against a positive `min_age_seconds`?
|
|
101
|
+
- Shape B probes: (a) for very small `min_age_seconds` (e.g., `min_age_seconds=0` as in `test_skips_nonempty_dir` at line 73), the comparison `now - created >= 0` is dominated by `getctime` filesystem-resolution rounding (FAT32 = 2-second granularity, NTFS = 100ns) — does this matter for the test's intent? (b) does `time.time()` ever return a value equal to `os.path.getctime` for a directory just created, producing `0.0 >= 0` = `True` and immediate deletion? (c) is the float subtraction monotonic under wall-clock adjustment (NTP slew, manual clock change) — could `now < created` produce a negative result that compares False against a positive `min_age_seconds`?
|
|
102
102
|
|
|
103
103
|
**G6. Date / time arithmetic** ⭐ canonical surface
|
|
104
104
|
- `time.time()` at line 20 of `sweep_empty_dirs.py` returns a UTC-anchored Unix timestamp (seconds since 1970-01-01T00:00:00Z) per Python docs. There is no DST math, no timezone arithmetic, no leap-second handling in the file.
|
|
@@ -30,7 +30,7 @@ ID prefix: `find`.
|
|
|
30
30
|
**H3. Path traversal**
|
|
31
31
|
- Surface check: any filesystem operation (`open`, `os.walk`, `os.rmdir`, `Path(…).read_text`, `shutil.copy`, `Get-Content`, `Test-Path`, `Remove-Item`) whose path is built from attacker-controlled input?
|
|
32
32
|
- Shape A pattern: user input joined to a base path without `realpath`/`normpath` and without a containment check verifying the resolved path stays under the intended root; symlink-following enabled where it shouldn't be; UNC / device-namespace paths (`\\?\`, `\\.\`) accepted without filtering; trailing-dot or trailing-space Windows pathname tricks.
|
|
33
|
-
- Shape B probes (when no traversal surface exists): (1) `os.walk` / equivalent does not follow symlinks (`followlinks=False` in Python; `-NoFollowSymlink` semantics in PowerShell). (2) UNC / drive-letter / reparse-point handling — document whether the artifact honors them as given (operator authority) or rejects them. (3) Path normalization — does the code call `realpath`/`normpath` before filesystem ops? (4) TOCTOU between any pre-flight check (`isdir`, `Test-Path`) and the
|
|
33
|
+
- Shape B probes (when no traversal surface exists): (1) `os.walk` / equivalent does not follow symlinks (`followlinks=False` in Python; `-NoFollowSymlink` semantics in PowerShell). (2) UNC / drive-letter / reparse-point handling — document whether the artifact honors them as given (operator authority) or rejects them. (3) Path normalization — does the code call `realpath`/`normpath` before filesystem ops? (4) TOCTOU between any pre-flight check (`isdir`, `Test-Path`) and the filesystem op. (5) Pre-flight gate identification — what is the only validation between attacker input and the filesystem syscall?
|
|
34
34
|
|
|
35
35
|
**H4. Authentication bypass**
|
|
36
36
|
- Surface check: any HTTP / RPC / IPC entry point that should require authentication?
|
|
@@ -54,7 +54,7 @@ ID prefix: `find`.
|
|
|
54
54
|
|
|
55
55
|
**H8. CSRF / state-changing without token**
|
|
56
56
|
- Surface check: any state-changing HTTP handler (POST, PUT, DELETE, PATCH) reachable by an authenticated browser session?
|
|
57
|
-
- Shape A pattern: state-changing handler with no CSRF token validation; SameSite-cookie assumption used as the sole CSRF defense without verifying the framework
|
|
57
|
+
- Shape A pattern: state-changing handler with no CSRF token validation; SameSite-cookie assumption used as the sole CSRF defense without verifying the framework sets it; pre-flight CORS check trusted as authentication; same-origin assumed without enforcement.
|
|
58
58
|
- Shape B probes (when no CSRF surface exists): (1) confirm no `@app.route`-style POST handler, no `@router.post`, no `flask.Flask`, no `fastapi.FastAPI`, no `aiohttp.web.RouteTableDef`, no Express `app.post`. (2) Confirm any local trigger surface (named pipe, Unix socket, COM endpoint, scheduled task) is local-only and not reachable by a remote unauthenticated caller. (3) Confirm no inter-process listener exists that an unprivileged caller could poke to trigger the state change.
|
|
59
59
|
|
|
60
60
|
**H9. Deserialization**
|
|
@@ -71,7 +71,7 @@ ID prefix: `find`.
|
|
|
71
71
|
|
|
72
72
|
Q1: Are there any inputs that cross two H sub-buckets? (e.g., a path that flows through H3-style filesystem handling AND becomes an interpolated argument in an H2 shell command — are the two trust assumptions consistent across both sites?)
|
|
73
73
|
Q2: What's the worst injection / leakage hazard introduced by this artifact? Cite `<file>:<line>` for the specific construction.
|
|
74
|
-
Q3: Which input is most fragile to a future API addition — i.e., where would a future change most likely turn an operator-trust assumption into an
|
|
74
|
+
Q3: Which input is most fragile to a future API addition — i.e., where would a future change most likely turn an operator-trust assumption into an attacker-reachable surface? Name the line(s) most likely to break.
|
|
75
75
|
|
|
76
76
|
## Output
|
|
77
77
|
|
|
@@ -167,7 +167,7 @@ This sub-bucket has TWO distinct command-string-build sites in the diff, both wi
|
|
|
167
167
|
|
|
168
168
|
Q1: Are there any inputs that cross two H sub-buckets? (For PR #394, the candidate is `arguments.root` flowing through H3-style path-handling code that ALSO becomes the `$Target` interpolated into the H2 PowerShell argument string when an operator re-runs the installer with the same path. Are the two trust assumptions consistent?)
|
|
169
169
|
Q2: What's the worst injection / leakage hazard introduced by this PR? Cite `<file>:<line>` for the specific construction. (Candidate: `Install-SweepEmptyDirs.ps1:69` `New-ScheduledTaskAction -Argument` interpolating `$Target` — embedded-`$` and trailing-backslash hazards are both reachable in normal Windows path-naming.)
|
|
170
|
-
Q3: Which input is most fragile to a future API addition — i.e., where would a future change most likely turn an operator-trust assumption into an
|
|
170
|
+
Q3: Which input is most fragile to a future API addition — i.e., where would a future change most likely turn an operator-trust assumption into an attacker-reachable surface? Name the line(s) most likely to break. (Candidate: `test_sweep_empty_dirs.py:25` `_set_creation_time_windows` — if a future test parametrizes `path` from a non-tempfile source, the f-string-into-single-quoted-PowerShell pattern flips from "fragile-but-bounded" to "exploitable.")
|
|
171
171
|
|
|
172
172
|
## Output
|
|
173
173
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Audit [REPO/ARTIFACT] [TARGET_ID] for **Category I only** (concurrency hazards). Skip A–H, J–P. Sub-bucket forced-exhaustion mode: Category I is decomposed into 8 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
2
|
|
|
3
|
-
[ARTIFACT METADATA — including: is this code single-threaded, threaded, asyncio, multiprocessing, or mixed? Name the runtime (CPython 3.x, Node, Go, JVM, .NET, PowerShell runspace, browser JS), the concurrency primitives
|
|
3
|
+
[ARTIFACT METADATA — including: is this code single-threaded, threaded, asyncio, multiprocessing, or mixed? Name the runtime (CPython 3.x, Node, Go, JVM, .NET, PowerShell runspace, browser JS), the concurrency primitives present (`threading`, `asyncio`, `multiprocessing`, `concurrent.futures`, `Thread`, `goroutine`, `Promise`, `Task`, `Start-ThreadJob`, `ForEach-Object -Parallel`, etc.), and the inter-process surface (shared filesystem, shared DB, shared cache, shared queue, signals). State explicitly which primitives are absent so each sub-bucket has a Shape B basis.]
|
|
4
4
|
|
|
5
5
|
ID prefix: `find`.
|
|
6
6
|
|
|
@@ -46,23 +46,23 @@ Inline the artifact under audit below this section. Chunking guidance — pick t
|
|
|
46
46
|
- Compound filesystem ops where no single syscall expresses the intent: "delete if older than X" (must `stat` then `unlink`), "rename if doesn't exist" (must `stat` then `rename` — POSIX `rename` is atomic but overwrites, Windows `rename` fails on overwrite).
|
|
47
47
|
- DB compound ops outside a transaction: read-then-update without `SELECT FOR UPDATE`, `UPDATE ... WHERE version = ?` (optimistic locking missing).
|
|
48
48
|
- CPython GIL gives some atomicity to single bytecode ops (list.append, dict.__setitem__) but not to `+=` or `dict.setdefault` callbacks.
|
|
49
|
-
- Adversarial probes: (a) does any single statement compile to multiple bytecodes (e.g., `obj.attr += 1`) that the GIL does not protect? (b) is there a JSON config rewritten by `json.dump(open(path, 'w'))` — non-atomic; concurrent reader sees a truncated file mid-write? (c) does a "transactional" wrapper
|
|
49
|
+
- Adversarial probes: (a) does any single statement compile to multiple bytecodes (e.g., `obj.attr += 1`) that the GIL does not protect? (b) is there a JSON config rewritten by `json.dump(open(path, 'w'))` — non-atomic; concurrent reader sees a truncated file mid-write? (c) does a "transactional" wrapper start a transaction, or just open a connection?
|
|
50
50
|
|
|
51
51
|
**I6. Thread-local / async-local context bleed**
|
|
52
52
|
- `threading.local()` instances surviving thread-pool reuse (the same OS thread services many tasks; the `local` is keyed to the OS thread, not the logical task).
|
|
53
53
|
- `contextvars.ContextVar` set without `Context.run(...)` — propagation across `asyncio.create_task` is automatic but copying-on-create; mutations after task creation do not propagate.
|
|
54
|
-
- Request-scoped state stored on a module global (Flask `g`, Django thread-local request) leaking when the framework's scoping does not match the
|
|
54
|
+
- Request-scoped state stored on a module global (Flask `g`, Django thread-local request) leaking when the framework's scoping does not match the concurrency model.
|
|
55
55
|
- ORM session-per-request that is reused across requests due to a misconfigured scope.
|
|
56
56
|
- `asyncio` task-local state inside an executor (`run_in_executor` runs in a thread, not a coroutine — `contextvars` may or may not propagate depending on Python version and library version).
|
|
57
57
|
- Adversarial probes: (a) does any helper called from both sync and async paths assume the same context-storage primitive? (b) is there a pool-warmup that pre-populates `threading.local` and assumes it stays populated forever? (c) does logging context (correlation ID) propagate across `loop.run_in_executor`?
|
|
58
58
|
|
|
59
59
|
**I7. Cancellation handling**
|
|
60
60
|
- Every `await` inside an `async def` is a cancellation point. Cleanup code that follows `await` may be skipped on `CancelledError`.
|
|
61
|
-
- `asyncio.shield(...)` to protect critical cleanup; verify that what is shielded is
|
|
61
|
+
- `asyncio.shield(...)` to protect critical cleanup; verify that what is shielded is critical and not just convenient.
|
|
62
62
|
- `try/except Exception:` swallowing `CancelledError` — `CancelledError` inherits from `BaseException` in 3.8+ but the codebase may run on older Python.
|
|
63
63
|
- Synchronous cancellation: `KeyboardInterrupt` landing between two syscalls; `SIGTERM` arriving mid-cleanup.
|
|
64
64
|
- `asyncio.timeout()` (3.11+) vs `asyncio.wait_for(...)` semantics — both raise `CancelledError`, both can race with the wrapped task completing.
|
|
65
|
-
- Adversarial probes: (a) does any `finally` block contain an `await` that could itself be cancelled, leaving cleanup half-done? (b) does the code rely on `__aexit__` running to release a resource, and does the cancellation path
|
|
65
|
+
- Adversarial probes: (a) does any `finally` block contain an `await` that could itself be cancelled, leaving cleanup half-done? (b) does the code rely on `__aexit__` running to release a resource, and does the cancellation path invoke `__aexit__`? (c) is there a `task.cancel()` call without `await task` afterward to surface the cancellation result?
|
|
66
66
|
|
|
67
67
|
**I8. Signal handling in multi-threaded code**
|
|
68
68
|
- Python: signals are always delivered to the main thread. A custom signal handler installed by `signal.signal(...)` on a non-main thread silently no-ops.
|
|
@@ -131,7 +131,7 @@ This is the highest-signal sub-bucket. There are TWO concrete TOCTOU windows in
|
|
|
131
131
|
- Delete the directory entirely → `getctime` raises `FileNotFoundError` (subclass of `OSError`), absorbed by the except at sweep_empty_dirs.py:28-29 (`continue`). Race handled.
|
|
132
132
|
- Repopulate the directory with files → `os.rmdir` raises `OSError` (`ENOTEMPTY`), absorbed by sweep_empty_dirs.py:35-36 (`pass`). Race handled — but `removed.append(...)` at sweep_empty_dirs.py:34 was NOT yet appended (it's inside the same try). Verify the append is correctly gated by successful `rmdir`.
|
|
133
133
|
- Replace the directory with a symlink → `os.rmdir` on a symlink targeting a populated dir behaves OS-dependently; on Windows `os.rmdir` removes a directory symlink without touching the target, on POSIX it raises `ENOTDIR`. Verify the protective except is broad enough.
|
|
134
|
-
- Update creation time on the directory between `getctime` (sweep_empty_dirs.py:27) and the `if` check (sweep_empty_dirs.py:30) → no
|
|
134
|
+
- Update creation time on the directory between `getctime` (sweep_empty_dirs.py:27) and the `if` check (sweep_empty_dirs.py:30) → no window here because the local `created` is already captured; the `now - created` comparison uses the captured value. Verify by re-reading the loop body.
|
|
135
135
|
- `topdown=False` means children are visited before parents. Verify that another process creating a file inside a child directory between the child's `rmdir` and the parent's `rmdir` does not cause the parent's `rmdir` to silently spare a now-non-empty parent — and that this is the *intended* behavior (skip parents whose children re-populated).
|
|
136
136
|
|
|
137
137
|
*Window 2 — PowerShell Test-Path vs Register-ScheduledTask.*
|
|
@@ -36,7 +36,7 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
|
|
|
36
36
|
- Walk every numeric or non-trivial literal other than `0`, `1`, `-1` inside production function bodies.
|
|
37
37
|
- Test files follow the same no-new-comment policy.
|
|
38
38
|
- Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified. Module-level declarations are J3-scope, not J1-scope.
|
|
39
|
-
- For each literal found, decide: structural value that belongs in `config/` (flag) vs.
|
|
39
|
+
- For each literal found, decide: structural value that belongs in `config/` (flag) vs. local arithmetic constant tied to the line's logic (defendable).
|
|
40
40
|
- Adversarial probes must each verify a distinct angle: (a) does the literal duplicate an existing value already centralized in `config/`? (b) does the literal silently couple two languages (e.g., a Python config value and a hand-typed PowerShell / shell mirror)? (c) does the literal appear in user-facing help/doc text in a way that would silently lie if the canonical value changed?
|
|
41
41
|
|
|
42
42
|
**J2. String-template magic**
|
|
@@ -50,20 +50,15 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
|
|
|
50
50
|
- Distinguish *imports* (`from config.X import FOO`) from *declarations* — imports are not J3-scope.
|
|
51
51
|
- Adversarial probes: (a) does any module-level constant masquerade as an "import" via a re-export pattern? (b) is there a `_PRIVATE_UPPER` declaration that escapes the visual UPPER_SNAKE filter but is still module-level? (c) does any test file declare a constant that *would* be flagged if it were in production, indicating the constant probably belongs in `config/` even if test-exempt?
|
|
52
52
|
|
|
53
|
-
**J4. File-global use-count**
|
|
54
|
-
- For every file-global constant outside `config/`, count references in the same file. Single ref → move to `config/`. Zero refs → delete.
|
|
55
|
-
- The rule applies to *constants*, not functions, classes, or imports.
|
|
56
|
-
- Adversarial probes: (a) is any imported constant referenced only once in the importing file (suggesting the import itself is gratuitous)? (b) is any helper function defined in a production file but never called from inside the same file (separate dead-code concern, surfaced here for completeness)? (c) does any constant in `config/` get imported from zero call sites across the repo?
|
|
57
|
-
|
|
58
53
|
**J5. Abbreviations**
|
|
59
54
|
- Walk every parameter, local, and attribute name across production, test, and JavaScript changed lines. Flag: `ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `elem`, `val`, `tmp`, `str`, `num`, `arr`, `obj`, `fn`, `cb`, `req`, `res`. Loop counters `i`/`j`/`k` and `e` for exceptions are exempt.
|
|
60
55
|
- This audit walks changed test-file and `.mjs` / `.js` lines for this rule, even though the write-time hook skips them.
|
|
61
|
-
- Adversarial probes: (a) is there a borderline name (e.g., `removed`, `arguments`) that someone might mis-classify as an abbreviation but is
|
|
56
|
+
- Adversarial probes: (a) is there a borderline name (e.g., `removed`, `arguments`) that someone might mis-classify as an abbreviation but is a full English word? Confirm. (b) does any callback / parameter / attribute use a short variant of a domain term that is technically a full word but conventionally abbreviates a longer one? (c) does any variable in a comprehension or lambda use a single letter outside the `i`/`j`/`k`/`e` exemption?
|
|
62
57
|
|
|
63
58
|
**J6. Vague names**
|
|
64
59
|
- Flag any name from the vague list: `result`, `data`, `output`, `response`, `value`, `item`, `temp`, `info`, `stuff`, `thing`. Vague verb prefixes for function names: `handle`, `process`, `manage`, `do`.
|
|
65
60
|
- This audit walks changed test-file and `.mjs` / `.js` lines for this rule, even though the write-time hook skips them.
|
|
66
|
-
- Adversarial probes: (a) does any local variable use a domain-adjacent name that is
|
|
61
|
+
- Adversarial probes: (a) does any local variable use a domain-adjacent name that is on the vague list (e.g., `result` from a parser, `data` from a fetch)? (b) does any newly-introduced function name start with a vague prefix? (c) does any public attribute / dict key use a vague label that the call site has to disambiguate by surrounding context?
|
|
67
62
|
|
|
68
63
|
**J7. Type hints**
|
|
69
64
|
- Walk every function across production, test, and JavaScript changed lines. Verify parameter and return types are present, no `Any`, no `# type: ignore`.
|
|
@@ -76,7 +71,7 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
|
|
|
76
71
|
- Comments tied to untouched code remain unchanged; a changed comment is removed with the code it describes.
|
|
77
72
|
- Test files follow the same no-new-comment policy.
|
|
78
73
|
- Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
|
|
79
|
-
- Adversarial probes: (a) is there any `# type:` or marker comment that is
|
|
74
|
+
- Adversarial probes: (a) is there any `# type:` or marker comment that is inert prose rather than a type-checker / linter directive? (b) is any docstring carrying inline-comment content (line-level explanations rather than module/function description)? (c) does any newly-added blank line between code stanzas function as a comment substitute, suggesting the author wanted to add a comment but couldn't?
|
|
80
75
|
|
|
81
76
|
**J9. Logging format**
|
|
82
77
|
- Walk every `log_*(...)` call. Must be `log_*("template with {}", arg)`, not `log_*(f"...")`.
|
|
@@ -101,7 +96,7 @@ A `test_*.py` name or a `.mjs` extension takes the line out of the write-time ga
|
|
|
101
96
|
|
|
102
97
|
## Cross-bucket questions to answer at the end
|
|
103
98
|
|
|
104
|
-
Q1: Are there literals or names that span two sub-buckets (e.g., a magic value in J1 that also appears inside an f-string scrutinized by J2; an UPPER_SNAKE in J3 that also
|
|
99
|
+
Q1: Are there literals or names that span two sub-buckets (e.g., a magic value in J1 that also appears inside an f-string scrutinized by J2; an UPPER_SNAKE in J3 that is also an abbreviation under J5)? Cite the literal/name and both sub-bucket IDs.
|
|
105
100
|
|
|
106
101
|
Q2: What is the worst CODE_RULES drift introduced by this artifact? Cite `<file>:<line>`. (Common candidates: cross-language duplicates, stale help text mirroring a config constant, abbreviations in a public API surface, bare `Any` annotations, hardcoded user paths in installer scripts.)
|
|
107
102
|
|
|
@@ -149,13 +144,6 @@ ID prefix: `find`.
|
|
|
149
144
|
- `Install-SweepEmptyDirs.ps1` line 245 — `$TaskName = "SweepEmptyDirs"` is a PowerShell variable; J3 enforces Python module-level UPPER_SNAKE outside `config/`. PowerShell out of scope for J3.
|
|
150
145
|
- Adversarial probes: (a) does any module-level constant in `sweep_empty_dirs.py` masquerade as an "import"? (b) is there any `_PRIVATE_UPPER` declaration that escapes the visual UPPER_SNAKE filter? (c) does the test file accidentally declare a constant that *would* be flagged if it were in production?
|
|
151
146
|
|
|
152
|
-
**J4. File-global use-count**
|
|
153
|
-
- For every file-global constant outside `config/`, count references in the same file. Single ref → move to `config/`. Zero refs → delete.
|
|
154
|
-
- `sweep_empty_dirs.py` — no file-global constants declared (`DEFAULT_AGE_SECONDS` and `DEFAULT_POLL_INTERVAL` are imports). Imports follow standard import-usage rules, not the file-global use-count rule.
|
|
155
|
-
- `config/sweep_config.py:142-143` — `DEFAULT_AGE_SECONDS` and `DEFAULT_POLL_INTERVAL` live in `config/`, exempt by location.
|
|
156
|
-
- Test file `_SCRIPTS_DIR` — test files exempt.
|
|
157
|
-
- Adversarial probes: (a) is any imported constant in `sweep_empty_dirs.py` referenced only once (line 104 for `DEFAULT_AGE_SECONDS`, line 108 for `DEFAULT_POLL_INTERVAL`) — wait, `DEFAULT_AGE_SECONDS` is referenced TWICE on line 104 (`default=DEFAULT_AGE_SECONDS`) and line 105 (`f"...default: {DEFAULT_AGE_SECONDS} = 2 minutes"`); `DEFAULT_POLL_INTERVAL` is referenced TWICE on line 108 and line 109. Both meet the ≥2-references threshold (note: file-global use-count technically applies to declarations, not imports — listed here for completeness). (b) is any helper function in `sweep_empty_dirs.py` defined but never called from inside the file? `_log_walk_error` (line 73) is referenced once at line 84; `_build_parser` (line 101) is referenced once at line 114. The use-count rule applies to *constants*, not functions, so neither is flagged. (c) does any constant in `config/sweep_config.py` get imported from zero call sites?
|
|
158
|
-
|
|
159
147
|
**J5. Abbreviations**
|
|
160
148
|
- Walk every parameter, local, and attribute name in `sweep_empty_dirs.py`.
|
|
161
149
|
- `_log_walk_error(os_error: OSError)` — `os_error` is full word, not `e` (exception loop var allowed but not required). Verified clean.
|
|
@@ -191,7 +179,7 @@ ID prefix: `find`.
|
|
|
191
179
|
- `config/sweep_config.py` — line 140 is a module-level docstring. No inline comments. Clean.
|
|
192
180
|
- Test file — the same no-new-comment rule applies.
|
|
193
181
|
- `Install-SweepEmptyDirs.ps1` — line 227 (`#!/usr/bin/env pwsh`) is a shebang, exempt. No inline `#` comments added in the PowerShell file. Clean.
|
|
194
|
-
- Adversarial probes: (a) is there any `# type:` comment that is
|
|
182
|
+
- Adversarial probes: (a) is there any `# type:` comment that is inert noise rather than a type-checker directive? No occurrences in the diff. (b) is any docstring carrying inline-comment content as line-level explanations rather than module/function description? Module docstring on line 62 is one line ("Delete empty directories older than 2 minutes under a given root."); function docstrings on 78 and 102 are one-liners. Clean. (c) does any newly-added blank line between code stanzas function as a comment substitute? Visual whitespace is allowed.
|
|
195
183
|
|
|
196
184
|
**J9. Logging format**
|
|
197
185
|
- Walk every `log_*(...)` call. Must be `log_*("template with {}", arg)`, not `log_*(f"...")`.
|
|
@@ -247,11 +235,9 @@ import time
|
|
|
247
235
|
from config.sweep_config import DEFAULT_AGE_SECONDS
|
|
248
236
|
from config.sweep_config import DEFAULT_POLL_INTERVAL
|
|
249
237
|
|
|
250
|
-
|
|
251
238
|
def _log_walk_error(os_error: OSError) -> None:
|
|
252
239
|
print(f"warning: cannot scan {os_error.filename} — {os_error.strerror}", file=sys.stderr)
|
|
253
240
|
|
|
254
|
-
|
|
255
241
|
def sweep(root: str, min_age_seconds: int) -> list[str]:
|
|
256
242
|
"""Remove empty directories under *root* older than *min_age_seconds*."""
|
|
257
243
|
|
|
@@ -275,7 +261,6 @@ def sweep(root: str, min_age_seconds: int) -> list[str]:
|
|
|
275
261
|
|
|
276
262
|
return removed
|
|
277
263
|
|
|
278
|
-
|
|
279
264
|
def _build_parser() -> argparse.ArgumentParser:
|
|
280
265
|
parser = argparse.ArgumentParser(description="Delete empty directories older than a given age.")
|
|
281
266
|
parser.add_argument("root", help="Root directory to scan")
|
|
@@ -287,7 +272,6 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
287
272
|
help=f"Poll interval in seconds when looping (default: {DEFAULT_POLL_INTERVAL})")
|
|
288
273
|
return parser
|
|
289
274
|
|
|
290
|
-
|
|
291
275
|
def main() -> None:
|
|
292
276
|
parser = _build_parser()
|
|
293
277
|
arguments = parser.parse_args()
|
|
@@ -309,7 +293,6 @@ def main() -> None:
|
|
|
309
293
|
print("
|
|
310
294
|
stopped.")
|
|
311
295
|
|
|
312
|
-
|
|
313
296
|
if __name__ == "__main__":
|
|
314
297
|
main()
|
|
315
298
|
```
|
|
@@ -342,7 +325,6 @@ if str(_SCRIPTS_DIR) not in sys.path:
|
|
|
342
325
|
|
|
343
326
|
from sweep_empty_dirs import sweep # noqa: E402
|
|
344
327
|
|
|
345
|
-
|
|
346
328
|
def _set_creation_time_windows(path: str, timestamp: float) -> None:
|
|
347
329
|
dt = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
|
348
330
|
date_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
@@ -352,7 +334,6 @@ def _set_creation_time_windows(path: str, timestamp: float) -> None:
|
|
|
352
334
|
check=True, capture_output=True,
|
|
353
335
|
)
|
|
354
336
|
|
|
355
|
-
|
|
356
337
|
def test_deletes_empty_dir_older_than_threshold() -> None:
|
|
357
338
|
with tempfile.TemporaryDirectory() as tmp:
|
|
358
339
|
empty_dir = os.path.join(tmp, "old_empty")
|
|
@@ -362,7 +343,6 @@ def test_deletes_empty_dir_older_than_threshold() -> None:
|
|
|
362
343
|
assert empty_dir in removed
|
|
363
344
|
assert not os.path.isdir(empty_dir)
|
|
364
345
|
|
|
365
|
-
|
|
366
346
|
def test_skips_empty_dir_newer_than_threshold() -> None:
|
|
367
347
|
with tempfile.TemporaryDirectory() as tmp:
|
|
368
348
|
fresh_dir = os.path.join(tmp, "fresh_empty")
|
|
@@ -371,7 +351,6 @@ def test_skips_empty_dir_newer_than_threshold() -> None:
|
|
|
371
351
|
assert fresh_dir not in removed
|
|
372
352
|
assert os.path.isdir(fresh_dir)
|
|
373
353
|
|
|
374
|
-
|
|
375
354
|
def test_deletes_nested_empty_dirs() -> None:
|
|
376
355
|
with tempfile.TemporaryDirectory() as tmp:
|
|
377
356
|
leaf = os.path.join(tmp, "parent", "child", "leaf")
|
|
@@ -384,13 +363,11 @@ def test_deletes_nested_empty_dirs() -> None:
|
|
|
384
363
|
assert os.path.join(tmp, "parent", "child") in removed
|
|
385
364
|
assert os.path.join(tmp, "parent") in removed
|
|
386
365
|
|
|
387
|
-
|
|
388
366
|
def test_empty_root_does_not_crash() -> None:
|
|
389
367
|
with tempfile.TemporaryDirectory() as tmp:
|
|
390
368
|
_set_creation_time_windows(tmp, time.time() - 300)
|
|
391
369
|
sweep(tmp, min_age_seconds=120)
|
|
392
370
|
|
|
393
|
-
|
|
394
371
|
def test_skips_nonempty_dir() -> None:
|
|
395
372
|
with tempfile.TemporaryDirectory() as tmp:
|
|
396
373
|
nonempty_dir = os.path.join(tmp, "has_stuff")
|
|
@@ -34,7 +34,7 @@ ID prefix: `find`.
|
|
|
34
34
|
**M3. Consumer-expects-set anti-pattern**
|
|
35
35
|
- For every consumer that calls `set(producer())`, `dict.fromkeys(producer())`, `dict((k, v) for k, v in producer())`, `INSERT ... ON CONFLICT`, or `pandas.DataFrame.set_index`, walk back to the producer: should the producer have returned the set/dict directly?
|
|
36
36
|
- The anti-pattern is a sign that the producer's `list[X]` return type lied about cardinality — the consumer is paying for the deduplication that the producer should have done.
|
|
37
|
-
- Adversarial probes: (a) does any test mock the producer with a list containing duplicates — does the consumer's set-conversion silently drop them? (b) does the consumer's set / dict size differ from the producer's list length in production logs? (c) does the consumer raise `RuntimeError: duplicate key` on
|
|
37
|
+
- Adversarial probes: (a) does any test mock the producer with a list containing duplicates — does the consumer's set-conversion silently drop them? (b) does the consumer's set / dict size differ from the producer's list length in production logs? (c) does the consumer raise `RuntimeError: duplicate key` on production inputs?
|
|
38
38
|
|
|
39
39
|
**M4. `extend(...)` into list consumers (acceptable)**
|
|
40
40
|
- For every consumer whose only operation is `accumulator.extend(producer())` into a list, verify the accumulator's downstream consumers tolerate duplicates.
|
|
@@ -11,7 +11,7 @@ Audit [REPO/ARTIFACT] [TARGET_ID] for **Category N only** (test-name scenario ve
|
|
|
11
11
|
|
|
12
12
|
ID prefix: `find`.
|
|
13
13
|
|
|
14
|
-
[ONE-PARAGRAPH FRAME: enumerate every test whose name includes a scenario claim (`_when_*`, `_at_*`, `_under_*`, `_with_*`, `_on_*`, `_after_*`, `_during_*`). State the audit goal: for each scenario-named test, verify the body sets up the named condition via fixture / monkeypatch / environment gate so the production code's scenario-named branch
|
|
14
|
+
[ONE-PARAGRAPH FRAME: enumerate every test whose name includes a scenario claim (`_when_*`, `_at_*`, `_under_*`, `_with_*`, `_on_*`, `_after_*`, `_during_*`). State the audit goal: for each scenario-named test, verify the body sets up the named condition via fixture / monkeypatch / environment gate so the production code's scenario-named branch runs during the act phase.]
|
|
15
15
|
|
|
16
16
|
## Source material ([N] files/sections, all lines in scope)
|
|
17
17
|
|
|
@@ -37,12 +37,12 @@ ID prefix: `find`.
|
|
|
37
37
|
|
|
38
38
|
**N5. Assertion shape mismatch**
|
|
39
39
|
- For every assertion, verify the assertion's shape can fail by construction. `assert <substring> not in result` where the substring is misspelled relative to the production output, or `assert result == ""` when the production function returns `None` on the negative case, or `len(result) > 0` when the production function returns an empty list on the no-feature path.
|
|
40
|
-
- Adversarial probes: (a) inspect each assertion's shape against the production function's
|
|
40
|
+
- Adversarial probes: (a) inspect each assertion's shape against the production function's return-value space; (b) check for assertions where the substring shape never appears in the production output by construction; (c) check for `assert x is True` where the production function returns truthy non-bool values.
|
|
41
41
|
|
|
42
42
|
**N6. Cross-platform scenario gating**
|
|
43
43
|
- For every test named `_on_windows` / `_on_linux` / `_on_macos`, verify the body gates on `sys.platform`, `monkeypatch.setattr(os, "name", ...)`, or `@pytest.mark.skipif`.
|
|
44
44
|
- Bare scenario names that run unchanged across platforms claim more than they prove.
|
|
45
|
-
- Adversarial probes: (a) does the production function's platform-specific branch get skipped on the CI runner's
|
|
45
|
+
- Adversarial probes: (a) does the production function's platform-specific branch get skipped on the CI runner's platform; (b) does the test pass against the platform fallback rather than the platform-specific code; (c) is the platform fixture installed and respected by the test runner.
|
|
46
46
|
|
|
47
47
|
**N7. Time / clock scenario gating**
|
|
48
48
|
- For every test named `_after_<duration>` / `_at_midnight` / `_during_business_hours`, verify the body injects a frozen clock (`freezegun.freeze_time`, `monkeypatch.setattr(time, "time", ...)`, `unittest.mock.patch("datetime.now")`).
|
|
@@ -62,8 +62,8 @@ ID prefix: `find`.
|
|
|
62
62
|
- For every test, verify the fixture / path / import wiring resolves to the artifact the test name claims.
|
|
63
63
|
- Path arithmetic: walk every `Path(__file__).parents[k]` chain symbolically and confirm it reaches the directory the assertion expects — a `parents[3]` that stops at `skills/` while the test expects the package root cannot fail for the right reason.
|
|
64
64
|
- Same-symbol dual imports: `from module import helper` plus `from module import helper as helper_alias` bind two names to the same function object, so any parity assertion between the two bound names is true by construction and proves nothing.
|
|
65
|
-
- Fixture file lookups: confirm every `open(Path(__file__).parent / "fixture.txt")` (or equivalent) reaches a file that
|
|
66
|
-
- Adversarial probes: (a) re-derive each `parents[k]` index against the
|
|
65
|
+
- Fixture file lookups: confirm every `open(Path(__file__).parent / "fixture.txt")` (or equivalent) reaches a file that exists in the repo.
|
|
66
|
+
- Adversarial probes: (a) re-derive each `parents[k]` index against the directory depth and flag any off-by-k; (b) check whether two imports in the test resolve to the same object before trusting a cross-name comparison; (c) confirm each referenced fixture path exists on disk at the depth the arithmetic produces.
|
|
67
67
|
|
|
68
68
|
## Cross-bucket questions to answer at the end
|
|
69
69
|
|
|
@@ -87,7 +87,7 @@ PR: refactor(hooks): cross-platform path resolution for windows-rmtree-blocker
|
|
|
87
87
|
Head SHA: (the commit that landed the platform-conditional logic)
|
|
88
88
|
ID prefix: `find`.
|
|
89
89
|
|
|
90
|
-
The PR adds platform-conditional path-resolution logic to `windows_rmtree_blocker.py` and ships 5 new tests named `test_*_on_windows` and `test_*_on_linux` across `test_windows_rmtree_blocker.py`. The audit goal: verify each scenario-named test sets up the named platform via monkeypatch or skipif gate so the production function's platform-specific branch
|
|
90
|
+
The PR adds platform-conditional path-resolution logic to `windows_rmtree_blocker.py` and ships 5 new tests named `test_*_on_windows` and `test_*_on_linux` across `test_windows_rmtree_blocker.py`. The audit goal: verify each scenario-named test sets up the named platform via monkeypatch or skipif gate so the production function's platform-specific branch runs during the act phase.
|
|
91
91
|
|
|
92
92
|
## Sub-buckets (each requires Shape A finding OR Shape B with ≥3 adversarial probes)
|
|
93
93
|
|
|
@@ -128,7 +128,7 @@ The PR adds platform-conditional path-resolution logic to `windows_rmtree_blocke
|
|
|
128
128
|
|
|
129
129
|
## Cross-bucket questions to answer at the end
|
|
130
130
|
|
|
131
|
-
Q1: Five scenario-named tests (F5, F21, F23, F26, F27) do not gate on `sys.platform` and pass against the Linux-fallback branch on the CI runner. The Windows-specific code path has zero
|
|
131
|
+
Q1: Five scenario-named tests (F5, F21, F23, F26, F27) do not gate on `sys.platform` and pass against the Linux-fallback branch on the CI runner. The Windows-specific code path has zero coverage despite the test names claiming it. Cite `test_windows_rmtree_blocker.py:42` (F5 first test) and `windows_rmtree_blocker.py:67` (the `if sys.platform == "win32":` branch) as the misclaim pair.
|
|
132
132
|
|
|
133
133
|
Q2: Worst false-coverage signal: F5 — the test's name `test_resolves_path_on_windows` reads as Windows-branch coverage in the PR review, but the act phase exercises the Linux fallback. A reviewer reading the test name during PR review would assume Windows coverage exists; it does not.
|
|
134
134
|
|
|
@@ -57,7 +57,7 @@ ID prefix: `find`.
|
|
|
57
57
|
|
|
58
58
|
**O6. Free-form `Args:`-adjacent claims**
|
|
59
59
|
- Judgment: thick rubric O6 (unions, suppressors, exclusion axis, delegation summaries, Returns/Raises/Note claims, run-mode field meaning). Gate inventory and free-form checklist live in the rubric.
|
|
60
|
-
- Adversarial probes: (a) check `Returns:` claims against every `return` statement in the body — is the documented return shape the
|
|
60
|
+
- Adversarial probes: (a) check `Returns:` claims against every `return` statement in the body — is the documented return shape the shape the body returns; (b) check `Raises:` claims against every `raise` and propagating callee — is every documented raise reachable; (c) check `Example:` snippets — does the snippet compile against the signature.
|
|
61
61
|
|
|
62
62
|
**O7. Module-doc-vs-split-module after refactor**
|
|
63
63
|
- Judgment: thick rubric O7.
|
|
@@ -89,7 +89,7 @@ Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket O1-O9, produce Shape A
|
|
|
89
89
|
|
|
90
90
|
Audit jl-cmd/claude-dev-env PR #522 for **Category O only** (docstring / fixture-prose vs implementation drift). Skip A-N, P. Sub-bucket forced-exhaustion mode: Category O is decomposed into 9 sub-buckets below.
|
|
91
91
|
|
|
92
|
-
PR #522 split `pr_description_command_parser.py` into two modules — the original parser and a new `pr_description_pr_number.py` — but the originating module's docstring still claims the PR-number recovery responsibility. A sibling change to `pr_description_body_audit.py` introduced a module docstring whose verb (`detects vague language`) overstates the module's
|
|
92
|
+
PR #522 split `pr_description_command_parser.py` into two modules — the original parser and a new `pr_description_pr_number.py` — but the originating module's docstring still claims the PR-number recovery responsibility. A sibling change to `pr_description_body_audit.py` introduced a module docstring whose verb (`detects vague language`) overstates the module's responsibility (it only exposes `_extract_vague_scan_text()`; detection runs elsewhere).
|
|
93
93
|
|
|
94
94
|
Expected findings on PR #522:
|
|
95
95
|
- **O1 finding:** `pr_description_body_audit.py:8` docstring uses verb `detects`, but the only exported symbol prepares input for a regex scan that fires in a different module. Body line(s) showing `_extract_vague_scan_text` returning normalized text without a detection call.
|
|
@@ -37,11 +37,11 @@ Two canonical failures to probe for:
|
|
|
37
37
|
|
|
38
38
|
**P1. Boolean / flag names assert state the body keeps**
|
|
39
39
|
- For every new `is_*` / `has_*` / `was_*` / `should_*` flag, trace the body. Every set site must be paired with a reset site that fires when the named condition becomes false.
|
|
40
|
-
- Adversarial probes: (a) grep every assignment to the flag — count set-true vs set-false; (b) for AST-driven flags, walk the visit method and confirm scope-exit semantics (def visited → flag set; dedent / function-exit → flag reset); (c) construct an input where the flag should be false after a
|
|
40
|
+
- Adversarial probes: (a) grep every assignment to the flag — count set-true vs set-false; (b) for AST-driven flags, walk the visit method and confirm scope-exit semantics (def visited → flag set; dedent / function-exit → flag reset); (c) construct an input where the flag should be false after a region that set it true — does the code agree.
|
|
41
41
|
|
|
42
42
|
**P2. Predicate-name breadth matches body coverage**
|
|
43
43
|
- For every new `_is_*` / `_has_*` / `_can_*` predicate function, list the body's `return True` branches. The named predicate must hold on the union of those branches and nothing else.
|
|
44
|
-
- Adversarial probes: (a) construct an input that satisfies the name but returns False — that is a P2 narrowness finding; (b) construct an input that does not satisfy the name but returns True — that is a P2 breadth finding; (c) check neighbor predicates — is the body's
|
|
44
|
+
- Adversarial probes: (a) construct an input that satisfies the name but returns False — that is a P2 narrowness finding; (b) construct an input that does not satisfy the name but returns True — that is a P2 breadth finding; (c) check neighbor predicates — is the body's contract the responsibility of a different helper.
|
|
45
45
|
|
|
46
46
|
**P3. Regex name vs regex shape** ⭐ canonical P case
|
|
47
47
|
- For every new `*_PATTERN` / `*_REGEX` constant, read the regex source. Confirm the anchors (`^`, `$`, `\b`, lookarounds) match the name's promised shape. `FILE_PATH_PATTERN` implies path shape; the regex must include path-shape anchors (slash count, segment shape, length bounds) enough to reject non-path inputs.
|
|
@@ -73,7 +73,7 @@ Q3: Which identifier most clearly shows the body should be renamed (the name is
|
|
|
73
73
|
|
|
74
74
|
## Output
|
|
75
75
|
|
|
76
|
-
Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket P1-P7, produce Shape A or Shape B (with ≥3 probes). Each Shape A finding must cite (a) the identifier file:line, (b) one concrete input that shows the name-vs-body gap, and (c) the recommended fix direction (rename vs body-tighten). Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 identifiers whose names overstate what the body
|
|
76
|
+
Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket P1-P7, produce Shape A or Shape B (with ≥3 probes). Each Shape A finding must cite (a) the identifier file:line, (b) one concrete input that shows the name-vs-body gap, and (c) the recommended fix direction (rename vs body-tighten). Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 identifiers whose names overstate what the body does — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
|
@@ -30,13 +30,13 @@ ID prefix: `find`.
|
|
|
30
30
|
|
|
31
31
|
**Q2. PR-description claim verification**
|
|
32
32
|
- Read the PR body. List each falsifiable claim — a dead-code removal, a performance or ceiling number, a "last caller" assertion, a "no remaining X" cleanup claim.
|
|
33
|
-
- Verify each claim two ways against the diff and the repo. A dead-code claim gets a reference search across the repo for the named symbol. A ceiling or performance claim gets checked against the
|
|
33
|
+
- Verify each claim two ways against the diff and the repo. A dead-code claim gets a reference search across the repo for the named symbol. A ceiling or performance claim gets checked against the config value the diff sets and the call sites it touches. Cite the PR-body line and the diff or repo line that confirms or contradicts it.
|
|
34
34
|
- Adversarial probes when the body looks correct: (a) search the repo for a surviving caller of any symbol the body calls dead; (b) compare each stated number against the config constant the diff sets; (c) check whether a "this finishes the cleanup" claim leaves a matching pattern in an unchanged sibling file.
|
|
35
35
|
|
|
36
36
|
**Q3. Message-vs-guard consistency**
|
|
37
37
|
- For every log, error, and evidence string the diff adds or edits, read the branch condition that gates it. The message text matches what that guard guarantees: a string asserting an outcome fires only on a branch that reaches the outcome.
|
|
38
38
|
- Check temporal wording against the lifetime of the state variable the message names: "this run" versus "this round", "is open" versus "was opened", "reached" versus "attempted". Cite the message line and the guard or state-variable line.
|
|
39
|
-
- Adversarial probes when messages look aligned: (a) trace each success message back to the return value or flag it claims and confirm the branch cannot reach it with the opposite value; (b) check every "missing" / "not found" error against the exception class the branch
|
|
39
|
+
- Adversarial probes when messages look aligned: (a) trace each success message back to the return value or flag it claims and confirm the branch cannot reach it with the opposite value; (b) check every "missing" / "not found" error against the exception class the branch catches; (c) check every counter-based message against where the counter resets.
|
|
40
40
|
|
|
41
41
|
## Cross-bucket questions to answer at the end
|
|
42
42
|
|
|
@@ -41,6 +41,10 @@ test('EVER_SHIPPED_SKILL_NAMES includes the windows scheduled task skill', () =>
|
|
|
41
41
|
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('windows-scheduled-task'), true);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
test('EVER_SHIPPED_SKILL_NAMES includes the plugin eval standalone skill', () => {
|
|
45
|
+
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('plugin-eval-standalone-skill'), true);
|
|
46
|
+
});
|
|
47
|
+
|
|
44
48
|
test('EVER_SHIPPED_SKILL_NAMES includes the session continuity companion', () => {
|
|
45
49
|
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('session-continuity'), true);
|
|
46
50
|
});
|
|
@@ -6,7 +6,7 @@ Source: Smart & Molak, *BDD in Action* 2e, Chapter 7.6 — scenario quality patt
|
|
|
6
6
|
|
|
7
7
|
## Declarative Focus
|
|
8
8
|
|
|
9
|
-
Scenarios work best when they name what users want to achieve in the language of the business. Lead with goals and recognizable domain tasks so readers grasp intent at a glance. Reserve step-level or UI detail for places where it
|
|
9
|
+
Scenarios work best when they name what users want to achieve in the language of the business. Lead with goals and recognizable domain tasks so readers grasp intent at a glance. Reserve step-level or UI detail for places where it clarifies behavior.
|
|
10
10
|
|
|
11
11
|
> "Good scenarios model business behavior, not system interactions." — Smart & Molak §7.6.3
|
|
12
12
|
|
|
@@ -42,7 +42,7 @@ Then I should be presented with the following hotels:
|
|
|
42
42
|
|
|
43
43
|
Personas ground scenarios in realistic goals and context. Use light soap-opera personas when you need depth before full UX research: introduce names and roles as needed and deepen them across scenarios.
|
|
44
44
|
|
|
45
|
-
> Smart & Molak §7.6.5 describe personas as rich, realistic descriptions: each persona captures goals, abilities, and background information that ground the test scenario in a
|
|
45
|
+
> Smart & Molak §7.6.5 describe personas as rich, realistic descriptions: each persona captures goals, abilities, and background information that ground the test scenario in a user context.
|
|
46
46
|
|
|
47
47
|
Good scenarios name who acts and what they need in plain language.
|
|
48
48
|
|