Skip to content

feat(code-metrics): clone classes, explicit caps, registry cluster lines, and rollups for audit-duplication - #4111

Draft
kyle-sexton wants to merge 17 commits into
mainfrom
claude/code-metrics-duplication-audit-0z4y1i
Draft

feat(code-metrics): clone classes, explicit caps, registry cluster lines, and rollups for audit-duplication#4111
kyle-sexton wants to merge 17 commits into
mainfrom
claude/code-metrics-duplication-audit-0z4y1i

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Closes #4068

Summary

/code-metrics:audit-duplication reported detector pairs as groups, so eighteen byte-identical copies of hook-utils.sh arrived as seventeen rows and the fragment's lines counted seventeen times; a 1.4 MB minified bundle was silently left out of the scan by jscpd's own size guard; the root canonical of a vendored file could not be declared in the sanctioned-replication registry; and the markdown read like a size report (Files. Functions. Over reference.) with no way to see which lane or directory carried the duplication. This PR fixes all of that in the code-metrics plugin (0.2.3 to 0.2.4), adds the registry cluster-line grammar this repository needed, and records the five clusters.

Fix

  • Explicit caps, reported instead of hidden. duplication.max_size (default 1mb, binary units) and duplication.max_lines (default null) are applied by the jscpd adapter before the tool runs, because jscpd 4.x and 5.x disagree on their --max-size/--max-lines defaults and on what 0 means, and neither names a skipped file. A skip makes the lane's run row partial (count and largest file), the document partial, and the summary carries a Partial: line. Both majors are translated; reference/collectors.md pins 5.2.0 and records 4.3.0.
  • Clone classes. New cluster-clones.py merges pair rows that share an identical (file, start_line, end_line) and equal lines into one row per class (union-find, instances sorted by root-relative path, clustered label); lines count once. Overlap without identity stays separate, so a class is never wider than what the detector called identical.
  • Registry cluster lines. <canonical> -> <member>... names a root-relative canonical copy and the plugin paths or gitignore-style globs that carry it. registry-filter.py matches root-relative (a run from a subdirectory matches the same lines), first matching line wins; the drift checker and replica-collapse.py skip such lines. Five annotated cluster lines are added for the sync-declared root lib/ canonicals (ADR 0033).
  • Report. summary.by_lane and summary.by_directory (additive v1 fields, cumulative ancestors of each class's first instance, computed after exclusion); duplication rows listed largest first; a ## Rollup section to duplication.rollup_depth (default 2); Files with clones: N.; an empty exclusion list is stated with its reason; one headline with the install hint when no clone detector ran, and the skill offers the install without performing it; run rows gain an additive hint field.
  • Docs. SKILL.md, README, CHANGELOG [0.2.4], reference/config.md, reference/report-schema.md, setup template and bundled defaults; the claude-config audit-pass exclusion-set reference describes both registry line shapes (claude-config 0.42.4, wording only).

Two plan corrections came from live runs and are recorded in the plan: jscpd emits a star for N copies (each later copy paired with the first, same range every time), so three full copies at different offsets are one class and the "offset" fixture is a partial third copy; and the surviving root lib/ groups after exclusion are all lib/*.test.sh per-suite harness boilerplate that no sync script declares, so they are a finding, not sanctioned replication.

Verification

  • scripts/affected-tests.sh --run over the branch: every shell suite green except plugins/claude-ops/skills/plugins/scripts/cache-content-check.test.sh, whose process-budget PS4 trace probe fails identically on an untouched tree in this sandbox (the pid-stamped PS4 did not reach the traced shell); the NOT RUN Python and Node suites pass in their own lanes (932 pytest cases across the twenty selected suites; the autonomy fixtures .mjs, 544 checks) except plugins/session-flow/scripts/tests/test_save_point.py::test_new_origin_falls_back_to_directory_name, which fails identically on a detached origin/main worktree and touches no file this branch changes.
  • code-metrics suites on the merged tree: dispatch.test.sh 79 cases, audit-duplication.test.sh 27 cases, test_report.py 42, test_registry_filter.py 20, test_cluster_clones.py 13, test_jscpd.py 15, test_replica_collapse.py 8, test_pathglob.py with the shared root_relative cases; check-cross-plugin-source-drift.sh --check exit 0 and its test 15 cases; check-code-metrics-config-reference.py 28 keys; check-changelog-parity.sh --check-bump origin/main, --check, and --check-order exit 0; check-changed-skills.sh origin/main and the catalog check pass.
  • Lint: pinned ruff check and format, shellcheck, shfmt, markdownlint over every changed markdown file, the em-dash gate, and the ai-slop detector over the instruction surfaces, all clean. CI's lint lane on an earlier head found three hygiene defects (exec bits on two new scripts, final newlines on three regenerated captures, the skill description over 1024 codepoints), fixed in d91a5971; ci-lanes reported all lanes passed on that head.
  • Live runs with jscpd 5.2.0 and 4.3.0 on this tree (--all --registry scripts/cross-plugin-source-registry.txt): before, 919 pair rows and 75267 duplicated lines with the miro bundle silently skipped; after, 722 classes and 11364 lines under 5.2.0, each of the five cluster lines excluding its class (18, 7, 4, 2, 2 instances), by_lane and by_directory identical from the repository root and from lib/, and the partial typescript row naming plugins/miro/dist/index.min.js when the default **/dist/** exclusion is lifted.
  • Review and outcome verification: the section below the plan.

Related

Approved plan, Brief and dogfood (the full PLAN.md with the five phases is in the branch history at `beb6169d:docs/topics/code-metrics-duplication-audit/PLAN.md`; the contract slice is pruned at close-out)

Brief

Scope-change note (2026-09-11, before approval, from the fresh-context plan review and the
stress-test, each finding verified against the tree): four criteria below were corrected. The
hook-utils.sh class has eighteen instances (root plus seventeen plugins), not seventeen. The
shipped 1mb cap skips one tracked file here, plugins/miro/dist/index.min.js (1.45mb), so the
typescript lane reads partial by design. The hook-telemetry-sink.sh pair differs at line 51
and has no sync script, so it is not sanctioned replication and its registry line is dropped
(five lines, not six). Pair-to-class merging joins byte-aligned copies; a copy embedded at a
different offset with different surrounding lines stays its own group. Cluster lines carry an
explicit -> marker because a registered path may contain a space.

TLDR

  • audit-duplication reports skipped files instead of hiding them: the jscpd adapter pre-filters
    by byte size and line count, passes both caps explicitly on 4.x and 5.x, and marks the lane
    partial when anything was skipped; defaults are no line cap and a 1mb byte cap.
  • jscpd's pair reports are merged into clone classes before summarizing, so byte-aligned copies
    count once.
  • The sanctioned-replication registry gains cluster lines
    (<canonical path> -> <copy path or glob>...) so a canonical file outside any plugin can declare
    its copies; this repo's drift checker skips marked lines and five such lines are added here.
  • The markdown report sorts clone rows by duplicated lines, adds per-lane and per-directory rollups
    (cumulative, counts beside share, computed after exclusion, listed to depth 2), and the JSON gains
    summary.by_lane / summary.by_directory as additive v1 fields with an explicit ignore-unknown
    rule in the schema reference.
  • The duplication summary line drops "Functions" and "Over reference"; a run with no detector prints
    one consolidated install headline and the skill offers, never performs, the install. Version 0.1.9.

Goal

A whole-tree or change-scoped duplication audit on any repository, this one included, produces
numbers a reader can act on without re-aggregating: no file is silently dropped by a size cap,
byte-aligned copies of a fragment form one group counted once, replication the repository declares
about itself (including copies of a root-level canonical file) is excluded and shown as an
exclusion, and the report says where the surviving duplication sits by lane and by directory. On
this repository the audit reads clean apart from genuine duplication.

Constraints

  • The plugin never installs, downloads, or npx-fetches a detector; SKILL.md may instruct Claude to
    offer the install command and run it only on the user's confirmation.
  • The report emits no finding, severity, or exit-code gate; duplication has no reference value.
  • The code-metrics/v1 schema string is unchanged; every JSON change is additive (new optional
    fields), and reference/report-schema.md states that readers ignore unknown keys.
  • Single-token registry lines keep their exact meaning, including a path that contains a space;
    the drift checker (scripts/check-cross-plugin-source-drift.sh) keeps its behavior for them and
    skips only lines carrying the -> marker.
  • Both jscpd 4.x (latest-4 = 4.3.0) and 5.x (5.2.0) stay supported by the adapter; the adapter
    always passes explicit --max-lines and --max-size on both majors, treats a configured 0 as
    null (jscpd reads 0 as "default" for 4.x lines and "skip all" everywhere else), and never
    emits a 0 cap.
  • An unmeasured value is null, never 0; a run that measured nothing keeps "Measured nothing".
  • Validate with scripts/affected-tests.sh --run; every changed file maps to at least one suite.
  • No lib/hook-utils.sh or other cross-plugin synced source is edited; every cluster line mirrors
    the src= and copy list its scripts/sync-*.sh already declares.
  • One issue, one draft PR whose body opens with Closes #<issue> and carries the four required
    sections; CHANGELOG entry under [0.1.9].

Acceptance criteria

  • Running audit-duplication.sh --json --all on this repository with jscpd 4.3.0 and again with
    5.2.0 yields, for every byte-identical whole-file class, the same set of instance files per
    group; tokens, instance order, and the exact line range are excluded from the comparison
    because the two majors tokenize differently and hub on different copies (5.x drops a leading
    comment block from the clone, so a file that opens with one is reported from its first code line
    under 5.x and from line 1 under 4.x; the verifier found one such class,
    resolve-hook.mjs in two knowledge skills). The bash row reads ok under both; with the
    repository's default **/dist/** scope exclusion lifted, the typescript row reads partial
    under both, naming plugins/miro/dist/index.min.js as the one file over the 1mb cap; with the
    exclusion in force (this repository's shipped team file) the bundle never reaches the cap and the
    row reads ok.
  • duplication.max_lines defaults to null (no cap) and duplication.max_size to 1mb; both are
    documented in reference/config.md (gated against config-defaults.json) and exported to the
    adapter, and the adapter's tests cover the explicit-cap argv on both majors, the 0-means-null
    rule, and that no 0 cap is ever passed.
  • IF every file in a lane is skipped by the caps, THEN that lane's run row reads partial with the
    reason, no collector is invoked for it, and the script never exits 3 for that cause.
  • The eighteen byte-identical copies of hook-utils.sh (root lib/ plus the seventeen plugin
    copies scripts/sync-hook-utils.sh --print-manifest lists) produce exactly one clone group with
    eighteen instances, and with the registry line
    lib/hook-utils.sh -> plugins/*/hooks/hook-utils.sh that group appears once under excluded[]
    with duplicated_lines counted once.
  • Two groups whose instances overlap without identical line ranges (the hook-telemetry-sink.sh
    shape, and a third copy that carries only part of a fragment two full copies share, so the
    detector names the first copy with two different ranges) stay separate groups after the merge.
    Three full copies at different line offsets are one class: jscpd pairs each later copy with the
    first and names that first copy with the same range in every pair (verified against 5.2.0).
  • A cluster line excludes a group only when every instance's root-relative path matches the
    canonical path or one of the members (literal or glob) and the instances' directories are all
    distinct; two copies inside one directory still count as duplication; a single-token line behaves
    exactly as before, a registered path containing a space included; when a single-token line and a
    cluster line both match, the first matching line in file order wins.
  • scripts/check-cross-plugin-source-drift.sh --check passes on this repository with the five new
    cluster lines present, each under its own annotation block, and its tests cover a marked line
    being skipped.
  • After this change, audit-duplication.sh --json --registry scripts/cross-plugin-source-registry.txt --all
    on this repository reports zero surviving groups whose instances include a root lib/ file that
    a sync script declares as its canonical copy (scripts/sync-*.sh --print-manifest src), from
    the repository root and from a subdirectory alike. Groups over the per-suite test-harness
    boilerplate that lib/*.test.sh files share with plugin test files survive: no sync script
    declares them and the shell-test-helpers convention keeps those helpers per file, so they are
    the audit's finding, not sanctioned replication.
  • WHILE no registry is configured and none is passed, the report's excluded[] is empty and the
    summary states that no registry was configured.
  • The markdown Measures table for a duplication document lists clone groups in descending order of
    duplicated lines, and the report carries a ## Rollup section with a per-lane table and a
    per-directory table (rows to depth 2 by default, duplication.rollup_depth configurable) whose
    numbers are cumulative up the tree and carry groups and duplicated_lines; the per-lane
    values sum to summary.duplicated_lines, and the root row of by_directory equals it.
  • The JSON summary carries by_lane and by_directory with the same numbers (empty maps when a
    duplication collector ran and found nothing); schema is still code-metrics/v1;
    reference/report-schema.md documents both fields, the run row's additive hint field, and
    states that readers ignore unknown keys; verification:measure, the one marketplace consumer,
    reads only status and is unaffected.
  • The duplication summary line reads Files with clones: N. followed by the duplicated-lines and
    exclusion lines, with no "Functions" or "Over reference" text; the sibling skills' summary lines are
    byte-identical to today's.
  • When no duplication collector resolves, the markdown opens with one headline naming the install
    command and /code-metrics:setup, taken from the run row's hint field rather than parsed out of
    its reason, and SKILL.md instructs Claude to offer the install and run it only on confirmation.
  • The jscpd adapter's docstring and reference/collectors.md state that 4.x and 5.x are both
    translated, pin 5.2.0, note the kind field, and record that the two majors tokenize differently;
    the schema reference names "intentional clones" beside "sanctioned replication".
  • plugin.json carries the next patch version above the one on the default branch at merge time
    (0.2.4 over the 0.2.3 main shipped while the PR was open; the Brief was drafted against 0.1.8) and
    CHANGELOG.md carries the matching entry covering every item above; scripts/affected-tests.sh --run exits 0, or exits 3 with only Python and Node suites listed as not run, each of which then
    passes in its own lane, or exits 1 only for a suite that fails identically on a detached
    origin/main worktree and touches no file this change edits (named in the PR body).

Captured assumptions

  • Version bumps to 0.1.9, not 0.2.0, because this plugin's changelog bumps patch for features
    (0.1.7 was a feat). Revisit if the marketplace's release convention says a new registry grammar
    or JSON fields require a minor bump.
  • Once-per-class counting follows PMD CPD and SonarQube; the fetched literature is silent on how
    duplicated lines are totalled, and jscpd v5 is the one tool that sums per pair. Revisit if a
    standard sets a duplicated-lines definition.
  • The merge keys on identical (file, start_line, end_line) instances and equal lines, so only
    byte-aligned copies join; jscpd 5.x hubs every later copy on the first and names it with the same
    range, so full copies at different offsets are one class, while 4.x hubs on the last input and
    extends into shared flanking lines, so under 4.x the same copies can stay two groups (a live
    probe of three offset copies gave one class under 5.2.0 and two groups under 4.3.0). Closure is exact only for
    type-1/type-2 clones, which is all the adapter receives because it passes no --max-gap-lines.
    Revisit if similar clones are ever enabled.
  • Merged instances are sorted by root-relative path (cluster-clones.py --root) so the first
    instance, and therefore by_directory attribution, is the same under 4.x (which hubs on the last
    input) and 5.x (which hubs on the first), and the same whichever directory the run started from.
  • rollup_depth default 2 is the plugin's choice; no upstream sets a depth (SonarQube and Codacy
    roll up every directory). Revisit if a consuming repository's layout makes depth 2 meaningless.
  • The byte cap of 1mb aligns with jscpd 5.0.7's parser guard and SonarJS's 1000kb generated-code
    rule, and means 1,048,576 bytes, the value jscpd 5.2.0 reports for 1mb; no line cap by default
    aligns with jscpd 5, PMD CPD, SonarQube, and Linguist. Revisit if jscpd changes its default or
    multiplier in a later major.
  • The cluster lines live in the existing registry file because the plugin's own documentation names
    that file as the shape; this is a repository-convention choice with no external authority.
    Revisit if the drift checker grows a second consumer of the file.
  • The -> marker is the cluster-line signal because a registered single-token path may contain a
    space and the drift checker's tests protect that case. Revisit if a consuming repository has a
    path containing ->.
  • The .claude/hooks/hook-telemetry-sink.sh and plugins/claude-ops/hooks/hook-telemetry-sink.sh
    pair differs at line 51 and has no sync script, so the audit keeps reporting its overlap as
    duplication; a future sync script earns it a cluster line. Revisit when that script exists.
  • "Sanctioned replication" stays the plugin's term; the literature's term "intentional clones"
    (Cordy 2008) is named beside it. No upstream tool models the canonical-plus-copies relation, so
    no vocabulary conflict exists.
  • The upstream doc drift found in jscpd (docs/rust.md "no limit" for --max-size, "per block"
    help text for --max-lines, stale jscpd.dev v5 defaults) is reported separately, not here.

Out-of-scope

  • Installing a detector on the user's behalf, or an npx fallback.
  • A pass/fail gate, threshold, or severity for duplication.
  • Cross-language clone detection.
  • Changing dupl or cpd adapters beyond passing their rows through the new merge and rollup
    steps unchanged.
  • A second registry file or a manifest generated from the sync scripts.
  • Migrating the registry's single-token lines to the new grammar.
  • A repository config excluding plugins/miro/dist/ from this repo's own audits; the partial
    row is the designed reading and a config is the consuming repo's choice.

Deferred questions

  • None. Every question registered in the interview was answered; no row was deferred or blocked.

Dogfood

Detector Status Surviving classes Duplicated lines Files with clones Excluded Partial lanes
jscpd 5.2.0 partial 742 11831 487 14 (the five cluster lines: 18, 7, 4, 2, 2 instances) typescript (1 of 295 files over 1mb: plugins/miro/dist/index.min.js)
jscpd 4.3.0 partial 625 10926 429 14 typescript (same file)

Before this change the same 5.2.0 run reported 919 pair rows and 75267 duplicated lines with no
skipped file named. After the merge with the 0.2.1 line of the plugin (default scope.exclude
globs, the other lane, scope.registries in this repository's team file) the 5.2.0 run reads
complete with 722 classes, 11364 duplicated lines, 492 files with clones, and 16 exclusions: the
miro bundle now falls under the default **/dist/** exclusion before the cap is reached, so no
lane is partial; pass --all on a tree without that exclusion, or set scope.exclude: [], to
see the cap and the partial row. The two majors tokenize differently, so their counts are not comparable with
each other. The largest surviving classes are per-suite test-harness boilerplate (pass/fail
helpers shared by lib/*.test.sh and plugin test files), which no sync script declares and the
shell-test-helpers convention keeps per file: a finding for the operator, not sanctioned
replication.

Review and outcome verification

Outcome verification (/verification:confirm, a fresh-context verifier auditing the acceptance criteria against the diff with live jscpd 4.3.0 and 5.2.0 runs): thirteen of fifteen criteria passed on direct evidence. The two failures were criteria whose wording the tree contradicted, not the code: the plugin version (the Brief named 0.1.9; main was already at 0.2.1 and shipped 0.2.2 and 0.2.3 while this PR was open, so the change ships as 0.2.4), the typescript partial row being observable only with the repository's own default **/dist/** exclusion lifted, one byte-identical class that jscpd 5.x names from its first code line where 4.x names it from line 1, and the two pre-existing suite failures listed above. The criteria are amended to those facts in the plan. One observation outside the criteria was a real defect and is fixed on the branch: the class merge sorted instances by cwd-relative path, so summary.by_directory attributed a class differently from lib/ than from the root; the merge now takes --root, sorts root-relative, and the rollups are identical from both.

Code review (/review:quality-gate code, fresh-context reviewer over the merge-base diff): no CRITICAL finding. One IMPORTANT finding, fixed on the branch: with no line cap configured the jscpd adapter passed --max-lines 1000000, a number a generated file can reach, so such a file would have been skipped by jscpd's own gate with nothing named; the sentinel is now the signed 32-bit maximum, which both majors accept. Of two SUGGESTION items, one was taken (the root-relative path helper now lives in pathglob.py and is shared by the summarizer, the registry filter, and the class merge) and one left as it is (the skill script's integer reader falls back silently on a float or quoted number for rollup_depth).

🤖 Generated with Claude Code

https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N

Interview contract for the six audit-duplication gaps a whole-tree run
exposed: size-cap skip detection and defaults, pair-to-class merging, a
registry grammar for canonical copies outside a plugin, report sort and
rollups with additive JSON fields, the duplication summary line, and the
no-detector experience. Brief only; the Plan section is filled next.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
Five phases under the locked Brief (caps end to end, pair-to-class merge,
registry cluster lines with the drift-checker skip, report rollups and
summary line, docs and release), plus the Tier B design early-exit with
the type sketch the phases build to. Draft pending the fresh-context plan
review and the stress-test; approval comes after both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
…d stress-test

Fold in the verified findings from the fresh-context plan review and the
devil's-advocate pass: eighteen hook-utils instances, the 1.45mb minified
bundle that the 1mb cap skips by design, the dropped telemetry-sink line
(the pair differs and has no sync script), a `->` marker for cluster lines
so a registered path with a space keeps working, dirname-based carrying
directories, root-relative instance paths, a `partial` zero floor, an
additive run-row `hint`, path-sorted merged instances, fixtures outside
the scoped tree, and the affected-tests exit-3 contract. Adds blast
radius, stress-test summary, execution shape, and handoff sections.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
Phase 1 of the duplication audit plan (docs/topics/code-metrics-duplication-audit/PLAN.md).

- Add duplication.max_lines (null), duplication.max_size (1mb), and
  duplication.rollup_depth (2) to the bundled defaults, the config
  reference, and the setup template.
- audit-duplication.sh exports CODE_METRICS_DUP_MAX_LINES and
  CODE_METRICS_DUP_MAX_SIZE beside the existing tunables; a null or 0
  cap exports empty.
- The jscpd adapter applies both caps itself before jscpd runs (stat
  size, binary newline count only when a line cap is set) and passes
  jscpd one above its own bound, because jscpd 4.x and 5.x disagree on
  the --max-size/--max-lines defaults and on what 0 means, and neither
  names a skipped file. Every skip is one line to
  CODE_METRICS_PARTIAL_REASON_FILE, or stderr when unset; zero files
  left is a zero measurement, not a failure. Both majors translate.
- dispatch.sh gives each collect a partial-reason file and writes the
  run row as `partial` with that line when it is non-empty; every run
  row gains an additive `hint` field carrying the first install hint a
  failed probe produced.
- registry-filter.py --zero-floor counts a `partial` duplication row as
  measured.
- collectors.md pins jscpd 5.2.0 and records the 4.3.0 maintenance line
  and the caps semantics; report-schema.md documents `hint`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
Phase 2 of the duplication audit plan.

jscpd (both majors) and PMD CPD report a clone as a pair, so N copies of
one fragment arrive as N-1 rows that all name the first copy, and the
summary counted the fragment's lines N-1 times. cluster-clones.py runs
between the dispatcher and the registry filter and joins pair rows that
share an instance with an identical (file, start_line, end_line) and
equal lines, keeping the first pair's values, the union of instances
sorted by path, and a `clustered` label. Overlap without an identical
range stays separate, so a class is never widened past what the
detector said was identical.

Fixtures under scripts/fixtures/clone-classes (three byte-identical
copies; two full copies plus one partial copy) with captures from a real
jscpd 5.2.0 run rewritten to repo-relative names. jscpd emits a star for
N copies and names the first copy with the same range in every pair, so
the plan's offset example is corrected to the partial-copy shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
…classes

Phase 3 of the duplication audit plan, committed alone because the
registry edit fans CI's test selection out.

- registry-filter.py reads a second line shape, `<canonical> -> <member>...`:
  a root-relative canonical copy and the plugin paths or gitignore-style
  globs that carry it. A clone group is excluded when every instance is
  the canonical or matches a member and the instances' directories are
  pairwise distinct. Instance paths are compared root-relative, so a run
  from a subdirectory matches the same lines, and the first matching line
  in file order wins. A plain line is still one path taken whole.
- check-cross-plugin-source-drift.sh skips ` -> ` lines, which key on a
  root path this check never sees; its test covers the skip.
- scripts/cross-plugin-source-registry.txt gains five annotated cluster
  lines for the sync-declared root lib/ canonicals: hook-utils,
  rewrite-guard, index-regen, resolve-convention-pattern, and
  parse-concern-value. The whole-tree audit now excludes each class
  (18, 7, 2, 2, and 4 instances) from the root and from a subdirectory.
- config.md and the claude-config exclusion-set reference describe the
  grammar; the fixture registry carries a commented example.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
…summary

Phase 4 of the duplication audit plan.

- report.py summarize adds `summary.by_lane` and `summary.by_directory`
  (every ancestor of each group's first instance, cumulative, made
  relative to --root) beside duplicated_lines and clone_groups;
  by_directory["."] and the per-lane sum both restate the totals. The
  zero floor states both as empty maps on a clone-free run.
- assemble reads a `partial` run row with no measure rows as `partial`,
  not `empty`: a lane that skipped every file said so in its row.
- render lists a duplication document's groups largest first, adds a
  `## Rollup` section (per lane, and per directory to --rollup-depth,
  default 2 from duplication.rollup_depth), summarizes as
  `Files with clones: N.`, states an empty excluded list with its
  reason, adds a `Partial:` line, and prints one headline with the
  install hint when no clone detector ran in any lane. Every other
  skill's document renders as before.
- report-schema.md documents the rollups, the ignore-unknown-keys rule,
  the partial status for a skipped lane, and intentional clones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
…ase 0.1.9

Phase 5 of the duplication audit plan.

- audit-duplication SKILL.md: the three new keys and the 0-means-null
  rule, both registry line shapes, the five exported tunables and why
  the jscpd adapter applies the caps itself, the no-detector headline
  and the offer-never-perform install rule, clone classes and the
  identity-not-overlap merge, the rollups and their root identity, the
  partial reading, and the gotchas for drifted copies and cross-major
  comparison.
- README row and known-gaps list; CHANGELOG [0.1.9]; plugin.json 0.1.9.
- The two-copy jscpd capture is regenerated under jscpd 5.2.0 with
  repo-relative names so no stale version string remains; the plan
  records the whole-tree dogfood numbers under both majors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
Resolves the code-metrics 0.2.1 line against the duplication audit work:
the plugin becomes 0.2.2; run rows keep the additive `hint` field on the
slot-based parallel dispatcher and the partial-reason file rides with
`launch_collect`; `report.py render` takes both `--document` and
`--rollup-depth`, sorts duplication rows largest first and every other
document by the upstream order; `scope.registries` is the key the skill
reads through the resolver, with the cluster-line grammar documented on
it; the drift checker drops cluster lines from the read-list entries so
the stale-baseline pass never sees them, and replica-collapse.py skips
them for the same reason with a test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
The sanctioned-replication registry is a contract three readers parse, so
the arrow line that declares a root canonical plus its copies is recorded
with the alternatives it displaced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
Review finding: with no line cap configured the adapter passed jscpd
--max-lines 1000000, a number a generated file can reach, so such a file
would survive the adapter's pre-filter and then be skipped by jscpd's own
gate with nothing named, the exact failure the pre-filter exists to close.
The sentinel is now the signed 32-bit maximum, which both majors accept
(verified against 4.3.0 and 5.2.0); tests and the collectors reference
follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
…ctory

Verification finding: cluster-clones.py sorted a class's instances by the
cwd-relative path the detector gave them, so a run from lib/ put
`../plugins/...` ahead of `hook-utils.sh` and the directory rollup
attributed the class elsewhere than a run from the root did. The merge now
takes --root and sorts by the root-relative path (the instances keep their
paths); the skill passes it. The root-relative helper the summarizer and
the registry filter each carried moves into pathglob.py and all three
share it. The plan's acceptance criteria are amended to the facts the
verifier established: the two majors can name a byte-identical class with
different line ranges (5.x drops a leading comment block), the typescript
partial row is observable only with the repository's default dist
exclusion lifted, and the version is the next patch above the default
branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
Close-out of docs/topics/code-metrics-duplication-audit: the Brief and the
dogfood numbers are published in the pull request body, the five-phase
plan and the design sketch stay in the branch history
(beb6169:docs/topics/code-metrics-duplication-audit/), the registry
grammar is ADR 0033, and the user-facing outcomes are the code-metrics
CHANGELOG [0.2.2] entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
Main shipped code-metrics 0.2.2 (the principles skill fix) while this
pull request was open, so this change moves to 0.2.3 with its changelog
entry above that one. The claude-config reference edit that describes the
registry cluster-line grammar now carries its own patch bump and entry,
which the version-reuse gate requires for any shipped plugin file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
CI's lint lane on the pull request head reported three hygiene failures:
the two new Python scripts carried a shebang without the exec bit, the
three regenerated jscpd captures ended without a newline, and the
audit-duplication description had grown past the Agent Skills field
maximum of 1024 codepoints. The description now states the same triggers
and scope in 963.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
Main bumped code-metrics to 0.2.3 and claude-config to 0.42.3 (the
description-prose sweep) while this pull request was open, the same
numbers this branch had taken, so the branch moves to 0.2.4 and 0.42.4
with its changelog entries above the ones main shipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QShQwS5yDYYToBmofdEY5N
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

code-metrics: fix six audit-duplication gaps a whole-tree run exposed

2 participants