Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/code-metrics/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "code-metrics",
"version": "0.3.17",
"version": "0.3.18",
"description": "Read-only code measures for a change, with cited references and no verdict: lines per file (audit-size), cyclomatic, cognitive, and Halstead complexity (audit-complexity), duplication with sanctioned-replication exclusions (audit-duplication), coverage per function with CRAP from existing lcov, Cobertura, coverage.py, or Go artifacts (audit-coverage), type debt for TypeScript and Python (audit-type-debt), the literacy router for what each number can and cannot say (principles), and a setup skill for the consumer's .claude/code-metrics.yaml. Runs external collectors only when they already resolve, never installs, never runs tests, never emits a finding.",
"author": {
"name": "Melodic Software",
Expand Down
12 changes: 12 additions & 0 deletions plugins/code-metrics/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
All notable changes to the `code-metrics` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.3.18] - 2026-09-27

### Fixed

- **A total sanctioned-replication exclusion no longer leaves two unexplained file counts.** When a
registry excluded every clone group, the duplication report read `Files with clones: 0` beside a
scope header counting every scanned file, with nothing saying why. The two measure different
populations and stay apart: the exclusion line now adds how many files hold nothing but excluded
groups, and that `Files with clones` counts surviving groups only. `reference/report-schema.md`
states which population `summary.files` and `scope.files` each describe. A run with no exclusion,
or whose excluded groups touch only files a surviving group also touches, renders as before.

## [0.3.17] - 2026-09-25

### Changed
Expand Down
10 changes: 10 additions & 0 deletions plugins/code-metrics/reference/report-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ methods in one file count as two, while a cyclomatic row and a Halstead row for
count as one even though only the first reports where it begins. `summary.files` counts every
file a row stands for, the copies behind a `replicas` row included.

`summary.files` and `scope.files` count different populations and are not expected to agree.
`scope.files` is every file the run scanned. `summary.files` is the files the surviving rows stand
for; in a duplication document that is every file holding an instance of a surviving clone group,
which the markdown labels `Files with clones`. A clone group a registry excluded adds nothing to
`summary.files`: its files stay in `scope.files` and in `excluded[].instances`. When at least one
file holds nothing but excluded groups, the markdown's exclusion line says how many, so a total
exclusion (`Files with clones: 0` beside a non-zero scope count) explains itself. The two counts
were deliberately kept apart rather than made equal: folding excluded files into the summary would
make it describe the scanned population a second time and stop saying where duplication remains.

