Skip to content

Feat: stage 0 core spec - #63

Merged
rgdevment merged 18 commits into
mainfrom
feat/stage-0-core-spec
Sep 14, 2026
Merged

rgdevment merged 18 commits into
mainfrom
feat/stage-0-core-spec

Conversation

@rgdevment

Copy link
Copy Markdown
Owner

No description provided.

Four modules under forbid(unsafe), no platform dependencies, 19 tests.

search: the fold applied to index and term alike, which is what 2.x gets
wrong in both directions for Straße, encyclopædia and Łódź.

formats: payload, presence or never. Fixes the type that hangs, the ten
megabytes of icon Finder offers for a nine-byte file, PNG over TIFF, the
legacy twins, and password detection by type rather than content.

hash: whole content up to 256 KB, sixteen spread blocks above it. Two
screenshots sharing half an image no longer collide.

paste: the clipboard is written before anything touches focus, unknown
focus still pastes, a successful send is never retried, races get two
retries and a denied send none, and Secure Input warns without aborting.

Also fixes three CI guards that failed on their own first run: the SPDX
identifier in the licence allow-list, wildcard paths for workspace-internal
dependencies, and the synthetic merge commit GitHub checks out on a PR.
The UTI tables had landed inside cp-core, which ties the core to macOS:
Windows speaks CF_UNICODETEXT and CF_HDROP, not public.utf8-plain-text.

cp-core now holds a Catalog of data only, with the rules written once on
top of it. cp-mac supplies the macOS table; cp-win will supply its own
without touching the core. The core's tests use invented type names to
keep that boundary honest, while cp-mac's assert against the real type
lists measured from Safari, Finder and Preview.

Also drops three unsafe blocks the -sys crate did not need: objc2 exposes
the whole pasteboard surface as safe, so macOS needs none so far.
…esting

watch: the sequence-number receipt that invariant 2 asks for. A jump of
three means two copies happened and were lost, and they get counted
rather than ignored. Our own write is discarded by the exact count that
produced it, so a real copy right after it is no longer swallowed.

Five proptest properties cover generated tick sequences, the central one
being conservation: every emitted event matches a step forward or a
session restart, and whatever advanced without being emitted is counted
as missed. The regression seed for the shrunk counterexample is checked
in, so a backwards counter is retried before any generated case.

cargo-mutants went from 11 survivors to none over 59 mutants, and the
survivors taught three things: the backwards-counter branch was dead code
already covered by max(0); the sampling hash cannot see a change that
falls between blocks, which is now a documented test rather than a
surprise; and wasteful now explicitly beats wanted when a type is in both
lists. Threshold coherence moved to compile-time assertions.

CI gains a mutants job so the suite cannot quietly stop proving anything.
The item model now distinguishes five states of a format instead of
present or absent: inline, blob, too big with its size recorded, announced
but never asked for, and asked for but empty. The last one is not
theoretical: linkpresentation.metadata and fndf both do exactly that.

capture applies the catalogue against the live pasteboard. Concealed
content is decided before a single byte is read, legacy twins collapse so
the item is not stored twice, and the costlier image representation is
recorded without ever being requested, so the source application does not
have to materialise it either.

System invariants cannot live in cargo test: NSPasteboard requires the
main thread and the test runner does not guarantee it. They move to an
executable harness, cargo run -p cp-mac --example probe, which is wired
into CI and currently passes all three checks against the real pasteboard.
…policy

The FTS index is fed from search_text, which stores the same fold applied
to the query. Both sides match because it is the same function, so the four
cases 2.x gets wrong now resolve: strasse finds Straße, encyclopaedia finds
encyclopædia, lodz finds Łódź. And the direction nobody tests, typing the
special character to find it, works too.

An item keeps a row per offered format, including the ones carrying no
bytes. Broken items are marked rather than deleted, survive until their
deadline, do not restart their clock when marked again, and are never
purged while pinned.

cargo-mutants on cp-store: 25 mutants, none survive on the first pass. The
CI job now covers both crates.
Three probes that are not the same thing: CGPreflightPostEventAccess is the
one this app needs, AXIsProcessTrusted only opens the menu fallback, and
IsSecureEventInputEnabled is read to warn and never to abort. can_paste()
depends on the first alone.

Keycode resolution asks which key produces "v" with the Command bit set,
which covers every layout in one formulation. Layouts can be queried by
identifier without activating them, so checking Dvorak no longer means
changing anyone's keyboard. Measured: every layout resolves to 0x09 except
plain Dvorak, which needs 0x2F. The 2.x constant types Cmd+K there.

This is the first unsafe in the project. It stays inside cp-mac-sys, one
operation per block, each with its SAFETY note, enforced by clippy.

CLA moves to version 1.2, adopting the successor clause: the rights granted
travel with the project if it is ever assigned. Signatures already given
stay valid, since a later version only governs later contributions.
…stem

The concealed-content invariant was only tested against in-memory lists.
The harness now writes what a password manager writes, the secret plus its
marker, and asserts nothing is captured: ConcealedType, the transient type,
and three legacy markers from 1Password, KeeWeb and Flycut. Plus the way
back, so exclusion does not quietly become "nothing is ever stored".

The watcher can finally poll from its own thread. Reading the counter alone
materialises no representation, so it stays clear of the file-promise path
where NSPasteboard's documented concurrency bug lives; everything else still
requires the main thread, enforced at compile time.

Twelve properties added where only examples existed: canonical must be
idempotent or deduplication silently breaks, retries are bounded, nothing is
retried after a successful send, and placement is monotonic in size.

Coverage measured with the harness included, because without it every
platform module reads as zero: 96.92% lines, 98.45% functions. A CI job
holds the floor at 90% and includes the harness, so the difficult half
cannot be deleted to make the number look better.
The schema had been designed from the case file rather than from the DDL of
something that has been running for years. Reading it found five things that
were about to be lost and one bug about to be repeated.

auto_vacuum is ignored silently on a database that already has tables, so
2.x ran for months believing it compacted when it did not. Pragmas now run
before any CREATE TABLE, with a test that fails if the mode is not
INCREMENTAL.

Recovered: modified_at as the list order, so copying something again lifts
it instead of burying it; label and app_source in the search index, so a
label or a source application can be searched for; a colour index; the
thumbnail column; and the composite index the type tab actually queries.

Added for the sync that will come, cheap now and expensive later: a
monotonic updated_at, distinct from modified_at because pinning an old item
should not lift it but must still sync, and deleted_at tombstones, without
which a first sync resurrects everything deleted elsewhere.

The budget caught the regression this caused and the fix was not what it
looked like: the corpus was artificial, repeating one word in every row.
With realistic data the correct ordering costs 2.7ms, not 34.
Hiding the panel with orderOut leaves CopyPaste active, so asking who is
frontmost at that moment reads ourselves and the paste returns to the panel.
That reason is real and is inherited literally; the observer 2.x uses to
work around it is not needed, because the watcher is already looking. The
logic is "the last one in front that was not us" and it lives in cp-core
with eight tests, while the platform crate only reports who is frontmost.

Two improvements fall out of writing it that way: the pid is kept alongside
the bundle, which closes finding 24 because identifying a target by bundle
alone picks arbitrarily between two windows of the same application, and an
application that quits stops being a destination instead of staying pinned.

The timer in kCFRunLoopCommonModes turns out not to be an invariant at all.
It stops firing during menu tracking because it shares a run loop with the
interface; a watcher on its own thread has no such run loop to be blocked
by. It is a patch, and it goes away with the architecture that caused it.
…ttery

The paste sequence applies the core state machine against the real system:
the keycode is resolved once per layout rather than on every paste, physical
modifiers are read from the event source because a global hotkey never
reaches the application's keyboard, and the send carries the left-command
device bit with 9ms between down and up, without which Chromium deduplicates
and pasting "sometimes fails".

The harness becomes a battery of 26 numbered cases across six groups:
capture and formats, privacy, watcher, keyboard, permissions, destination
and paste. F4 is the one that was missing, a real round trip through
TextEdit, verified without accessibility by pasting, selecting all, copying
and checking what comes back.

When another application holds the foreground and open cannot activate the
target, the case is skipped with its reason instead of failing: that is the
cooperative activation this project already measured, not a core defect.
cargo-deny had been failing in CI for three pushes: xxhash-rust is BSL-1.0
and the licence was never declared. It fails locally too, so the tool was
right and the mistake was running it once and treating the result as
permanent.

The paster cached its keycode at construction, so changing keyboard layout
with the application running kept the stale one, which in Dvorak types a
different letter. Resolving it costs 458ns, so the cache bought nothing and
carried the bug. Case D5 now checks the paster agrees with the system.
The core knew three kinds; 2.x distinguishes thirteen, and its tab filter
lives off that. Losing it would have removed a whole view of the interface
without anyone noticing until it was missed.

Recovered the six 2.x actually recognised: email, colour in hex, rgb and
hsl, ip, uuid, phone and json. Plus the five it declared and never assigned:
link, audio, video, image by extension, and folder. Added code, which 2.x
does not detect, with a deliberately conservative heuristic and a test that
ordinary prose never trips it.

No regex: these are hand-written parsers, because this runs on every copy.

Family and Kind are now separate things. Family is what the format tells you
-- text, image, files -- and Kind is the fine class the content tells you.
They were the same type before, which is why the fine classification had
nowhere to live.
@rgdevment rgdevment changed the title Feat/stage 0 core spec Feat: stage 0 core spec Sep 14, 2026
@rgdevment
rgdevment merged commit 61250d9 into main Sep 14, 2026
6 of 9 checks passed
@rgdevment
rgdevment deleted the feat/stage-0-core-spec branch September 14, 2026 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant