Skip to content

Release 0.5.0: a scheme that cannot disagree with itself - #8

Merged
neosergio merged 1 commit into
mainfrom
feat/v0.5.0
Jul 14, 2026
Merged

Release 0.5.0: a scheme that cannot disagree with itself#8
neosergio merged 1 commit into
mainfrom
feat/v0.5.0

Conversation

@neosergio

@neosergio neosergio commented Jul 14, 2026

Copy link
Copy Markdown
Owner

generate_reference() and verify_reference() take their configuration as loose arguments, and neither can see what the other was told. Two things went wrong, both quietly.

Verify with a different alphabet, prefix or separator than the reference was made with and it returns False. False means "this reference is a typo", so a caller tells a customer their perfectly good reference is invalid when it was the caller's own configuration that was wrong. Three of five plausible mistakes did exactly that on a valid reference.

Worse: verify a reference that has no check character -- the default -- and it reads the last character of the suffix as one, passing about one time in len(alphabet). Measured: 105 of 1000 unchecked references "verified". It was noise, and it never said so.

Neither can be fixed inside those functions, because neither can see the other's configuration. CompactRef holds it once; generate() and verify() read the same object, so there is nothing left to get out of step, and verify() on a scheme without check=True raises rather than guessing. The configuration is validated at construction, so a bad alphabet fails on the line that made it rather than on the ten-thousandth call. The functions stay: the class is a layer over them.

Hypothesis found a real bug on its first run. expected_rejected_inserts() returned a NEGATIVE count -- -1.65e-07 for two references over ten digits, where the answer is 1e-10. Spelled directly, the formula subtracts two nearly equal numbers and the true value falls below what a double can carry. It goes through log1p and expm1 now, which keep their precision exactly where that cancellation happens.

Hypothesis also killed one of my own invariants, which is the more useful finding. I asserted that successive attempts never repeat; they can. Two attempts are independent draws, not a walk through unused values, so they collide exactly as two sources do -- nine attempts into a 64-suffix bucket did. A retry loop is therefore not guaranteed to find a free reference in a fixed number of tries: it must keep checking, and it must give up rather than spin. Both the docstring and the README now say so.

The README's retry loop was pseudocode, which is a way of saying we had not run it. It runs now, in tests/test_integration.py, against a real table with a real unique constraint, a real IntegrityError and a real retry -- including the case where the format is hopeless and the loop must raise instead of looping forever.

Summary by CodeRabbit

  • New Features

    • Added CompactRef, a reusable configuration for generating, verifying, and sizing compact references.
    • Added configurable prefixes, separators, alphabets, namespaces, date formats, and check characters.
    • Added collision probability, capacity, and suffix-length planning helpers.
    • Updated collision-recovery guidance with a SQLAlchemy retry example.
  • Bug Fixes

    • Improved accuracy for expected rejected-insert calculations, especially with small reference spaces.
  • Documentation

    • Clarified that reference attempts are independent draws.
    • Updated release notes and roadmap for version 0.5.0.

generate_reference() and verify_reference() take their configuration as
loose arguments, and neither can see what the other was told. Two things
went wrong, both quietly.

Verify with a different alphabet, prefix or separator than the reference
was made with and it returns False. False means "this reference is a
typo", so a caller tells a customer their perfectly good reference is
invalid when it was the caller's own configuration that was wrong. Three
of five plausible mistakes did exactly that on a valid reference.

Worse: verify a reference that has no check character -- the default --
and it reads the last character of the suffix as one, passing about one
time in len(alphabet). Measured: 105 of 1000 unchecked references
"verified". It was noise, and it never said so.

Neither can be fixed inside those functions, because neither can see the
other's configuration. CompactRef holds it once; generate() and verify()
read the same object, so there is nothing left to get out of step, and
verify() on a scheme without check=True raises rather than guessing. The
configuration is validated at construction, so a bad alphabet fails on the
line that made it rather than on the ten-thousandth call. The functions
stay: the class is a layer over them.

Hypothesis found a real bug on its first run. expected_rejected_inserts()
returned a NEGATIVE count -- -1.65e-07 for two references over ten digits,
where the answer is 1e-10. Spelled directly, the formula subtracts two
nearly equal numbers and the true value falls below what a double can
carry. It goes through log1p and expm1 now, which keep their precision
exactly where that cancellation happens.

Hypothesis also killed one of my own invariants, which is the more useful
finding. I asserted that successive attempts never repeat; they can. Two
attempts are independent draws, not a walk through unused values, so they
collide exactly as two sources do -- nine attempts into a 64-suffix bucket
did. A retry loop is therefore not guaranteed to find a free reference in
a fixed number of tries: it must keep checking, and it must give up rather
than spin. Both the docstring and the README now say so.

The README's retry loop was pseudocode, which is a way of saying we had
not run it. It runs now, in tests/test_integration.py, against a real
table with a real unique constraint, a real IntegrityError and a real
retry -- including the case where the format is hopeless and the loop must
raise instead of looping forever.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Version 0.5.0 introduces CompactRef, a reusable configuration for reference generation, verification, and sizing. It also improves rejection-count precision, documents retry semantics, adds SQLAlchemy integration coverage, expands property-based tests, and updates release documentation.

Changes

CompactRef release

Layer / File(s) Summary
CompactRef scheme API
src/compactref/reference.py, src/compactref/__init__.py, README.md, pyproject.toml
Adds the frozen CompactRef configuration with generation, verification, collision sizing, package exports, and documented reuse patterns.
Stable calculations and database retry flow
src/compactref/core.py, README.md, tests/test_integration.py
Uses numerically stable rejection calculations, clarifies independent attempts, and tests retries against a real unique database constraint.
Property-based scheme validation
tests/test_properties.py, pyproject.toml
Adds Hypothesis coverage for reference shape, determinism, verification, attempts, compatibility, rejection bounds, and sizing recommendations.
Release and roadmap updates
CHANGELOG.md, ROADMAP.md
Documents the 0.5.0 release, shipped changes, and revised 1.0.0 API contract decisions.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant create_order
  participant CompactRef
  participant Database
  Caller->>create_order: create order
  create_order->>CompactRef: generate reference for attempt
  CompactRef-->>create_order: candidate reference
  create_order->>Database: insert with unique reference
  Database-->>create_order: success or IntegrityError
  create_order->>CompactRef: generate next attempt after collision
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: Release 0.5.0 adds a unified reference scheme so generation and verification use the same configuration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v0.5.0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@neosergio
neosergio merged commit c8a9c15 into main Jul 14, 2026
7 of 8 checks passed
@neosergio
neosergio deleted the feat/v0.5.0 branch July 14, 2026 02:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/compactref/reference.py`:
- Around line 81-87: Update __post_init__ to validate that separator is not
present in alphabet, preventing _strip_label from removing generated suffix
characters, and ensure date_format produces a fixed-length date string across
supported dates rather than relying only on the January 1st _body_length
calculation. Raise ValueError for incompatible configurations while preserving
the existing suffix_length, alphabet, and namespace validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89807188-b14a-4d5f-b91c-eb3bfd4c28f2

📥 Commits

Reviewing files that changed from the base of the PR and between 6631839 and 240ed02.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • README.md
  • ROADMAP.md
  • pyproject.toml
  • src/compactref/__init__.py
  • src/compactref/core.py
  • src/compactref/reference.py
  • tests/test_integration.py
  • tests/test_properties.py

Comment on lines +81 to +87
def __post_init__(self) -> None:
if self.suffix_length < 1:
raise ValueError("suffix_length must be greater than zero")

_validate_alphabet(self.alphabet)
_normalize_namespace(self.namespace)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate separator and date_format to prevent silent verification failures.

verify() expects a strictly fixed-length payload and strips all occurrences of separator. This introduces two failure modes for valid references:

  1. If separator is present in alphabet, _strip_label will delete characters from the generated suffix, causing verification to fail.
  2. If date_format produces variable-length strings (e.g., using %-d or %B), the _body_length calculation based on January 1st will later reject references generated on dates with different formatted lengths.

Consider adding guards in __post_init__ to fail fast on incompatible configurations.

🛡️ Proposed fixes
     def __post_init__(self) -> None:
         if self.suffix_length < 1:
             raise ValueError("suffix_length must be greater than zero")
 
         _validate_alphabet(self.alphabet)
         _normalize_namespace(self.namespace)
+
+        if self.separator and self.separator in self.alphabet:
+            raise ValueError("separator cannot be in the alphabet")
+
+        len_jan = len(datetime(2026, 1, 1).strftime(self.date_format))
+        len_dec = len(datetime(2026, 12, 31).strftime(self.date_format))
+        if len_jan != len_dec:
+            raise ValueError("date_format must produce a fixed-length string")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __post_init__(self) -> None:
if self.suffix_length < 1:
raise ValueError("suffix_length must be greater than zero")
_validate_alphabet(self.alphabet)
_normalize_namespace(self.namespace)
def __post_init__(self) -> None:
if self.suffix_length < 1:
raise ValueError("suffix_length must be greater than zero")
_validate_alphabet(self.alphabet)
_normalize_namespace(self.namespace)
if self.separator and self.separator in self.alphabet:
raise ValueError("separator cannot be in the alphabet")
len_jan = len(datetime(2026, 1, 1).strftime(self.date_format))
len_dec = len(datetime(2026, 12, 31).strftime(self.date_format))
if len_jan != len_dec:
raise ValueError("date_format must produce a fixed-length string")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compactref/reference.py` around lines 81 - 87, Update __post_init__ to
validate that separator is not present in alphabet, preventing _strip_label from
removing generated suffix characters, and ensure date_format produces a
fixed-length date string across supported dates rather than relying only on the
January 1st _body_length calculation. Raise ValueError for incompatible
configurations while preserving the existing suffix_length, alphabet, and
namespace validation.

@coderabbitai coderabbitai Bot mentioned this pull request Jul 14, 2026
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