The markdown rendering joins the rows the same way the count does: one line per function with
every collector's values, a row with no start line joining the one function of its name in the
file, and never two rows whose values disagree. The JSON keeps one row per collector, because each
Expand Down
24 changes: 23 additions & 1 deletion plugins/code-metrics/scripts/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,9 +710,31 @@ def render(
f"{summary.get('clone_groups', 0)} clone group(s)."
)
if doc.get("excluded"):
lines.append(
excluded_line = (
f"Excluded by a sanctioned-replication registry: {len(doc['excluded'])}."
)
# `Files with clones` counts surviving groups only, while the scope
# header counts every file scanned. A file whose every group was
# excluded is in the second and not the first; name how many, so the
# two counts read as the different populations they are.
surviving = {
instance.get("file")
for row in doc.get("measures", [])
for instance in row.get("instances") or []
}
excluded_only = {
instance.get("file")
for group in doc["excluded"]
for instance in group.get("instances") or []
if instance.get("file")
} - surviving
if excluded_only:
excluded_line += (
f" Files with clones counts surviving groups only, so the "
f"{len(excluded_only)} file(s) holding nothing but excluded groups are "
"left out of it; the scope's file count is every file scanned."
)
lines.append(excluded_line)
elif duplication:
lines.append(
"Excluded by a sanctioned-replication registry: 0 (no registry configured, or "
Expand Down
42 changes: 42 additions & 0 deletions plugins/code-metrics/scripts/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,48 @@ def test_an_empty_excluded_list_is_stated_with_its_reason(self) -> None:
"Excluded by a sanctioned-replication registry: 1.", self.rendered(doc)
)

def test_a_total_exclusion_labels_the_two_file_counts(self) -> None:
# Every group excluded: the scope header still counts the scanned
# files while the summary counts none, and the report says why.
excluded = clone_row("bash", "a/shared/u.sh", "b/shared/u.sh", 20)
doc = resummarized(duplication_doc([]))
doc["excluded"] = [
{
"registry": "r.txt",
"line": 3,
"path": "shared/u.sh",
"instances": excluded["instances"],
}
]
self.assertEqual(doc["summary"]["files"], 0)
out = self.rendered(doc)
self.assertIn("Scope: all, 4 file(s).", out)
self.assertIn("\nFiles with clones: 0.\n", out)
self.assertIn(
"Excluded by a sanctioned-replication registry: 1. Files with clones counts "
"surviving groups only, so the 2 file(s) holding nothing but excluded groups "
"are left out of it; the scope's file count is every file scanned.",
out,
)

def test_a_file_in_a_surviving_group_is_not_named_as_excluded_only(self) -> None:
kept = clone_row("bash", "a/u.sh", "b/u.sh", 20)
doc = resummarized(duplication_doc([kept]))
doc["excluded"] = [
{
"registry": "r.txt",
"line": 3,
"path": "u.sh",
"instances": [
{"file": "a/u.sh", "start_line": 40, "end_line": 60},
{"file": "c/u.sh", "start_line": 40, "end_line": 60},
],
}
]
out = self.rendered(doc)
self.assertIn("\nFiles with clones: 2.\n", out)
self.assertIn("so the 1 file(s) holding nothing but excluded groups", out)

def test_no_detector_prints_one_headline_with_the_hint(self) -> None:
hint = "jscpd: https://github.com/kucherenko/jscpd (npm install -g jscpd)"
doc = duplication_doc(
Expand Down
5 changes: 3 additions & 2 deletions plugins/code-metrics/skills/audit-duplication/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ continues. This plugin never installs, downloads, or `npx`-fetches a detector.
Present the markdown report as printed. It opens with the scope and a "Coverage of this run"
table (lane, collector, status, reason), then one row per clone group, largest first, listing
every instance as `file:start-end`, then a rollup per lane and per directory, then the summary
lines: files with clones, the duplicated-line total, how many groups a registry excluded, and
which lanes were partial. When the report opens with `No clone detector ran in any lane`, offer
lines: files with clones, the duplicated-line total, how many groups a registry excluded (and how
many files hold nothing but excluded groups, which files with clones leaves out while the scope's
count keeps them), and which lanes were partial. When the report opens with `No clone detector ran in any lane`, offer
the user the install command that headline carries (`npm install -g jscpd`, or a devDependency)
and run it only when they confirm; never install silently and never `npx`-fetch it. Keep the
`--json` document when the numbers feed a comparison:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ excluded_path="$(printf '%s' "$out" | "$PY" -c 'import json,sys; print(json.load
assert_eq "the excluded entry names the registry line" "shared/shared-utils.sh" "$excluded_path"
excluded_registry="$(printf '%s' "$out" | "$PY" -c 'import json,sys; print(json.load(sys.stdin)["excluded"][0]["registry"])' 2>/dev/null)"
assert_contains "the excluded entry names the registry file" "$excluded_registry" "cluster.txt"
assert_doc "a total exclusion counts no file with clones while the scope counts the scanned files" "$out" \
'd["summary"]["files"] == 0 and d["scope"]["files"] >= 2'

# 2. Without the registry the same clones are duplication debt.
out="$(PATH="$STUBS:$EMPTY_PATH" bash "$SCRIPT" --json --all "$CLUSTER")"
Expand All @@ -100,6 +102,9 @@ assert_eq "the markdown run exits 0" 0 "$?"
assert_contains "markdown carries the run table" "$out" "## Coverage of this run"
assert_contains "markdown states the duplicated-line count" "$out" "Duplicated lines"
assert_contains "markdown states the exclusion" "$out" "Excluded by a sanctioned-replication registry"
assert_contains "markdown counts no file with clones after a total exclusion" "$out" "Files with clones: 0."
assert_contains "markdown says which files the summary leaves out, and why" "$out" \
"so the 2 file(s) holding nothing but excluded groups are left out of it"

# 4. Every collector absent: a report is still produced and says so.
out="$(PATH="$EMPTY_PATH" bash "$SCRIPT" --json --all "$CLUSTER")"
Expand Down
2 changes: 1 addition & 1 deletion plugins/gaming/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "gaming",
"version": "0.8.0",
"version": "0.8.1",
"description": "Apply, track, tune, and cleanly remove the community DLSS 5 Neural Rendering mod (OptiScaler forks) in PC games on Windows, from Steam, Epic, EA app, Battle.net, GOG, Ubisoft Connect and the Xbox app. Anti-cheat checks that refuse by default and install only on a typed at-your-own-risk acknowledgement, pre-install snapshots and manifests for byte-exact removal, a per-game ledger, and an upstream watch for fork and driver releases. Ships no NVIDIA binary: the runtime DLL comes from a path, an installed DLSS 5 title, or a source the user configures.",
"author": {
"name": "Melodic Software",
Expand Down
6 changes: 6 additions & 0 deletions plugins/gaming/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
All notable changes to the `gaming` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.8.1] - 2026-09-27

### Fixed

- A folder the discovery scan cannot list is reported with the path from the error, from `CategoryInfo.TargetName`, or from a quoted path in the message when Windows leaves `TargetObject` empty. The selftest holds that folder open with no sharing so the listing fails with a sharing violation. A Deny ACE, including one for Everyone, does not stop an elevated runner from listing.

## [0.8.0] - 2026-09-27

### Added
Expand Down
64 changes: 49 additions & 15 deletions plugins/gaming/skills/dlss5/scripts/Invoke-Dlss5Mod.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ function Find-AntiCheat($root) {
Get-ChildItem -LiteralPath $p -Force -ErrorAction SilentlyContinue
}
}
$script:AcScanGaps = @($err | ForEach-Object { "on-disk scan: $($_.TargetObject) unreadable: $($_.Exception.Message)" })
$script:AcScanGaps = @($err | ForEach-Object { "on-disk scan: $(ErrorPath $_ '') unreadable: $($_.Exception.Message)" })
@($items | Where-Object { IsAntiCheatName $_.Name } | ForEach-Object FullName | Sort-Object -Unique)
}

