Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,29 @@ jobs:
print("interactive e2e ok")
PY

cli-io:
name: CLI I/O / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip

- name: Install
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev]"

- name: Real file stdin and stdout I/O
run: python -m pytest -q tests/test_cli.py tests/test_cli_io_contract.py tests/test_letter_mix_machine_text.py

calibration-readiness:
name: Frozen calibration readiness
runs-on: ubuntu-24.04
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## Unreleased

Audit fixes F01-F07 against `be6ae7645fda8b39d1d308722ac249f519e68de5`. Package version remains 0.4.0. Frozen confirmation files and their SHA-256 sums are unchanged.

- F01: contenteditable rendering sets `textContent`. The 2026-08-26 Chromium contenteditable VERIFIED rows compared blank divs and are not proof of text rendering equivalence. Replacement measurement: `evidence/audit-fixes-2026-08-27/`.
- F02: common relative paths without `./` and Windows paths that use `/` keep exact original bytes.
- F03: Markdown reference labels are protected at uses and definitions. Link checks use reference resolution, not only visible projection.
- F04: file and stdin I/O keep LF, CRLF, CR, mixed endings, and a missing final newline.
- F05: `--text` is literal text. `--file` requires an existing UTF-8 file. Ordinary sentences are not treated as missing files.
- F06: stdin is decoded as strict UTF-8. Invalid bytes exit nonzero with no payload and no clipboard copy.
- F07: letter-mix site selection no longer hashes the whole document per candidate. Compose-time invariant checks remain.

Live mix bytes can differ from frozen `u034f-ufe00-letter-alt-v1` output hashes when a newly protected span is present. Visible projection remains the source. `cycle8-mix-freeze-v1` still pins `letter_mix_source_sha256` to the freeze-time file.

## v0.4.0 — Product CLI

- Transforms ordinary English ASCII text. Visible words stay exactly the same.
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ Paste or type text, then a line that is only `:done`. The result is copied to th

```text
printf 'I do not agree.\n' | fuckmark
fuckmark "I do not agree."
fuckmark notes.txt -o notes.fm.txt
fuckmark --text "I do not agree."
fuckmark --text "I agree. You are right"
fuckmark --file notes.txt -o notes.fm.txt
printf 'I do not agree.\n' | fuckmark --visible
```

Expand All @@ -48,7 +49,7 @@ Pipes and files write the payload to stdout. `fuckmark --help` is enough to star

`VISIBLE(original) == VISIBLE(transformed)`. FuckMark only inserts U+034F and U+FE00 after eligible ASCII letters. It does not contract, paraphrase, homoglyph, or add spaces.

Supported input: tab, newline, carriage return, and ASCII space through tilde. Other Unicode is returned unchanged. Only UTF-8. URLs, code spans, paths, and similar machine text are left intact.
Supported input: tab, newline, carriage return, and ASCII space through tilde. Other Unicode is returned unchanged with exit 0. That status means I/O succeeded, not that hidden characters were inserted. Only UTF-8. URLs, code spans, paths (including `src/main.py` and `C:/Users/...`), Markdown reference labels, and similar machine text are left intact.

## Verified results

Expand All @@ -66,7 +67,15 @@ Google synthid-text 30-key GPT-2 transformed text: **0/192**.

This does not remove every AI watermark. It is not a claim against unknown, proprietary, or future detectors.

Stripping combining marks or default-ignorable characters restores the source. English ASCII only. Cap 192 insertion sites.
Stripping combining marks or default-ignorable characters restores the source. On the frozen Gate v2 confirmation set, mix plus those two stress sanitizers restored detection to 188/192. The stored 0/192 result applies only to the specified detectors, threshold, samples, and required sanitizer paths.

Primary confirmation uses 64 generated tokens per sample. Insertion stops after the first 192 eligible letter sites, so the tail of a long document is unchanged. That coverage limit is not a measured long-document detection result.

English ASCII only. Valid Unicode outside that domain, including accented letters, is returned unchanged with exit 0.

Equal visible projection is not the same as equal behavior in other software. Raw codepoint search, Markdown reference matching, and exact byte paths can still fail if hidden characters remain. Historical Chromium `contenteditable` VERIFIED rows from 2026-08-26 used a blank-div measurement bug and are not proof of rendering equivalence. Safari/WebKit and terminal-pixel results remain UNKNOWN.

Cap 192 insertion sites.

## How it works

Expand Down
37 changes: 30 additions & 7 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ Redirected or piped stdin does not open this UI. It is processed as a stream.

```text
printf 'I do not agree.\n' | fuckmark
fuckmark "I do not agree."
fuckmark --text "I do not agree."
fuckmark --text "I agree. You are right"
fuckmark --text "Version 1.2 works"
fuckmark --text "Use input/output here"
fuckmark --text=-starts-with-hyphen
fuckmark --file notes.txt
fuckmark --file "my notes.txt" -o notes.fm.txt
fuckmark notes.txt
fuckmark notes.txt -o notes.fm.txt
printf 'I do not agree.\n' | fuckmark --copy
Expand All @@ -52,11 +58,28 @@ printf 'I do not agree.\n' | fuckmark --visible

Piped or quoted input writes the hidden payload to stdout. `--visible` writes the original visible text. `--copy` also places whatever was written on the clipboard.

## Input modes

| Form | Behavior |
| --- | --- |
| `--text TEXT` | Always a literal string. Use this for sentences, decimals, slashes, or text that looks like a path. `--text=-foo` is literal text that starts with a hyphen. |
| `--file FILE` | Always a file. Missing files are errors. Directories are errors. |
| positional operand | If that path exists as a file, it is read. If it looks like a missing path (`notes.txt`, `src/main.py`, `~/mail`, or a slash), it is an error. Otherwise it is literal text. |
| `--stdin` or `-` | Read all of standard input as bytes and decode them as strict UTF-8. |
| no operand, terminal | Paste UI. |
| no operand, pipe | Stream mode. |

`--text`, `--file`, and `--stdin` are mutually exclusive. Quotes group arguments for the shell. They do not switch FuckMark into text mode. If `notes.txt` exists and you want the string `notes.txt`, use `--text notes.txt`.

Existing files are read as UTF-8 bytes with no newline conversion. LF, CRLF, CR, mixed endings, and a missing final newline are preserved. Invalid UTF-8 is rejected: nonzero exit, no stdout payload, no clipboard copy, and no traceback.

## Options

| Option | Behavior |
| --- | --- |
| `--version` | Print `FuckMark 0.4.0`. |
| `--text TEXT` | Transform TEXT as a literal string. |
| `--file FILE` | Read UTF-8 from FILE. |
| `--stdin` | Read all of standard input. |
| `-o FILE`, `--output FILE` | Write UTF-8 output to FILE. |
| `--copy` | Also copy the output to the clipboard. The paste UI always copies. |
Expand All @@ -67,22 +90,22 @@ Piped or quoted input writes the hidden payload to stdout. `--visible` writes th

`--non-interactive` is an alias of `--stdin`.

Quoted text that is not an existing file is transformed as a string. Existing files are read as UTF-8. A path-like argument that is missing (for example `notes.txt`) is an error, not a string. Invalid UTF-8 input is rejected.

## Supported input

Tab, newline, carriage return, and ASCII space through tilde. Other Unicode is returned unchanged (exit 0). Only UTF-8 files.
Tab, newline, carriage return, and ASCII space through tilde. Other Unicode is returned unchanged (exit 0). Exit 0 means I/O succeeded. It does not mean that a transformation occurred or that a watermark was removed. Only UTF-8 files.

Machine spans stay intact: fenced/inline code, markdown destinations, URLs, emails, IPs, dates, currency, percents, numbers, POSIX/Windows paths, CLI flags. Quote interiors are eligible. Cap 192 insertion sites.
Machine spans stay intact: fenced/inline code, markdown destinations, markdown reference labels, URLs, emails, IPs, dates, currency, percents, numbers, POSIX/Windows paths (including relative paths such as `src/main.py` and `C:/Users/...`), CLI flags. Quote interiors are eligible. Cap 192 insertion sites. Insertions fill the first 192 eligible letter sites and then stop, so the tail of a long document is unchanged.

If the text is already transformed, or has no eligible letters, the CLI returns it unchanged.

Same visible words are not the same as identical Markdown, path, or search behavior in other programs. Reports must not treat visible-projection equality as Markdown or filesystem equality.

## Exit status

| Status | Meaning |
| ---: | ---: |
| 0 | Output was written or copied. Unsupported input is returned unchanged. |
| 1 | No input, bad file, unsupported encoding, missing `:done`, or output could not be written. |
| 0 | Output was written or copied. Unsupported Unicode is returned unchanged. |
| 1 | No input, bad file, invalid UTF-8, unsupported encoding, missing `:done`, or output could not be written. |
| 2 | Transform succeeded, but clipboard copy failed. Stream modes still write stdout. The paste UI does not print the payload. |
| 130 | Interrupted with Ctrl+C. |

Expand Down
5 changes: 3 additions & 2 deletions docs/product-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ These are not product success:
- hyphenation or apostrophe substitution;
- extra or missing visible spaces;
- newline / layout changes;
- homoglyphs or compatibility characters.
- homoglyphs or compatibility characters;
- the same visible projection implying the same Markdown, path, or search behavior in other software.

## Current product path

`release_transform_registry()` stays empty. The public CLI (`release-cli-v5`) applies frozen `apply_letter_alternating_mix` directly: U+034F on even selected ASCII-letter sites, U+FE00 on odd sites, cap 192, outside hard machine spans.
`release_transform_registry()` stays empty. The public CLI (`release-cli-v5`) applies `apply_letter_alternating_mix` directly: U+034F on even selected ASCII-letter sites, U+FE00 on odd sites, cap 192, outside hard machine spans. Live protection now also covers common relative paths without a leading `./`, Windows paths that use `/`, and Markdown reference labels at both uses and definitions. Those protection changes can change outputs versus the v0.4.0 freeze of `letter_mix.py` (`cycle8-mix-freeze-v1`, `letter_mix_source_sha256` `b1ceec24e584c0e9e7135ef0c89a3bd249b0bda4a45e07aa7190b1b010ba56d4`). Frozen confirmation hashes stay historical. They are not silently rewritten.

Live `product_approved_carriers_v1()` is `{U+034F, U+FE00}`. The v1 contract's empty `product_authorized_carriers_v1` list is a historical snapshot of that file.

Expand Down
46 changes: 46 additions & 0 deletions evidence/audit-fixes-2026-08-27/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Audit fix evidence (2026-08-27)

This directory records measurements and invalidations for the F01-F07 audit fixes. It does not rewrite historical evidence files.

Audited baseline commit: `be6ae7645fda8b39d1d308722ac249f519e68de5` (package 0.4.0).
Fix commit: `1d686b6eb133c29479b2d9bc1b609f929b63e06e`.

## F01 contenteditable rendering

`cycle8-benchmark-render-v1` assigned `element.value` for every surface, then checked `'value' in element`. On a contenteditable div that assignment created a `value` property, so `textContent` never ran. Screenshots compared two blank divs and stored `VERIFIED`.

Those `chromium_contenteditable` VERIFIED rows are not proof of text rendering equivalence:

- `evidence/cycle8-letter-system-benchmark-2026-08-26/local-system.json`
- `evidence/cycle8-letter-system-benchmark-2026-08-26/scorecard.json`
- `evidence/cycle8-mix-margin-2026-08-26/local-system.json`
- `evidence/cycle8-mix-margin-2026-08-26/scorecard.json`

Those files and their SHA-256 sums are unchanged. `render-v2.json` is the replacement measurement (`cycle8-benchmark-render-v2`).

On this host:

- Chromium: Google Chrome 148.0.7778.96
- Font: DejaVu Sans Mono
- contenteditable AAAA vs BBBB: REJECTED
- contenteditable AAAA vs AAAA: VERIFIED
- contenteditable AAAA vs empty: REJECTED
- textarea has the same positive, negative, and nonempty-vs-empty controls
- Safari/WebKit: UNKNOWN
- terminal pixels: UNKNOWN

An unavailable browser returns UNKNOWN, never VERIFIED.

## F02 and F03 protection

Live `hard_machine_intervals` now protects common relative paths without `./`, Windows paths that use `/`, and Markdown reference labels at uses and definitions. That can change mix output bytes versus the frozen `u034f-ufe00-letter-alt-v1` snapshot.

`cycle8-mix-freeze-v1` remains the historical freeze. Its `letter_mix_source_sha256` is pinned to `b1ceec24e584c0e9e7135ef0c89a3bd249b0bda4a45e07aa7190b1b010ba56d4`, the `letter_mix.py` bytes at freeze time. Live `letter_mix.py` may differ. Frozen confirmation output hashes are not regenerated.

## F07 scan refactor

`letter-mix-scan.json` is a same-host before/after-style observation of the post-fix scanner. It is not a cross-machine performance claim. Final compose-time invariant checks remain.

## L01-L04

These stay product limitations, not silent bug closures. See README and `docs/cli.md`.
92 changes: 92 additions & 0 deletions evidence/audit-fixes-2026-08-27/letter-mix-scan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"algorithm_version": "letter-mix-scan-v2-host-observation",
"artifact_hash": "74928e81db58fa5ac1dc007b99f98438315e9647418aa572c3cc7e690aa213ba",
"commit": "1d686b6eb133c29479b2d9bc1b609f929b63e06e",
"note": "Host observations only. Not a cross-machine performance guarantee.",
"timings": [
{
"input_chars": 1040,
"median_seconds": 0.0013507550002032076,
"min_seconds": 0.0013172589999612683,
"name": "prose_1k",
"output_chars": 1232,
"seconds": [
0.0016266380000615754,
0.0013507550002032076,
0.0013172589999612683
]
},
{
"input_chars": 10400,
"median_seconds": 0.011578481000015017,
"min_seconds": 0.011543329000005542,
"name": "prose_10k",
"output_chars": 10592,
"seconds": [
0.011669592000089324,
0.011543329000005542,
0.011578481000015017
]
},
{
"input_chars": 104000,
"median_seconds": 0.1135778840000512,
"min_seconds": 0.11315520800008017,
"name": "prose_100k",
"output_chars": 104192,
"seconds": [
0.11315520800008017,
0.1135778840000512,
0.11412591299995256
]
},
{
"input_chars": 9249,
"median_seconds": 0.0160103750001781,
"min_seconds": 0.012477553000053376,
"name": "url_only_10k",
"output_chars": 9249,
"seconds": [
0.012477553000053376,
0.01665635100016516,
0.0160103750001781
]
},
{
"input_chars": 18499,
"median_seconds": 0.024290382999879512,
"min_seconds": 0.02418305700007295,
"name": "url_only_20k",
"output_chars": 18499,
"seconds": [
0.024858777999952508,
0.02418305700007295,
0.024290382999879512
]
},
{
"input_chars": 2200,
"median_seconds": 0.0031189240000912832,
"min_seconds": 0.003113965000011376,
"name": "no_sites",
"output_chars": 2200,
"seconds": [
0.0032563459999437327,
0.003113965000011376,
0.0031189240000912832
]
},
{
"input_chars": 4820,
"median_seconds": 0.029459684999892488,
"min_seconds": 0.029254702999878646,
"name": "long_protected",
"output_chars": 4820,
"seconds": [
0.029254702999878646,
0.029459684999892488,
0.03241230200001155
]
}
]
}
Loading
Loading