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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions research/reconstruction/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Boundary-aware, constraint-first reconstruction: experimental v1

**This is a working research prototype, not a verified LLM-watermark remover.**
It changes ordinary visible wording. It does not extend the Unicode carrier stack,
change the production CLI, ship a detector product, or claim a novel watermark attack.

## What actually runs

The end-to-end frontend supports a deliberately narrow English reporting grammar:

```text
Orion Labs reported a revenue of 120 USD in 2025.
Vega Systems did not report a cost of 42 EUR in 2024.
```

Five metric families are supported: revenue/cost, distance, duration, and mass,
with explicit compatible units. Amounts are nonnegative decimal literals. Entities
follow a bounded ASCII proper-name grammar; years are 1900-2199. Positive and
negative reporting statements are accepted in four active/passive/time-fronted
forms. Unsupported prose, quotations, code, hidden characters, and languages are
not silently rewritten. The text API returns the original with `unsupported`;
the CLI returns exit 3. This is not an arbitrary-prose semantic parser.

Each accepted sentence becomes an ordered frame:
`(entity, metric, exact amount literal, unit, year, negated)`.
All alternatives must reparse to the same frame. The output is checked again
against the ordered frames and numeric literals. This is stronger than matching
an unordered bag of numbers, but it proves only the restricted grammar contract,
not the truth of assertions or semantic equivalence for unrestricted language.

The generic low-level planner accepts finite candidate lattices. It is **not**
a semantic verifier; do not feed unchecked LLM paraphrases into it and treat its
score as a quality guarantee. The included frontend supplies grammar-checked
candidates. Connecting a general-purpose generator/semantic verifier remains work
not implemented here.

## Algorithm, not another hidden-character layer

For each supported sentence, construct a finite candidate set. Search their
combinations jointly under a fixed normalized-word edit budget. The state carries
last `k-1` tokens for every measurement view, accumulated retained-window counts,
and edit cost. Extending a state scores both interior and cross-sentence windows.

The objective is lexicographic:

1. Minimize the largest source-normalized retained-window count across views.
2. Minimize the sum of those fractions.
3. Minimize edit cost, then choose a deterministic candidate-index path.

The denominator is fixed from the **source**, so adding filler cannot dilute the
score. A retained output window is counted when its tuple occurs anywhere in the
source, including at a different location. Repetition is counted with output
multiplicity; this is not an implementation of SynthID's repetition masking.

Pareto pruning is allowed only for equal tail states: a state can dominate another
only when its cost and every accumulated count are no larger. Future increments
are then identical for equal suffix choices. Exactness is conditional on the given
finite candidate lattice, supported tokenizers, objective, and successful bounded
completion. It is not an optimum over all possible rewrites.

A full-output recomputation must agree with the dynamic program. Unsupported
non-compositional tokenizers, including an arbitrary BPE callback, are rejected
rather than falsely declared exact. State/evaluation limits return the unchanged
source and `search_limit`; no truncated search is labeled optimal.

Current views are NFKC/casefold word 3-grams, word 5-grams, and word-plus-punctuation
5-grams. These are proxies, **not GPT-2/BPE observations or watermark scores**.
There are no watermark keys, detector queries, model/API calls, or new runtime
dependencies in candidate generation and selection.

## Run from a repository checkout

```sh
python -m research.reconstruction --text "Orion Labs reported a revenue of 120 USD in 2025. Vega Systems did not report a cost of 42 EUR in 2024." --json
python -m research.reconstruction --file input.txt --budget 32 --json
python -m pytest -q tests/test_reconstruction.py
python -m research.reconstruction.benchmark --output geometry-128.json
python -m research.reconstruction.legacy_control --output legacy-control.json
```

The research directory intentionally is not included in the current `fuckmark*`
wheel package selection. Existing `fuckmark` commands and evidence are unchanged.
Exit 0 means a supported reconstruction finished, not that a watermark was removed.
Exit 2 is invalid input/configuration; exit 3 is unsupported input or a search limit.
JSON receipts always include `watermark_removal_verified: false`.

## Measured results, and their exact scope

Local environment: CPython 3.13.5, Linux. New tests: **102 passed**. Existing CLI,
CLI I/O, and protected-machine-text tests: **70 passed**. Combined: **172 passed**.
This is targeted local validation, not a complete checkout CI result.

The protocol was written before the first development benchmark. The 128 fixtures
are newly synthesized controlled-grammar statements, **not watermarked LLM text**.
No previous frozen confirmation corpus was used. The greedy comparator uses the
same alternatives and budget, recomputes whole-output geometry, and changes one
candidate position at a time. It is not the repository's historical scheduler.

| Measurement | Result |
| --- | --- |
| Supported documents reconstructed | 128 / 128 |
| Ordered grammar-frame checks | 128 / 128 |
| Joint search strictly better than greedy on worst-view retention | 51 / 128 |
| Tied on worst-view retention | 77 / 128 |
| Mean worst-view retention, joint search | 0.5987639731 |
| Mean worst-view retention, greedy | 0.6205182055 |
| Exhaustive-search agreement on benchmark fixtures | 24 / 24 |
| Additional randomized exhaustive-search test cases | 48 / 48 |
| Generated frame/form roundtrips tested | 2,048 |
| Real watermark-detector evaluation | **Not run** |

Canonical benchmark row digest:
`36d173f4e1521f74a963754c917724eaf4e5ba37b62201feb662a5510f495736`.
The benchmark test pins it; elapsed time is excluded. `results.json` contains the
summary. Re-running the command produces all source/output texts and receipts.

In a separate control against baseline commit
`09c8cbf184e37e2e934e133b74a5db9af67b4f97`, exact deletion of known FuckMark carriers
restored all 128 legacy outputs. All 128 new outputs survived NFKC plus that same
carrier deletion unchanged. This demonstrates the new representation does not
rely on those carriers. It does **not** prove resistance to arbitrary normalization,
semantic rewrites, a tokenizer, or an actual watermark detector.

## Limits and the next scientific gate

The local runtime lacked pretrained model/tokenizer artifacts, and outbound DNS
failed. No real SynthID, KGW, proprietary detector, cross-tokenizer, or real-LLM
removal claim is made. Proxy optimization can fail to reduce a real detector score;
protected facts and short texts can retain strong signals. Meaning-preserving
reconstruction may also fail against semantic watermark designs.

Before product promotion: integrate an independently checked general-text
candidate provider; use genuine tokenizer handling with boundary verification;
freeze fresh model/watermark/threshold configurations; retain unsupported cases
and failures in the denominator; and measure fidelity and detection separately.
Neither the production CLI nor frozen scientific records should change merely
because this mechanism benchmark passes.

Prior work already studies paraphrase and character-level watermark attacks:
- https://arxiv.org/abs/2508.20228
- https://arxiv.org/abs/2509.09112

This prototype's measured contribution is a small, reproducible constrained
reconstruction implementation with exact joint selection on its finite lattice.
A broader novelty or watermark-efficacy claim remains unestablished.
3 changes: 3 additions & 0 deletions research/reconstruction/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .engine import Frame, Reconstruction, parse_frame, reconstruct, reconstruct_frames

__all__ = ['Frame', 'Reconstruction', 'parse_frame', 'reconstruct', 'reconstruct_frames']
47 changes: 47 additions & 0 deletions research/reconstruction/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from .engine import reconstruct


def main(argv=None):
parser = argparse.ArgumentParser(description='Experimental fact-preserving reconstruction, not verified watermark removal')
parser.add_argument('--text')
parser.add_argument('--file', type=Path)
parser.add_argument('--budget', type=int, default=32)
parser.add_argument('--json', action='store_true')
parser.add_argument('--max-states', type=int, default=20000)
args = parser.parse_args(argv)
if args.text is not None and args.file is not None:
parser.error('choose --text or --file')
try:
if args.text is not None:
source = args.text
elif args.file is not None:
with args.file.open(encoding='utf-8', newline='') as stream:
source = stream.read(100001)
else:
buffer = getattr(sys.stdin, 'buffer', None)
source = buffer.read(400001).decode('utf-8') if buffer is not None else sys.stdin.read(100001)
result = reconstruct(source, budget=args.budget, max_states=args.max_states)
if args.json:
print(json.dumps({'text': result.text, 'receipt': result.receipt()}, ensure_ascii=True, indent=2))
else:
buffer = getattr(sys.stdout, 'buffer', None)
if buffer is None:
sys.stdout.write(result.text)
else:
buffer.write(result.text.encode('utf-8'))
buffer.flush()
sys.stderr.write(json.dumps(result.receipt(), ensure_ascii=True) + '\n')
return 0 if result.status in {'changed', 'unchanged'} else 3
except (OSError, UnicodeError, ValueError, TypeError) as error:
print(f'reconstruction: {error}', file=sys.stderr)
return 2


if __name__ == '__main__':
raise SystemExit(main())
158 changes: 158 additions & 0 deletions research/reconstruction/benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
from __future__ import annotations

from dataclasses import asdict
from itertools import product
from pathlib import Path
from fractions import Fraction
from statistics import mean
import argparse
import hashlib
import json
import random
import time

from .engine import Frame, parse_frame, render, reconstruct_frames, edit_distance
from .planner import Alternative, DEFAULT_VIEWS, reference, residual_counts, objective, words

SEED = 27112401
BENCHMARK_ID = 'reconstruction-geometry-sanity-v1'


def _metric(text, refs, sizes, cost, path):
return objective(residual_counts(text, refs, DEFAULT_VIEWS), sizes, cost, tuple(path))


def _greedy(source, lattice, budget):
refs, sizes = reference(source, DEFAULT_VIEWS)
chosen = tuple(next(i for i, a in enumerate(row) if a.cost == 0) for row in lattice)
current = _metric(source, refs, sizes, 0, chosen)
evaluations = 1
while True:
best = None
for position, row in enumerate(lattice):
for choice, alternative in enumerate(row):
path = list(chosen)
path[position] = choice
cost = sum(lattice[i][j].cost for i, j in enumerate(path))
if cost > budget:
continue
text = ' '.join(lattice[i][j].text for i, j in enumerate(path))
score = _metric(text, refs, sizes, cost, path)
evaluations += 1
if score < current and (best is None or score < best[0]):
best = (score, tuple(path), text)
if best is None:
output = ' '.join(lattice[i][j].text for i, j in enumerate(chosen))
return current, output, evaluations
current, chosen, output = best


def _exhaustive(source, lattice, budget):
refs, sizes = reference(source, DEFAULT_VIEWS)
best = None
for path in product(*(range(len(row)) for row in lattice)):
cost = sum(lattice[i][j].cost for i, j in enumerate(path))
if cost > budget:
continue
text = ' '.join(lattice[i][j].text for i, j in enumerate(path))
score = _metric(text, refs, sizes, cost, path)
if best is None or score < best:
best = score
return best


def fixtures(count):
rng = random.Random(SEED)
combinations = [('revenue','USD'),('cost','EUR'),('distance','km'),('duration','hours'),('mass','kg')]
for index in range(count):
sentences = []
for _ in range(rng.randint(2, 5)):
metric, unit = rng.choice(combinations)
frame = Frame(rng.choice(['Orion Labs','Vega Systems','Nova7','Atlas Group','Helix Research']),
metric, f'{rng.randint(1,999)}.{rng.randint(0,99):02d}', unit,
str(rng.randint(2000,2025)), rng.random() < .25)
sentences.append(render(frame)[rng.randrange(4)])
yield f'grammar-{index:04d}', tuple(sentences), rng.choice([6,12,18,24,30])


def run(count=128, exhaustive_count=24):
if count < 1 or count > 4096 or exhaustive_count < 0 or exhaustive_count > count:
raise ValueError('invalid benchmark bounds')
start = time.perf_counter()
rows = []
checked = 0
for index, (identity, segments, budget) in enumerate(fixtures(count)):
source = ' '.join(segments)
frames = tuple(parse_frame(s) for s in segments)
lattice = tuple(tuple(Alternative(t,edit_distance(words(s),words(t)),str(i)) for i,t in enumerate(render(f))) for s,f in zip(segments,frames,strict=True))
result = reconstruct_frames(segments, budget=budget)
if result.plan is None:
raise AssertionError(f'bounded benchmark hit a search limit: {identity}')
p = result.plan
greedy, greedy_text, evaluations = _greedy(source, lattice, budget)
if p.objective > greedy:
raise AssertionError('exact search is worse than its own greedy baseline')
brute_ok = None
if index < exhaustive_count:
brute_ok = p.objective == _exhaustive(source,lattice,budget)
if not brute_ok:
raise AssertionError('optimality check failed')
checked += 1
row = {
'id':identity,'source':source,'output':result.text,'budget':budget,
'status':result.status,'source_frames':[asdict(f) for f in frames],
'source_windows':list(p.source_counts),'output_shared_windows':list(p.shared_counts),
'worst_source_retention':str(p.objective[0]),
'greedy_worst_source_retention':str(greedy[0]),
'joint_cost':p.edit_cost,'greedy_cost':greedy[2],
'greedy_output':greedy_text,'states_evaluated':p.states_evaluated,
'states_pruned':p.states_pruned,'greedy_evaluations':evaluations,
'exhaustive_agreement':brute_ok,'receipt':result.receipt(),
}
rows.append(row)
joint = [float(Fraction(r['worst_source_retention'])) for r in rows]
greedy = [float(Fraction(r['greedy_worst_source_retention'])) for r in rows]
strict = sum(Fraction(r['worst_source_retention']) < Fraction(r['greedy_worst_source_retention']) for r in rows)
view_retention = [mean(r['output_shared_windows'][i]/max(1,r['source_windows'][i]) for r in rows) for i in range(len(DEFAULT_VIEWS))]
numeric_signature = json.dumps(rows, sort_keys=True, separators=(',',':'), ensure_ascii=True).encode()
return {
'benchmark_id':BENCHMARK_ID, 'seed':SEED, 'samples':count,
'corpus_kind':'synthetic controlled-grammar reports, NOT LLM-generated watermarked text',
'measurement_kind':'source-window retention proxy, NOT watermark detection',
'generation_uses_watermark':False,'watermark_detector_run':False,
'prior_frozen_corpora_used':False,'selection_detector_queries':0,
'baseline':'full-text one-coordinate greedy over the identical alternatives and edit budget; not the historical FuckMark scheduler',
'views':[v.name for v in DEFAULT_VIEWS],
'summary':{
'changed':sum(r['status']=='changed' for r in rows),
'unchanged':sum(r['status']=='unchanged' for r in rows),
'ordered_frame_checks_passed':count,
'exact_search_better_worst_retention_than_greedy':strict,
'exact_search_tied_worst_retention_with_greedy':count-strict,
'mean_worst_retention_joint':mean(joint),
'mean_worst_retention_greedy':mean(greedy),
'mean_retention_per_view':view_retention,
'exhaustive_comparisons_passed':checked,
'max_states_evaluated':max(r['states_evaluated'] for r in rows),
'total_states_pruned':sum(r['states_pruned'] for r in rows),
},
'rows_sha256':hashlib.sha256(numeric_signature).hexdigest(),
'elapsed_seconds':time.perf_counter()-start,
'rows':rows,
}


def main():
parser=argparse.ArgumentParser(description='Geometry sanity only; no watermark-removal rate is measured')
parser.add_argument('--output',type=Path,required=True)
parser.add_argument('--samples',type=int,default=128)
parser.add_argument('--exhaustive',type=int,default=24)
args=parser.parse_args()
report=run(args.samples,args.exhaustive)
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(json.dumps(report,ensure_ascii=True,sort_keys=True,indent=2)+'\n',encoding='utf-8')
print(json.dumps({k:v for k,v in report.items() if k!='rows'},sort_keys=True,indent=2))


if __name__=='__main__':
main()
Loading
Loading