Expand Down Expand Up @@ -417,14 +417,24 @@ function Game($launcher, $name, $dir, $source) {
}
# One bad record is reported and skipped; the rest of that launcher's records still load.
function Record($label, [scriptblock]$body) { try { & $body } catch { $script:Unchecked += "${label} unreadable: $($_.Exception.Message)" } }
# The path a listing error names. Windows often leaves TargetObject empty and puts the path on
# CategoryInfo.TargetName, or only inside a quoted path in the message.
function ErrorPath($err, $fallback) {
foreach ($named in @($err.TargetObject, $err.CategoryInfo.TargetName)) { if ("$named") { return "$named" } }
$msg = "$($err.Exception.Message)"
if ($msg -match "'([A-Za-z]:\\[^']+)'") { return $Matches[1] }
if ($msg -match '"([A-Za-z]:\\[^"]+)"') { return $Matches[1] }
if ("$fallback") { return "$fallback" }
return "$fallback"
}
# Lists a folder; an absent one is silent, one that cannot be listed is reported.
# The name filter is applied afterwards: Get-ChildItem -Filter drops an access-denied folder, at
# any depth, without recording an error.
function ListDir($label, $path, [hashtable]$opts = @{}) {
$pattern = $opts.Filter ?? '*'; $o = @{} + $opts; $o.Remove('Filter')
$err = $null
Get-ChildItem -LiteralPath $path @o -Force -ErrorAction SilentlyContinue -ErrorVariable err | Where-Object Name -like $pattern
foreach ($x in $err) { if ($x.Exception -isnot [Management.Automation.ItemNotFoundException]) { $script:Unchecked += "${label}: $($x.TargetObject) unreadable: $($x.Exception.Message)" } }
foreach ($x in $err) { if ($x.Exception -isnot [Management.Automation.ItemNotFoundException]) { $script:Unchecked += "${label}: $(ErrorPath $x $path) unreadable: $($x.Exception.Message)" } }
}
function Find-SteamGames {
$steam = (RegProps 'HKCU:\Software\Valve\Steam').SteamPath
Expand Down Expand Up @@ -1282,7 +1292,7 @@ function Find-RuntimeCandidates {
@(Get-ScanRoots | Where-Object { Test-Path -LiteralPath $_ -PathType Container } | ForEach-Object {
$err = $null
Get-ChildItem -LiteralPath $_ -Recurse -File -Force -ErrorAction SilentlyContinue -ErrorVariable err | Where-Object Name -eq 'nvngx_dlssnr.dll'
foreach ($x in $err) { $script:ScanGaps += "runtime scan: $($x.TargetObject) unreadable: $($x.Exception.Message)" }
foreach ($x in $err) { $script:ScanGaps += "runtime scan: $(ErrorPath $x '') unreadable: $($x.Exception.Message)" }
} | Sort-Object FullName -Unique)
}
# Read-only: installed games per launcher, what could not be read, and runtime DLL candidates with
Expand Down Expand Up @@ -1874,23 +1884,47 @@ function Do-Selftest {
$vlock = [IO.File]::Open("$sp\steamapps\libraryfolders.vdf", 'Open', 'Read', 'None')
try { $vf = Find-Games } finally { $vlock.Dispose() }
Assert 'discover: a locked libraryfolders.vdf is reported and the main library still scans' (@($script:Unchecked | Where-Object { $_ -like 'Steam:*libraryfolders.vdf*unreadable*' }).Count -eq 1 -and @($vf | Where-Object name -eq 'Main Lib Game').Count -eq 1 -and -not @($vf | Where-Object name -eq 'Clean Game').Count)
# A library folder that cannot be listed is reported, not read as empty
$deny = [Security.AccessControl.FileSystemAccessRule]::new([Security.Principal.WindowsIdentity]::GetCurrent().User, 'ListDirectory', 'Deny')
$acl = Get-Acl -LiteralPath "$l2\steamapps"; $acl.AddAccessRule($deny); Set-Acl -LiteralPath "$l2\steamapps" -AclObject $acl
# A library folder that cannot be listed is reported, not read as empty.
# A Deny ACE does not stop an elevated runner from listing, even a Deny for Everyone.
# Hold the directory open with no sharing so the next listing fails with a sharing violation.
# pwsh on the runner has no FileOptions.BackupSemantics, so open the directory with CreateFileW.
$LockDir = {
param([string]$LiteralPath)
if (-not ('Dlss5DirHandle' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
public sealed class Dlss5DirHandle : IDisposable {
IntPtr handle;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr CreateFileW(string name, uint access, uint share, IntPtr sec, uint disp, uint flags, IntPtr template);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr handle);
public Dlss5DirHandle(string path) {
handle = CreateFileW(path, 0x80000000, 0, IntPtr.Zero, 3, 0x02000000, IntPtr.Zero);
if (handle == new IntPtr(-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
}
public void Dispose() {
if (handle != IntPtr.Zero && handle != new IntPtr(-1)) { CloseHandle(handle); handle = IntPtr.Zero; }
}
}
'@
}
[Dlss5DirHandle]::new($LiteralPath)
}
$lockSteam = & $LockDir "$l2\steamapps"
$od = "$tmp\pd\Origin\LocalContent\Denied"; Put "$od\x.mfst" '?id=x'
$acl2 = Get-Acl -LiteralPath $od; $acl2.AddAccessRule($deny); Set-Acl -LiteralPath $od -AclObject $acl2
$lockOd = & $LockDir $od
try { $null = Find-Games }
finally {
$acl = Get-Acl -LiteralPath "$l2\steamapps"; [void]$acl.RemoveAccessRule($deny); Set-Acl -LiteralPath "$l2\steamapps" -AclObject $acl
$acl2 = Get-Acl -LiteralPath $od; [void]$acl2.RemoveAccessRule($deny); Set-Acl -LiteralPath $od -AclObject $acl2
}
finally { $lockSteam.Dispose(); $lockOd.Dispose() }
Assert 'discover: a library that cannot be listed is reported' (@($script:Unchecked | Where-Object { $_ -like "Steam: *lib2\steamapps unreadable*" }).Count -eq 1)
Assert 'discover: a nested folder that cannot be listed is reported, and the rest still loads' (@($script:Unchecked | Where-Object { $_ -like 'Origin: *LocalContent\Denied unreadable*' }).Count -eq 1)
Put "$tmp\rc\a\nvngx_dlssnr.dll" 'held'; Put "$tmp\rc\b\nvngx_dlssnr.dll" 'fakemodel'; Put "$tmp\rc\c\x.txt" 'x'
$dlock = [IO.File]::Open("$tmp\rc\a\nvngx_dlssnr.dll", 'Open', 'Read', 'None')
$acl = Get-Acl -LiteralPath "$tmp\rc\c"; $acl.AddAccessRule($deny); Set-Acl -LiteralPath "$tmp\rc\c" -AclObject $acl
$lockRc = & $LockDir "$tmp\rc\c"
try { $script:ScanRoots = @("$tmp\rc"); $rc = (Do-Discover) | ConvertFrom-Json }
finally { $dlock.Dispose(); $script:ScanRoots = $null; $acl = Get-Acl -LiteralPath "$tmp\rc\c"; [void]$acl.RemoveAccessRule($deny); Set-Acl -LiteralPath "$tmp\rc\c" -AclObject $acl }
finally { $dlock.Dispose(); $lockRc.Dispose(); $script:ScanRoots = $null }
Assert 'discover: a game subfolder the runtime scan cannot list is reported' (@($rc.unchecked | Where-Object { $_ -like 'runtime scan:*rc\c*unreadable*' }).Count -eq 1)
Assert 'discover: an unreadable runtime candidate fails alone and the next is still reported' (@($rc.runtimeCandidates | Where-Object { -not $_.passes -and $_.reason -like 'unreadable*' }).Count -eq 1 -and @($rc.runtimeCandidates | Where-Object passes).Count -eq 1)
$dj = (Do-Discover) | ConvertFrom-Json
Expand Down Expand Up @@ -1944,8 +1978,8 @@ function Do-Selftest {
$cga = (Do-Assess $cg) | ConvertFrom-Json
Assert 'Steam with nothing disclosed anywhere: none-disclosed, no acknowledgement, caveat stated' ($cga.antiCheat.status -eq 'none-disclosed' -and -not $cga.acknowledgementRequired -and $cga.antiCheat.note -like '*not proof of no anti-cheat*')
New-Item -ItemType Directory -Force -Path "$cg\locked" | Out-Null
$acl = Get-Acl -LiteralPath "$cg\locked"; $acl.AddAccessRule($deny); Set-Acl -LiteralPath "$cg\locked" -AclObject $acl
try { $cgl = (Do-Assess $cg) | ConvertFrom-Json } finally { $acl = Get-Acl -LiteralPath "$cg\locked"; [void]$acl.RemoveAccessRule($deny); Set-Acl -LiteralPath "$cg\locked" -AclObject $acl; Remove-Item -LiteralPath "$cg\locked" -Force }
$lockCg = & $LockDir "$cg\locked"
try { $cgl = (Do-Assess $cg) | ConvertFrom-Json } finally { $lockCg.Dispose(); Remove-Item -LiteralPath "$cg\locked" -Force }
Assert 'a game folder the on-disk scan cannot list makes the status unknown' ($cgl.antiCheat.status -eq 'unknown' -and @($cgl.antiCheat.unchecked | Where-Object { $_ -like 'on-disk scan:*locked*unreadable*' }).Count -eq 1)
$script:SteamPages['444'] = '<div class="apphub_AppName">Unlisted Game</div>'
Put "$l2\steamapps\appmanifest_444.acf" (& $acf 444 'Unlisted Game' 'Unlisted')
Expand Down
Loading