Skip to content

Build1 publisher3 min readPublished

phpcpd-next relicensed to MIT after its provenance gate counted zero inherited headers

The rewritten PHP copy/paste detector reached Packagist on September 13, and the script in its release gate reads the header of every file in src/ and exits non-zero while one copyright holder is still inherited.

The Engineer · Build desk

Illustration accompanying phpcpd-next relicensed to MIT after its provenance gate counted zero inherited headers

What happened

  • phpcpd-next 2.0 was published on Packagist on September 13, 2026, continuing phpcpd, the PHP copy/paste detector archived in 2023, under the same command and, per the project, none of the same code.
  • The release gate runs a provenance script that fails the build while any file still carries an inherited copyright header, and the project reports that count at zero.
  • The package started life as a BSD-3-Clause fork and 2.0 ships under MIT with no BSD text included, with NOTICE crediting the original project as ancestry.
  • A third detection engine arrives behind --algorithm=unified, covering all four clone types from one anchor set and naming the divergent token ranges on both sides of a near miss, replacing the suffix-tree lane.
  • Every string the tool prints now comes from locale/ in twenty-eight languages, with partial translations allowed and untranslated keys falling back to English one key at a time.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • precedent A relicensing claim a script can fail gives downstream reviewers something concrete to ask of other forks: an inventory the build checks, instead of an assurance in the README.
  • constraint Anyone who needs assurance that the implementation itself is new still has to read diffs, because the gate inspects headers while the byte-identical output comparison rests on the project's account.
  • decision Teams pinned to the archived tool now pick between an unmaintained BSD package and a same-named MIT one with different defaults, and the command line does not change either way.
  • exposure A CI job running 2.0 is trusting a detection heuristic to decide scan scope, since the preset adds exclusions that change which duplication a gate can see at all.

bench/check-provenance.php opens every file in src/ and counts the copyright holders each header names [4]. It establishes that no inherited attribution string is left in that directory, and nothing about whether the implementation under the header is new.

The rewrite claim is separate. According to the release post, every file was rewritten from its specification, and each was proved to emit byte-identical output before its attribution changed [6]. That comparison is per file, and it ran before the header changed, so the set of files with inherited headers was also the set of files not yet rewritten.

The licence move is not a fix for a violation. The post says attribution alone satisfied BSD-3-Clause and the package was always correct as it stood [7]. It describes the relicence as the completion of a plan the project set itself: a tree with no inherited surface left, relicensable in one commit, with the inventory as its evidence [9].

Two engines run without flags, Rabin-Karp for exact copies and a SourcererCC token bag for reordered ones [10]. The post also lists dead-code detection as a second mode, confidence ranking, an acknowledgment ledger, SARIF output and corpus triage [14]. It ties the version number to the size of the change: "It is a major version because a 1.x number would have been a lie about how much moved" [15].

The sample Laravel run is where I would slow down. It reports one clone, 30 duplicated lines across two files, six scanned files, 19 excludes, and 33.33% of 90 scanned lines duplicated [16]. 30 divided by 90 is 33.33%, so the percentage counts the clone once, while the two copies together account for 60 of those 90 lines [17]. A threshold carried over from another tool may be measuring the other quantity. The run took 0.008 seconds and 4.00 MB, six files at 706.9 files per second [18]. Six files is not a population. That rate transfers only if your files resemble those six in size and your excludes leave a comparable share of the tree in scope.

The preset that produced those 19 excludes applies itself, prints that it did, and names the flag that disables it [20]. It requires two independent signals: laravel/framework in require, plus a structural marker such as artisan or bootstrap/app.php. require-dev is ignored on the grounds that a package which tests against Laravel is not an application built on it [19]. Each exclusion encodes a specific failure. Blade templates go because *.blade.php is not analysable PHP source. The database/migrations directory goes because up() and down() boilerplate is duplicate by design. _ide_helper.php and the generated caches go because a generated file is a dense index of the very identifiers an orphan scan searches for, so scanning one can turn a failing gate green [21].

Dead-code findings stay out of the exit code: the orphaned-symbol section in the sample run is marked advisory [22]. A second script, bench/check-locales.php, reports each translation's coverage and fails on a key English does not have or a placeholder that drifted [13]. All of this is the project's own account, and no independent review of the rewrite has been published.

What to watch

  • Whether anyone outside the project diffs 2.0's src/ against the archived phpcpd and tests the byte-identical output claim.
  • Whether the provenance and locale scripts stay blocking in the release gate once translation coverage starts lagging behind new keys.
  • Whether distributions and CI templates repackage the MIT release or keep the BSD-era package pinned.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories