Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package perl-Cavil-Matcher for 
openSUSE:Factory checked in at 2026-09-08 16:58:14
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/perl-Cavil-Matcher (Old)
 and      /work/SRC/openSUSE:Factory/.perl-Cavil-Matcher.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "perl-Cavil-Matcher"

Tue Sep  8 16:58:14 2026 rev:3 rq:1376326 version:1.50.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/perl-Cavil-Matcher/perl-Cavil-Matcher.changes    
2026-08-06 16:28:59.290628970 +0200
+++ 
/work/SRC/openSUSE:Factory/.perl-Cavil-Matcher.new.1265/perl-Cavil-Matcher.changes
  2026-09-08 17:01:54.726486500 +0200
@@ -1,0 +2,45 @@
+Fri Sep  4 09:40:56 UTC 2026 - Tina Müller <[email protected]>
+
+- updated to 1.50.0 (1.05)
+   see /usr/share/doc/packages/perl-Cavil-Matcher/Changes
+
+  1.05   2026-09-03
+          - Removed the on-disk fingerprint index (fp_build/fp_open, the 
Cavil::Matcher::Fingerprints
+            package, and Cavil::Matcher::FpIndex); consumers now keep the 
searchable index in their own
+            database. The fingerprint_file and content_hash primitives are 
unchanged.
+  1.04   2026-08-27
+          - FpIndex now caches opened segments and reuses them across searches 
instead of reopening every
+            segment on every query. Opening a segment is a fixed per-query 
cost that otherwise dominated search
+            time on a long-lived query server, regardless of the query. 
Segments are immutable append-only
+            files, so a cached handle always maps the same bytes; a handle 
whose segment has been compacted out
+            of the manifest is dropped on the next search. Results are 
unchanged.
+          - score() and FpIndex::search() take an optional minimum 
containment; matches below it are dropped
+            inside the scorer instead of being returned for the caller to 
filter. A query dominated by common
+            fingerprints can match hundreds of thousands of contents, almost 
all coincidental, so applying the
+            floor at this level avoids building and returning them. Defaults 
to 0 (return everything).
+          - score() and FpIndex::search() also take an optional max_df: query 
fingerprints appearing in more
+            than that many records of a segment are ignored (boilerplate that 
matches nearly everything, adding
+            only coincidental hits and skewing containment). Off by default 
(0). Intended for a compacted
+            single-segment index, where a segment's record count for a 
fingerprint is its document frequency.
+  1.03   2026-08-27
+          - Fingerprint scoring now reports, for each matched fingerprint, the 
query fingerprint value it was,
+            appended to each region tuple after the existing start line and 
span. This lets a caller map
+            matches back to their query positions - enough to tell an aligned 
copy from scattered coincidental
+            hits, and to derive each query fingerprint's document frequency 
from a full result set. Backward
+            compatible: the (start_line, span) pair at the front of every 
region tuple is unchanged.
+  1.02   2026-08-27
+          - Snippet code search: a new winnowed-fingerprint index for "where 
does this code already exist"
+            provenance queries, alongside the license-pattern matcher. 
fingerprint_file winnows a file's
+            tokens (k tokens per gram, w grams per window) into content 
fingerprints; a fingerprint segment
+            stores them keyed by a 128-bit content hash rather than filenames 
(so it stays lean and joins to
+            the Cavil database), and scoring ranks candidates by 
both-direction containment with exact
+            matched-line spans for highlighting.
+          - Cavil::Matcher::FpIndex is the pure-Perl lifecycle over a 
directory of fingerprint segments
+            (incremental add_segment, manifest/generation, memory-mapped 
shared reads), reusing the same
+            on-disk discipline as the pattern index: versioned, 
CRC-checksummed, structure-validated on
+            every open, with corrupt or foreign files rejected rather than 
mis-read.
+          - Byte-identical files are de-duplicated by content hash, so the 
same source shipped across many
+            package versions is stored once, with no package-name parsing.
+          - Additive only: the license-pattern format is unchanged and no 
migration is needed.
+
+-------------------------------------------------------------------

Old:
----
  Cavil-Matcher-1.01.tar.gz

New:
----
  Cavil-Matcher-1.05.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ perl-Cavil-Matcher.spec ++++++
--- /var/tmp/diff_new_pack.X8xdmx/_old  2026-09-08 17:01:55.467517475 +0200
+++ /var/tmp/diff_new_pack.X8xdmx/_new  2026-09-08 17:01:55.469517558 +0200
@@ -18,10 +18,10 @@
 
 %define cpan_name Cavil-Matcher
 Name:           perl-Cavil-Matcher
-Version:        1.10.0
+Version:        1.50.0
 Release:        0
-# 1.01 -> normalize -> 1.10.0
-%define cpan_version 1.01
+# 1.05 -> normalize -> 1.50.0
+%define cpan_version 1.05
 License:        GPL-1.0-or-later
 Summary:        Next-generation license pattern matcher for Cavil
 URL:            https://metacpan.org/release/%{cpan_name}

++++++ Cavil-Matcher-1.01.tar.gz -> Cavil-Matcher-1.05.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/Changes 
new/Cavil-Matcher-1.05/Changes
--- old/Cavil-Matcher-1.01/Changes      2026-08-04 20:28:44.956802618 +0200
+++ new/Cavil-Matcher-1.05/Changes      2026-09-03 12:55:39.857161295 +0200
@@ -1,5 +1,47 @@
 Revision history for Cavil::Matcher
 
+1.05   2026-09-03
+        - Removed the on-disk fingerprint index (fp_build/fp_open, the 
Cavil::Matcher::Fingerprints
+          package, and Cavil::Matcher::FpIndex); consumers now keep the 
searchable index in their own
+          database. The fingerprint_file and content_hash primitives are 
unchanged.
+
+1.04   2026-08-27
+        - FpIndex now caches opened segments and reuses them across searches 
instead of reopening every
+          segment on every query. Opening a segment is a fixed per-query cost 
that otherwise dominated search
+          time on a long-lived query server, regardless of the query. Segments 
are immutable append-only
+          files, so a cached handle always maps the same bytes; a handle whose 
segment has been compacted out
+          of the manifest is dropped on the next search. Results are unchanged.
+        - score() and FpIndex::search() take an optional minimum containment; 
matches below it are dropped
+          inside the scorer instead of being returned for the caller to 
filter. A query dominated by common
+          fingerprints can match hundreds of thousands of contents, almost all 
coincidental, so applying the
+          floor at this level avoids building and returning them. Defaults to 
0 (return everything).
+        - score() and FpIndex::search() also take an optional max_df: query 
fingerprints appearing in more
+          than that many records of a segment are ignored (boilerplate that 
matches nearly everything, adding
+          only coincidental hits and skewing containment). Off by default (0). 
Intended for a compacted
+          single-segment index, where a segment's record count for a 
fingerprint is its document frequency.
+
+1.03   2026-08-27
+        - Fingerprint scoring now reports, for each matched fingerprint, the 
query fingerprint value it was,
+          appended to each region tuple after the existing start line and 
span. This lets a caller map
+          matches back to their query positions - enough to tell an aligned 
copy from scattered coincidental
+          hits, and to derive each query fingerprint's document frequency from 
a full result set. Backward
+          compatible: the (start_line, span) pair at the front of every region 
tuple is unchanged.
+
+1.02   2026-08-27
+        - Snippet code search: a new winnowed-fingerprint index for "where 
does this code already exist"
+          provenance queries, alongside the license-pattern matcher. 
fingerprint_file winnows a file's
+          tokens (k tokens per gram, w grams per window) into content 
fingerprints; a fingerprint segment
+          stores them keyed by a 128-bit content hash rather than filenames 
(so it stays lean and joins to
+          the Cavil database), and scoring ranks candidates by both-direction 
containment with exact
+          matched-line spans for highlighting.
+        - Cavil::Matcher::FpIndex is the pure-Perl lifecycle over a directory 
of fingerprint segments
+          (incremental add_segment, manifest/generation, memory-mapped shared 
reads), reusing the same
+          on-disk discipline as the pattern index: versioned, CRC-checksummed, 
structure-validated on
+          every open, with corrupt or foreign files rejected rather than 
mis-read.
+        - Byte-identical files are de-duplicated by content hash, so the same 
source shipped across many
+          package versions is stored once, with no package-name parsing.
+        - Additive only: the license-pattern format is unchanged and no 
migration is needed.
+
 1.01   2026-08-04
         - Big indexing speedup: the scan path (load/attach) no longer 
re-checksums a segment's whole
           payload on every open. A compiled segment is an immutable, 
atomically-published cache that is
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/MANIFEST 
new/Cavil-Matcher-1.05/MANIFEST
--- old/Cavil-Matcher-1.01/MANIFEST     2026-08-04 20:33:35.156164233 +0200
+++ new/Cavil-Matcher-1.05/MANIFEST     2026-09-03 12:57:59.858246039 +0200
@@ -13,6 +13,8 @@
 src/bag.h
 src/bindings.cc
 src/bindings.h
+src/fingerprint.cc
+src/fingerprint.h
 src/matcher.cc
 src/matcher.h
 src/segment.cc
@@ -36,6 +38,7 @@
 t/14coverage.t
 t/15segment_header.t
 t/16resolve.t
+t/fingerprint.t
 t/fixtures/licenses/04license.1.pattern
 t/fixtures/licenses/04license.1.txt
 t/fixtures/licenses/04license.10.pattern
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/MANIFEST.SKIP 
new/Cavil-Matcher-1.05/MANIFEST.SKIP
--- old/Cavil-Matcher-1.01/MANIFEST.SKIP        2026-07-22 14:59:22.000000000 
+0200
+++ new/Cavil-Matcher-1.05/MANIFEST.SKIP        2026-08-27 13:20:49.000000000 
+0200
@@ -1,5 +1,7 @@
 ^blib/
 ^Matcher\.c$
+^tools/
+^MANIFEST\.bak$
 ^Makefile$
 ^Makefile\.old$
 ^pm_to_blib
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/META.json 
new/Cavil-Matcher-1.05/META.json
--- old/Cavil-Matcher-1.01/META.json    2026-08-04 20:33:35.142118638 +0200
+++ new/Cavil-Matcher-1.05/META.json    2026-09-03 12:57:59.848143644 +0200
@@ -60,6 +60,6 @@
          "web" : "https://github.com/openSUSE/cavil-matcher";
       }
    },
-   "version" : "1.01",
+   "version" : "1.05",
    "x_serialization_backend" : "JSON::PP version 4.16"
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/META.yml 
new/Cavil-Matcher-1.05/META.yml
--- old/Cavil-Matcher-1.01/META.yml     2026-08-04 20:33:35.110368413 +0200
+++ new/Cavil-Matcher-1.05/META.yml     2026-09-03 12:57:59.814717384 +0200
@@ -28,5 +28,5 @@
   bugtracker: https://github.com/openSUSE/cavil-matcher/issues
   license: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
   repository: https://github.com/openSUSE/cavil-matcher.git
-version: '1.01'
+version: '1.05'
 x_serialization_backend: 'CPAN::Meta::YAML version 0.020'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/Makefile.PL 
new/Cavil-Matcher-1.05/Makefile.PL
--- old/Cavil-Matcher-1.01/Makefile.PL  2026-07-21 16:19:43.063785774 +0200
+++ new/Cavil-Matcher-1.05/Makefile.PL  2026-09-03 12:43:00.243958370 +0200
@@ -14,7 +14,7 @@
   if $Config{uvsize} < 8;
 
 # C++ core lives under src/; the XS shim (Matcher.xs) at the top marshals 
between Perl and it.
-my @cpp = qw(SpookyV2.cpp tokenizer.cc segment.cc matcher.cc bag.cc 
bindings.cc);
+my @cpp = qw(SpookyV2.cpp tokenizer.cc segment.cc matcher.cc bag.cc 
fingerprint.cc bindings.cc);
 my @obj = ('$(BASEEXT)$(OBJ_EXT)');    # the compiled XS shim 
(Matcher$(OBJ_EXT))
 for my $src (@cpp) {
   (my $o = $src) =~ s/\.(?:cc|cpp)$/\$(OBJ_EXT)/;
@@ -57,8 +57,9 @@
 
   clean  => {FILES => 'cover_db'},
   depend => {
-    'src/bindings$(OBJ_EXT)'  => 'src/bindings.h src/tokenizer.h src/segment.h 
src/matcher.h src/bag.h src/SpookyV2.h',
+    'src/bindings$(OBJ_EXT)'  => 'src/bindings.h src/tokenizer.h src/segment.h 
src/matcher.h src/bag.h src/fingerprint.h src/SpookyV2.h',
     'src/matcher$(OBJ_EXT)'   => 'src/matcher.h src/segment.h src/tokenizer.h',
+    'src/fingerprint$(OBJ_EXT)' => 'src/fingerprint.h src/tokenizer.h 
src/SpookyV2.h',
     'src/segment$(OBJ_EXT)'   => 'src/segment.h src/tokenizer.h',
     'src/tokenizer$(OBJ_EXT)' => 'src/tokenizer.h src/SpookyV2.h',
     'src/bag$(OBJ_EXT)'       => 'src/bag.h src/tokenizer.h',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/Matcher.xs 
new/Cavil-Matcher-1.05/Matcher.xs
--- old/Cavil-Matcher-1.01/Matcher.xs   2026-08-04 20:27:15.064991701 +0200
+++ new/Cavil-Matcher-1.05/Matcher.xs   2026-09-03 12:34:18.652668444 +0200
@@ -56,6 +56,18 @@
   OUTPUT:
     RETVAL
 
+AV *fingerprint_file(const char *path, int k, int w)
+  CODE:
+    RETVAL = fp_fingerprint_file(path, k, w);
+  OUTPUT:
+    RETVAL
+
+SV *content_hash(const char *path)
+  CODE:
+    RETVAL = fp_content_hash(path);
+  OUTPUT:
+    RETVAL
+
 MODULE = Cavil::Matcher  PACKAGE = Cavil::Matcher::Engine
 
 void add_pattern(Cavil::Matcher::Engine self, UV id, AV *tokens)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/docs/Architecture.md 
new/Cavil-Matcher-1.05/docs/Architecture.md
--- old/Cavil-Matcher-1.01/docs/Architecture.md 2026-08-04 20:20:04.757907627 
+0200
+++ new/Cavil-Matcher-1.05/docs/Architecture.md 2026-09-03 12:45:21.340350699 
+0200
@@ -7,7 +7,7 @@
 ## Why this exists
 
 Cavil reviews the licensing of software by scanning source code for the text 
of known licenses. The scanning
-is done by matching every file against a large, ever-growing collection of 
*patterns* — normalized fragments
+is done by matching every file against a large, ever-growing collection of 
*patterns* - normalized fragments
 of license text. The previous engine did this well and fast, and this one 
keeps its core idea unchanged. What
 it changes is everything around that core, because the old design had three 
operational problems that grow
 worse over time:
@@ -20,8 +20,8 @@
 - **The on-disk form was fragile.** The compiled file had no header, version, 
or checksum; it was trusted
   blindly. A format change or a truncated file could be silently misread.
 
-The goal of this engine is to fix those three things — cheap incremental 
updates, one shared copy per machine,
-and a safe, versioned on-disk format — while keeping matching itself 
bit-for-bit identical, so switching to it
+The goal of this engine is to fix those three things - cheap incremental 
updates, one shared copy per machine,
+and a safe, versioned on-disk format - while keeping matching itself 
bit-for-bit identical, so switching to it
 requires no re-processing of existing data.
 
 ## The Perl/native split, and why
@@ -30,8 +30,8 @@
 the pattern collection. That inner loop, and the hashing that feeds it, is 
where nearly all the time goes, and
 it is written in a small, carefully-frozen C++ core.
 
-Everything else — deciding which patterns are active, recording that a pattern 
was removed, choosing when to
-compact, reading and writing the little file that describes the collection — 
happens rarely and on small data.
+Everything else - deciding which patterns are active, recording that a pattern 
was removed, choosing when to
+compact, reading and writing the little file that describes the collection - 
happens rarely and on small data.
 All of that is plain Perl, because that is where the team is strongest, 
because it is where a newcomer can
 follow the logic without a debugger, and because none of it is on the hot 
path, so nothing is lost by keeping
 it in Perl. The native side is deliberately dumb: it is handed a list of 
things to search and simply searches
@@ -40,7 +40,7 @@
 The native core is C++ rather than a rewrite in another language for a simple 
reason: the matching algorithm
 and its hashing are already proven on a legal tool, where a subtle behavioural 
change is the worst kind of
 bug. Reusing that code unchanged is the safest possible choice, so the core 
stays in the language it is
-already written in. The genuinely new native code — the reader for the on-disk 
format — is small and only ever
+already written in. The genuinely new native code - the reader for the on-disk 
format - is small and only ever
 reads files this same software wrote, and it validates everything it reads, 
which removes the one real
 weakness the old format had.
 
@@ -50,7 +50,7 @@
 and the files being scanned are put through the same normalization: text is 
lower-cased and split into words,
 punctuation and common comment or markup noise is discarded, and each 
surviving word is reduced to a number.
 Only those numbers are ever compared. This is what lets a match survive 
reformatting, rewrapping, and
-different comment styles — the layout simply disappears during normalization.
+different comment styles - the layout simply disappears during normalization.
 
 One wildcard exists. A pattern may say "skip one to N words here" (at least 
one word, at most N - it does not
 match a zero-word gap), which lets a single pattern absorb the parts of a 
license that legitimately vary, such
@@ -85,7 +85,7 @@
   not touched. Absorbing a new pattern is therefore cheap and local, no matter 
how large the collection has
   grown.
 - **Removing a pattern writes a tombstone** in the manifest and nothing else. 
At scan time, matches belonging
-  to a tombstoned pattern are dropped before overlap resolution — so removing 
a pattern correctly reveals any
+  to a tombstoned pattern are dropped before overlap resolution - so removing 
a pattern correctly reveals any
   smaller matches it had been hiding, exactly as if it had never existed. No 
segment is recompiled.
 - **A query searches all active segments at once**, gathers their matches, 
discards the tombstoned ones, and
   then applies the ordinary overlap resolution to the combined set. The result 
is identical to what a single
@@ -93,7 +93,7 @@
 
 Because deltas and tombstones accumulate, an occasional **compaction** folds 
the current pattern set back into
 a single fresh base segment and clears the tombstones. This is the one 
operation that reads the full pattern
-set from the database — Cavil's source of truth — and it is rare and runs in 
the background, off the scanning
+set from the database - Cavil's source of truth - and it is rare and runs in 
the background, off the scanning
 path. It exists only to keep the number of segments and the length of the 
tombstone list bounded over time.
 
 ## Shared memory and reproducibility
@@ -106,14 +106,14 @@
 
 The generation number in the manifest gives reproducibility. A scan pins the 
generation it ran against, and a
 report can record it, so re-running an old report can use exactly the same 
patterns it originally saw. Updates
-are published atomically — a new segment and an updated manifest are written 
to the side and swapped into place
-in one step — so a reader never observes a half-written collection.
+are published atomically - a new segment and an updated manifest are written 
to the side and swapped into place
+in one step - so a reader never observes a half-written collection.
 
 ## The on-disk format
 
 Each segment file begins with a header identifying it, stating its format 
version, and carrying a checksum of
-everything that follows. Opening a segment always validates its **structure** 
— the header fields, the size,
-and every internal reference — so a file that is truncated, of the wrong 
version, structurally impossible, or
+everything that follows. Opening a segment always validates its **structure** 
- the header fields, the size,
+and every internal reference - so a file that is truncated, of the wrong 
version, structurally impossible, or
 simply not a segment at all is rejected cleanly; it is never partially trusted 
and never able to send the
 scanner off the end of the data. This structural validation is cheap and 
always on, so memory safety never
 depends on the checksum.
@@ -122,7 +122,7 @@
 recomputed on the hot scan path. A segment is Cavil's own derived cache: it is 
checksummed when compiled,
 published atomically (written to the side and renamed into place), and 
thereafter immutable and regenerable
 from the database. Re-checksumming a multi-hundred-megabyte payload on every 
one of the thousands of indexing
-opens — where the mmap already shares one physical copy — is almost the entire 
cost of a load (measured at
+opens - where the mmap already shares one physical copy - is almost the entire 
cost of a load (measured at
 ~97%), and it guards against a corruption that atomic publishing already 
prevents. So the CRC is verified where
 it is meaningful: once when the segment is written, and on demand through the 
engine's `verify` entry point (an
 fsck for operators). The manifest additionally records a checksum for each 
segment for that on-demand check.
@@ -135,7 +135,36 @@
 single-line files with no structure at all. The matcher treats all of this as 
ordinary input: it reads files
 in bounded chunks, stops cleanly at the end of usable data, and bounds the 
amount of a file it holds in memory
 at once. Unreadable paths and missing files produce empty results rather than 
errors. The guiding rule is
-simple and absolute — no input, however hostile or malformed, may crash the 
scan.
+simple and absolute - no input, however hostile or malformed, may crash the 
scan.
+
+## Fingerprinting for snippet provenance
+
+The pattern engine answers "which known licenses does this file contain". A 
closely related question is
+"which known open source code does this snippet resemble", used by a service 
where someone submits a
+fragment (for example AI-generated code) and asks how much of it already 
exists in the open source Cavil
+has seen. This distribution provides the *primitives* for that question. The 
searchable index that turns
+fingerprints back into packages and paths lives in the consuming application - 
Cavil keeps it in Postgres -
+not here.
+
+Two calls make up the surface. `content_hash` returns the 128-bit hash of a 
file's raw bytes as 32 hex
+characters, produced by the same frozen hasher the pattern side uses, so 
identical content always yields
+the same key and the database can join on it. `fingerprint_file` winnows a 
file into a set of fingerprints,
+each carrying the exact line range it covers so a match can be highlighted.
+
+Winnowing is the idea plagiarism detectors have used for decades. A file's 
tokens are grouped into
+overlapping runs (k tokens each), every run is hashed, and a stable subset of 
those hashes is selected by
+taking the smallest hash in each sliding window of the runs. The selection is 
deterministic and
+shift-stable, so the same code always yields the same fingerprints no matter 
where it sits in a file or how
+it was chopped up while reading, and it survives reformatting and renaming 
because the layout has already
+disappeared during tokenization. The window width is the one knob: a wider 
window keeps fewer fingerprints
+(cheaper) and a narrower window keeps more (more robust), with the count 
landing near two divided by the
+window-plus-one, times the number of runs.
+
+Everything downstream is the consumer's: storing the fingerprints, looking a 
query's fingerprints up,
+ranking candidates by containment, pruning boilerplate, and resolving a 
content hash back to packages and
+paths. Cavil does all of that in the database. Keeping only the primitives 
here means the expensive,
+corpus-sized index is owned where it is queried, and this distribution stays a 
deterministic fingerprint
+source with nothing on disk of its own to maintain.
 
 ## What deliberately stays the same
 
@@ -175,5 +204,5 @@
 can simply be deleted, and the self-contained suite stands on its own.
 
 A change to the pattern set flows through the system in the obvious way: 
adding patterns writes a new segment,
-removing one writes a tombstone, and compaction periodically rewrites a single 
clean base — none of which
+removing one writes a tombstone, and compaction periodically rewrites a single 
clean base - none of which
 disturbs the data an in-flight scan is already using.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/lib/Cavil/Matcher.pm 
new/Cavil-Matcher-1.05/lib/Cavil/Matcher.pm
--- old/Cavil-Matcher-1.01/lib/Cavil/Matcher.pm 2026-08-04 20:28:57.000000000 
+0200
+++ new/Cavil-Matcher-1.05/lib/Cavil/Matcher.pm 2026-09-03 12:45:54.000000000 
+0200
@@ -6,7 +6,7 @@
 use strict;
 use warnings;
 
-our $VERSION = '1.01';
+our $VERSION = '1.05';
 
 require XSLoader;
 XSLoader::load('Cavil::Matcher', $VERSION);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/src/bindings.cc 
new/Cavil-Matcher-1.05/src/bindings.cc
--- old/Cavil-Matcher-1.01/src/bindings.cc      2026-08-04 20:27:02.193018589 
+0200
+++ new/Cavil-Matcher-1.05/src/bindings.cc      2026-09-03 12:33:47.613296616 
+0200
@@ -4,6 +4,7 @@
 // Pure-C++ core first, so its <vector>/<map>/<string> are seen before Perl's 
macro soup.
 #include "SpookyV2.h"
 #include "bag.h"
+#include "fingerprint.h"
 #include "matcher.h"
 #include "segment.h"
 #include "tokenizer.h"
@@ -352,3 +353,28 @@
 int  bag_dump(Bag* b, const char* filename) { return b->dump(filename) ? 1 : 
0; }
 int  bag_load(Bag* b, const char* filename) { return b->load(filename, 
/*verify_crc=*/false) ? 1 : 0; }
 int  bag_verify(Bag* b, const char* filename) { return b->verify(filename) ? 1 
: 0; }
+
+// ---------------------------------------------------------------------------
+// Fingerprints (snippet provenance)
+// ---------------------------------------------------------------------------
+AV* fp_fingerprint_file(const char* path, int k, int w) {
+  dTHX;
+  AV* ret = newAV();
+  for (const Fingerprint& f : fingerprint_file(path, k, w)) {
+    AV* row = newAV();
+    av_push(row, newSVuv(f.fp));
+    av_push(row, newSVuv(f.sline));
+    av_push(row, newSVuv(f.eline));
+    av_push(ret, newRV_noinc((SV*)row));
+  }
+  return ret;
+}
+
+SV* fp_content_hash(const char* path) {
+  dTHX;
+  ContentHash h;
+  fingerprint_file(path, 1, 1, nullptr, &h);    // k/w irrelevant; we only 
want the content hash
+  char buf[33];
+  snprintf(buf, sizeof(buf), "%016llx%016llx", (unsigned long long)h.hi, 
(unsigned long long)h.lo);
+  return newSVpv(buf, 32);
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/src/bindings.h 
new/Cavil-Matcher-1.05/src/bindings.h
--- old/Cavil-Matcher-1.01/src/bindings.h       2026-08-04 20:27:06.717009139 
+0200
+++ new/Cavil-Matcher-1.05/src/bindings.h       2026-09-03 12:28:57.555999859 
+0200
@@ -40,6 +40,10 @@
 void     matcher_set_generation(Matcher* m, UV generation);
 UV       matcher_generation(Matcher* m);
 
+// Fingerprints (snippet provenance): winnow a file, or hash its content.
+AV* fp_fingerprint_file(const char* path, int k, int w);   // [[fp, sline, 
eline], ...]
+SV* fp_content_hash(const char* path);                     // 32-hex content 
hash
+
 // Bag
 Bag* pattern_init_bag();
 void destroy_bag(Bag* b);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/src/fingerprint.cc 
new/Cavil-Matcher-1.05/src/fingerprint.cc
--- old/Cavil-Matcher-1.01/src/fingerprint.cc   1970-01-01 01:00:00.000000000 
+0100
+++ new/Cavil-Matcher-1.05/src/fingerprint.cc   2026-09-03 12:24:38.111343093 
+0200
@@ -0,0 +1,97 @@
+// SPDX-FileCopyrightText: SUSE LLC
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "fingerprint.h"
+
+#include "SpookyV2.h"   // content hashing
+
+#include <cstdio>
+#include <cstring>
+#include <vector>
+
+static const int MAX_LINE_SIZE = 8000;
+
+// A single file that winnows to more than this many tokens is almost 
certainly a minified/generated
+// blob, not human source; cap it so one pathological file cannot balloon 
memory.
+static const size_t MAX_TOKENS_PER_FILE = 4000000;
+
+std::vector<Fingerprint> winnow_tokens(const TokenList& tokens, int k, int w) {
+  std::vector<Fingerprint> out;
+  if (k < 1) k = 1;
+  if (w < 1) w = 1;
+  int n      = (int)tokens.size();
+  int ngrams = n - k + 1;
+  if (ngrams <= 0) return out;
+
+  // Hash each k-gram of token hashes (files never contain $SKIP tokens, so 
every hash is real).
+  std::vector<uint64_t> g((size_t)ngrams);
+  for (int i = 0; i < ngrams; ++i) {
+    uint64_t h = 1469598103934665603ULL;
+    for (int j = 0; j < k; ++j) {
+      h ^= tokens[(size_t)i + j].hash;
+      h *= 1099511628211ULL;
+    }
+    g[(size_t)i] = h;
+  }
+
+  // Classic winnowing (Schleimer/Wilkerson/Aiken): emit the minimum hash of 
every window of w grams,
+  // preferring the rightmost on ties, never re-emitting the same selected 
position. A gram at position p
+  // spans tokens p..p+k-1, so its line range is those tokens' line numbers.
+  auto emit = [&](int pos) {
+    out.push_back({g[(size_t)pos], (uint32_t)tokens[(size_t)pos].linenumber,
+                   (uint32_t)tokens[(size_t)(pos + k - 1)].linenumber});
+  };
+  int min_pos = -1;
+  for (int i = 0; i + w <= ngrams; ++i) {
+    if (min_pos < i) {
+      min_pos = i;
+      for (int j = i + 1; j < i + w; ++j)
+        if (g[(size_t)j] <= g[(size_t)min_pos]) min_pos = j;
+      emit(min_pos);
+    } else if (g[(size_t)(i + w - 1)] <= g[(size_t)min_pos]) {
+      min_pos = i + w - 1;
+      emit(min_pos);
+    }
+  }
+  return out;
+}
+
+// The shared read+tokenize+winnow path, byte-for-byte the same numbering as 
Matcher::find_matches, so a
+// fingerprint's line spans line up with a pattern match's. When `content` is 
non-null the raw bytes are
+// hashed as read, before tokenize lower-cases them, yielding the content hash 
the caller joins on.
+static std::vector<Fingerprint> winnow_stream(FILE* input, int k, int w, 
size_t* out_tokens,
+                                              SpookyHash* content) {
+  TokenList ts;
+  char      line[MAX_LINE_SIZE];
+  int       linenumber = 1;
+  long      pos        = ftell(input);
+  while (fgets(line, sizeof(line) - 1, input)) {
+    long   npos = ftell(input);
+    size_t got  = (pos >= 0 && npos >= pos) ? (size_t)(npos - pos) : 
strlen(line);
+    pos         = npos;
+    if (got >= sizeof(line)) got = strlen(line);
+    if (content) content->Update(line, got);
+    bool line_end = got > 0 && line[got - 1] == '\n';
+    tokenizer().tokenize(ts, line, linenumber);
+    if (line_end) ++linenumber;
+    if (ts.size() > MAX_TOKENS_PER_FILE) break;
+  }
+  if (out_tokens) *out_tokens = ts.size();
+  return winnow_tokens(ts, k, w);
+}
+
+std::vector<Fingerprint> fingerprint_file(const std::string& path, int k, int 
w, size_t* out_tokens,
+                                          ContentHash* out_hash) {
+  if (out_tokens) *out_tokens = 0;
+  if (out_hash) *out_hash = ContentHash{};
+  FILE* input = fopen(path.c_str(), "rb");
+  if (!input) return {};
+
+  SpookyHash content;
+  if (out_hash) content.Init(0, 0);
+  auto fps = winnow_stream(input, k, w, out_tokens, out_hash ? &content : 
nullptr);
+  fclose(input);
+
+  if (out_hash) content.Final(&out_hash->hi, &out_hash->lo);
+  return fps;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/src/fingerprint.h 
new/Cavil-Matcher-1.05/src/fingerprint.h
--- old/Cavil-Matcher-1.01/src/fingerprint.h    1970-01-01 01:00:00.000000000 
+0100
+++ new/Cavil-Matcher-1.05/src/fingerprint.h    2026-09-03 12:28:46.703597230 
+0200
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: SUSE LLC
+// SPDX-License-Identifier: GPL-2.0-or-later
+//
+// Snippet-provenance primitives: winnow a file's tokens into content 
fingerprints, and hash its bytes
+// into a 128-bit content key. These answer "what fingerprints does this 
content have"; the searchable
+// index that turns fingerprints back into packages/paths lives in the 
consuming application (Cavil, in
+// Postgres), not here. Uses the frozen tokenizer/hashing so identical text 
always fingerprints
+// identically. No Perl types here.
+
+#ifndef CAVIL_MATCHER_FINGERPRINT_H_
+#define CAVIL_MATCHER_FINGERPRINT_H_
+
+#include "tokenizer.h"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+// A 128-bit content hash (SpookyHash of the raw file bytes), rendered 
elsewhere as 32 hex chars exactly
+// like Cavil::Matcher::Hash::hex, so the Cavil database can join on it.
+struct ContentHash {
+  uint64_t hi = 0;
+  uint64_t lo = 0;
+  bool operator==(const ContentHash& o) const { return hi == o.hi && lo == 
o.lo; }
+};
+
+// One winnowed fingerprint occurrence: the 64-bit gram hash and the source 
line range (start..end) it
+// covers, so a match can be highlighted exactly.
+struct Fingerprint {
+  uint64_t fp;
+  uint32_t sline;
+  uint32_t eline;
+};
+
+// Winnow a token stream into fingerprints. k = tokens per gram, w = grams per 
window; expected density
+// is ~2/(w+1) of grams. Deterministic (rightmost-min tie-break), so identical 
text always yields
+// identical fingerprints regardless of how the file was chunked while reading.
+std::vector<Fingerprint> winnow_tokens(const TokenList& tokens, int k, int w);
+
+// Fingerprint a whole file: read it (raw bytes, NUL-tolerant, token count 
bounded like the matcher),
+// tokenize, winnow. out_tokens receives the token count; out_hash receives 
the 128-bit content hash the
+// caller joins on (identical across byte-identical files). Missing or 
unreadable files yield an empty
+// result, not an error.
+std::vector<Fingerprint> fingerprint_file(const std::string& path, int k, int 
w, size_t* out_tokens = nullptr,
+                                          ContentHash* out_hash = nullptr);
+
+#endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/t/fingerprint.t 
new/Cavil-Matcher-1.05/t/fingerprint.t
--- old/Cavil-Matcher-1.01/t/fingerprint.t      1970-01-01 01:00:00.000000000 
+0100
+++ new/Cavil-Matcher-1.05/t/fingerprint.t      2026-09-03 12:37:29.000000000 
+0200
@@ -0,0 +1,53 @@
+# SPDX-FileCopyrightText: SUSE LLC
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+use strict;
+use warnings;
+use Test::More;
+use Cavil::Matcher;
+use File::Temp qw(tempdir);
+use File::Spec;
+
+# Small k/w so tiny fixtures still winnow to fingerprints (density ~2/(w+1)).
+my ($K, $W) = (3, 4);
+my $dir = tempdir(CLEANUP => 1);
+
+sub write_file {
+  my ($name, $text) = @_;
+  my $path = File::Spec->catfile($dir, $name);
+  open my $fh, '>', $path or die "write $path: $!";
+  print $fh $text;
+  close $fh;
+  return $path;
+}
+
+my $block_a = join ' ', map {"alpha$_ beta$_ gamma$_ delta$_"} 1 .. 20;
+my $block_b = join ' ', map {"omega$_ sigma$_ kappa$_ lambda$_"} 1 .. 20;
+
+my $file_a = write_file('a.txt', $block_a);
+my $file_b = write_file('b.txt', $block_b);
+
+my ($hash_a, $hash_b) = map { Cavil::Matcher::content_hash($_) } $file_a, 
$file_b;
+
+subtest 'content_hash is a stable 32-hex digest of the bytes' => sub {
+  like $hash_a, qr/^[0-9a-f]{32}$/, '32 hex chars';
+  is $hash_a,   Cavil::Matcher::content_hash($file_a), 'same file, same hash';
+  isnt $hash_a, $hash_b,                               'different content, 
different hash';
+};
+
+subtest 'fingerprint_file is deterministic with exact line spans' => sub {
+  my $fp1 = Cavil::Matcher::fingerprint_file($file_a, $K, $W);
+  my $fp2 = Cavil::Matcher::fingerprint_file($file_a, $K, $W);
+  ok @$fp1 > 0, 'produced fingerprints';
+  is_deeply $fp2, $fp1, 'identical input yields identical fingerprints';
+  is scalar(grep { @$_ == 3 && $_->[2] >= $_->[1] } @$fp1), scalar(@$fp1), 
'each row is [fp, sline, eline]';
+};
+
+subtest 'fingerprint_file on missing/empty input never dies' => sub {
+  is_deeply Cavil::Matcher::fingerprint_file(File::Spec->catfile($dir, 
'nope.txt'), $K, $W), [],
+    'missing file => empty';
+  is_deeply Cavil::Matcher::fingerprint_file(write_file('empty.txt', ''),      
 $K, $W), [], 'empty file => empty';
+  is_deeply Cavil::Matcher::fingerprint_file(write_file('tiny.txt', 'one 
two'), $K, $W), [], 'sub-k-gram file => empty';
+};
+
+done_testing;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.01/typemap 
new/Cavil-Matcher-1.05/typemap
--- old/Cavil-Matcher-1.01/typemap      2026-07-21 00:27:23.810104489 +0200
+++ new/Cavil-Matcher-1.05/typemap      2026-08-27 13:20:49.110595166 +0200
@@ -1,4 +1,6 @@
-Cavil::Matcher::Engine T_PTROBJ
-Cavil::Matcher::Hash   T_PTROBJ
-Cavil::Matcher::Bag    T_PTROBJ
-AV*                    T_AVREF_REFCOUNT_FIXED
+Cavil::Matcher::Engine       T_PTROBJ
+Cavil::Matcher::Hash         T_PTROBJ
+Cavil::Matcher::Bag          T_PTROBJ
+Cavil::Matcher::Fingerprints T_PTROBJ
+AV*                          T_AVREF_REFCOUNT_FIXED
+HV*                          T_HVREF_REFCOUNT_FIXED

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.X8xdmx/_old  2026-09-08 17:01:55.682526462 +0200
+++ /var/tmp/diff_new_pack.X8xdmx/_new  2026-09-08 17:01:55.689526754 +0200
@@ -1,6 +1,6 @@
-mtime: 1785916160
-commit: a3fe64e68ac939b73fcd0b0e07207129b78d41c9f326f32c4073c47d6e5167fc
+mtime: 1788514856
+commit: a1e7d2f055cf1f959a04e70b4cf478cbb42d3cd7d741fb5f737cb1c408ed7911
 url: https://src.opensuse.org/perl/perl-Cavil-Matcher
-revision: a3fe64e68ac939b73fcd0b0e07207129b78d41c9f326f32c4073c47d6e5167fc
+revision: a1e7d2f055cf1f959a04e70b4cf478cbb42d3cd7d741fb5f737cb1c408ed7911
 projectscmsync: https://src.opensuse.org/perl/_ObsPrj
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-09-04 11:40:56.000000000 +0200
@@ -0,0 +1,5 @@
+*.obscpio
+*.osc
+_build.*
+_service:*
+.pbuild

Reply via email to