Repository navigation
Add an opt-in duplication check with per-repository thresholds and an ignore block (#35) - #41
Conversation
… ignore block (#35) Duplicated logic is where two copies drift, and nothing flagged a new copy. lib/duplication.py is a standard-library copy-paste detector a repository vendors as scripts/dev/duplication.py and runs as the check-list step `duplication`. It is off, and its summary line says so, until the repository's jpdev.json declares a `duplication` member: the languages to read (go, javascript, python, shell, typescript), optional path and exclude globs, a token threshold and a line threshold. Each declared language is read as tokens with its own comment and string syntax, so layout and comments do not hide a copy. Runs of min_tokens tokens are hashed with a rolling hash and each run is compared with the previous place it appeared; a match is extended to the whole shared block and reported when each place spans at least min_lines lines. Two places in one file may not overlap. The report names each pair with both file:line ranges and the token count and exits 1. The code between comments holding duplication:ignore-start and duplication:ignore-end, in any comment syntax of the language, is left out; an unclosed, unopened or nested marker is reported. An invalid declaration is refused with exit 2 and a message that names the member. templates/dev.json carries the step, templates/jpdev.json and templates/jpdev.schema.json the member (generators is now required only when duplication is absent), and the generated-document checker accepts the member without reading it. README.md, docs/adopting.md, docs/policy.md and docs/generated-docs.md describe it. tests/duplication_test.py holds handwritten fixture repositories run through the command and through the template manifest's step, and checks that the schema and the command agree on every declaration vector; tests/run.sh runs it. Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both pull requests extend jpdev.json. The resolution keeps both:
- lib/generated_docs.py and scripts/dev/generated_docs.py accept the
members {version, generators, changed, duplication} and read generators.
- templates/jpdev.schema.json requires version and one of generators,
changed or duplication; its title names all three.
- lib/changed_checks.py accepts the duplication member (it refused every
member but version, generators and changed, so `jpdev check --changed`
would have refused templates/jpdev.json).
- The standard-library schema reader of tests/changed_checks_test.py reads
`enum`, which the duplication member's languages use.
- README "What is here" names the duplication check and thresholds.
Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jpdev.json declares the kit's duplication member: its Python and shell files at 100 tokens and 10 lines, leaving out scripts/dev/**, the vendored copies of templates/check.sh, lib/generated_docs.py and lib/handover_check.sh that are kept the same on purpose (the only pairs the tree has at those thresholds). dev.json runs `python3 lib/duplication.py` as the `duplication` step, the CI lint job runs it, and .github/ci-evidence.json lists it among the lint job's checks with lib/duplication.py and jpdev.json as inputs. tests/duplication_test.py reads the kit's own declaration, and runs the schema vectors through the standard-library reader of tests/changed_checks_test.py as well as jsonschema, so the vector test no longer skips where jsonschema is not installed. Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clean-room review (same vendor): Claude Opus 5.5, effort xhigh, 2026-10-25, reviewed SHA 7008b8aSame-vendor review at the maintainer's direction; not a cross-vendor claim. A fresh reviewer in a separate clone at the PR head; CI green. Condensed to its findings:
Disposition (the coordinator's)
The same reviewer re-reads the fixed head. |
…ks, links, size - A run of min_tokens tokens was compared only with the most recent place it appeared. When that pair spanned too few lines, earlier copies were never compared: two copies around a compact one went unreported. The 16 most recent places of each run are now kept and walked nearest first until one makes a reported pair; each block is still examined once. After a run that repeats itself in one file and is not reported, the other places of that file are passed over for that run, so a table of one short row is still not a copy of itself. - templates/jpdev.json selects `duplication` in every changed-path rule that can match a file of a declared language, as does the kit's own docs rule; docs/adopting.md says to do so; a test holds both files to it. - New fixtures: an ignore block elsewhere in a file does not hide a copy outside it; the code around two ignore blocks does not join; an ignore block inside a copy splits it. - A path is read only when it resolves to itself, so a tracked directory replaced by a link is not followed. - `max_file_bytes` (1024 to 104857600, default 1048576): a larger file is not read and is named; so is a file with a NUL byte in its first 8192 bytes. Running out of memory is a named refusal (exit 2). - The schema gives the integers `multipleOf: 1` and gains max_file_bytes; the standard-library schema reader of tests/changed_checks_test.py reads multipleOf. README says how the walk, the size and binary limits, here-documents, carriage-return line endings and a split copy behave. Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review re-read: 5dd5f44 (fix round)Same reviewer as the first record, same clean-room clone, moved to 5dd5f44. Same-vendor review, not a cross-vendor claim. Python 3.12.11, every run under a memory cap and a timeout; probes in a scratch copy only, no tracked file changed.
Verified: 41 duplication tests and 24 changed-checks tests pass; the kit's own duplication step passes over 21 files. The round-1 probes were re-run and the code broken ten ways (eight in the detector, two in the mappings); each break failed a test. No new HIGH. merge: yes (5dd5f44). |
|
🎉 Thank you for your contribution to Judgment Pack! If you’d like another task, choose a path that interests you: |
What it adds
duplicationcheck-list step (lib/duplication.py, python3 standard library, vendored by a repository asscripts/dev/duplication.py). It is off, and its summary line saysduplication: off: ..., until the repository'sjpdev.jsondeclares aduplicationmember:languages(any ofgo,javascript,python,shell,typescript), optionalpathsandexcludeglobs (*and?within one segment,**as a whole segment; default**),min_tokens(10 to 10000) andmin_lines(1 to 10000). The files read are tracked or untracked-and-not-ignored files of the declared languages' extensions; symbolic links are not followed.min_tokenstokens is hashed with a rolling hash and compared with the previous place the same run appeared; the match is extended both ways to the whole shared block, examined once, and reported when each place spans at leastmin_lineslines. Three copies make two pairs (each against the one before it). Two places in one file may not overlap, so a repeated short pattern is not a copy of itself. A generated tree of about 3.6 million Go tokens ran in about 5 s and 380 MB.file:first-lastranges and the shared token count, and exits 1:duplication:ignore-startand a comment holdingduplication:ignore-endis left out, in any comment syntax of the language (//,/* */,#); a barrier stands in for it so no block joins the code around it. The marker text in a string is not a marker. A start that is never closed, an end with no start and a start inside an open block are reported (exit 1,file:line).duplication: refused: jpdev.json duplication.min_tokens: an integer from 10 to 10000 is required (got 5).templates/dev.jsoncarries the step (aftergenerated-docs);templates/jpdev.jsonshows the member;templates/jpdev.schema.jsonadds it, and the root now requiresversionand one ofgenerators,changedorduplication(anyOf).lib/generated_docs.pyand its copyscripts/dev/generated_docs.pyaccept the member and do not read it. README has a "The duplication check" section,docs/adopting.mda "Duplication check" section,docs/policy.mdone sentence (duplication is where two copies drift; a repository opts in with its thresholds and marks an intentional copy with an ignore block), anddocs/generated-docs.mdnames the member.Checks
Candidate: head
7008b8a, no uncommitted diff. On top of5e8c374(the feature),91ebc02mergesmainatc122806(#40) and7008b8aopts the kit itself in. The whole list ran once on that head:The earlier run on
5e8c374alone (before the merge) was 7 passed, tests 429 passed, 0 failed.The merge with #40 (
91ebc02). Both pull requests extendjpdev.json. Git flagged two conflicts, the member set ofconfig()inlib/generated_docs.pyand its copy, and the rootanyOfoftemplates/jpdev.schema.json; both are resolved to the union ({version, generators, changed, duplication};versionplus one ofgenerators,changed,duplication). Two more did not conflict textually but broke a feature, and are fixed in the merge:lib/changed_checks.pyrefused every member exceptversion,generatorsandchanged, sojpdev check --changedwould have refusedtemplates/jpdev.json, and now acceptsduplication; the standard-library schema reader intests/changed_checks_test.pydid not knowenum, which thelanguagesmember uses, and now reads it. Probes on a scratch copy: droppingduplicationfrom the changed-path members fails the shipped-mapping test fortemplates/jpdev.jsonandjpdev.json(refused at load); ignoringenumin the reader fails the duplication schema test (unknown languageaccepted); dropping either member from the generated-document set fails that member's test.The kit on itself (
7008b8a).jpdev.jsondeclares{"languages": ["python", "shell"], "exclude": ["scripts/dev/**"], "min_tokens": 100, "min_lines": 10}, anddev.jsonrunspython3 lib/duplication.pyas theduplicationstep. The CI lint job runs it, and.github/ci-evidence.jsonlists it among the lint checks, withlib/duplication.pyandjpdev.jsonas inputs. At those thresholds the only pairs in the tree are the vendored copies underscripts/dev/(scripts/dev/check.shandtemplates/check.sh, 1816 tokens;lib/generated_docs.pyand its copy, 1518;lib/handover_check.shand its copy, 184), which are excluded. At 50 tokens and 5 lines, five pairs inside test files would be reported. The schema vector test now also runs through the standard-library reader, so it no longer skips where jsonschema is missing.Focused runs during development:
tests/duplication_test.py29 tests OK under Python 3.12 (jsonschema installed, so the schema vector test ran) and under 3.8;tests/generated_docs_test.py20 tests OK against both helper copies;scripts/dev/check.sh --only diff-check,local-paths,json,bash-n,shellcheck.tests/duplication_test.py(run bytests/run.sh) holds handwritten fixture repositories with hand-counted ranges and token counts: a copy above the threshold fails and names both places; the token threshold is inclusive (29 reported, 30 not); the shorter place decides the line threshold; a Go copy laid out and commented differently is found; three copies in one file make two pairs; a repeated short pattern is not reported; the ignore block suppresses a copy in Python (#) and Go (/* */start,//end); the marker in a string is not a marker; the three marker problems; per-language tokenizer cases; off with nojpdev.jsonand with no member; languages,paths,exclude, ignored files and links; 23 refused declarations by exact message (empty, missing and oversized among them) and through the command; and the template manifest's own step run bytemplates/check.shin a fixture repository (FAIL row with the pair in the summary block). The schema and the command agree on every vector (members, enum and bounds compared directly; the good and the 23 bad vectors validated with jsonschema when it is installed).Safeguard probes. Each broke a scratch copy of
lib/duplication.pyonce (run withDUPLICATION_HELPER), the clone unchanged:min_lines7)min_tokens9 and 10001,min_lines0 and 10001:Refused not raised) and the command refusal test (exit 1 instead of 2)threshold(Refused not raised)Schema probes (self-check b), each on a scratch copy of the schema: removing the
shellenum value, theexcludemember, theduplicationbranch ofanyOf, the glob pattern, or themin_linesmaximum each fails the schema tests.Self-checks: (a) not applicable,
dev.jsondeclares no handover paths; (b) above; (c) the schema and the command read the same vectors and agree; (d) no mutation rows in this repository; (e) refusals tested for empty, missing and oversized members; (f) no checkpoint; (g) README, adopting guide, policy sentence and module docstring read against the code. CI owns the exact-head run of the same list.Not in this PR
Fix round
Candidate: head
5dd5f44(one commit on7008b8a), no uncommitted diff. It answers the clean-room review of7008b8a.examinedstill extends each diagonal block once. After an unreported run that repeats itself in one file, that file's other places are passed over for the run; other files are still comparedlib/a.js, compactlib/b.js,lib/c.js; also with the compact one sorting last) and the same-file variant (spread, compact, spread):a.js:2-8 and c.js:2-8andone.js:2-8 and one.js:14-20, 31 tokens each. With 1 recent place (the old behaviour) both fail. With the same-file pass-over removed, the repeated-pattern test failschangedrule selectedduplicationdocs/**), as does the kit's own docs rule.docs/adopting.mdsays to do sotemplates/jpdev.jsonandjpdev.jsonagainst sample paths the declaration reads. Dropping the step from the template'sgo/**rule or from the kit's docs rule fails itreport.py:9-15); two blocks whose surrounding code is the same 8 + 8 tokens (not joined); a block inside a copy splits it (14 tokens reported, not 29)path.resolve()equals the path below the resolved rootinner/replaced by a link to a directory outside the root: not read. Removing the check fails the testmax_file_bytes(1024 to 104857600, default 1048576; schema and refusals included): a larger file is named on anot read:line and is not read. MemoryError is the refusalout of memory reading the files in scope; put large generated files in exclude or lower max_file_bytes(exit 2)multipleOf: 1on the integers (redundant withtype: integer, so no vector separates it; the standard-library reader intests/changed_checks_test.pyreads it). README sentences on the split copy, here-document markers and carriage-return-only files, each with a testThe detector on the earlier generated tree (about 3.6 million tokens) took 7.8 s and 380 MB after the change, against 5.4 s before.
The whole list ran once on
5dd5f44:Focused runs:
tests/duplication_test.py41 tests OK,tests/changed_checks_test.pyOK,tests/generated_docs_test.pyOK. CI owns the exact-head run.Closes #35
🤖 Generated with Claude Code