Release 0.5.0: a scheme that cannot disagree with itself - #8
Conversation
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.
📝 WalkthroughWalkthroughVersion 0.5.0 introduces ChangesCompactRef release
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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
CHANGELOG.mdREADME.mdROADMAP.mdpyproject.tomlsrc/compactref/__init__.pysrc/compactref/core.pysrc/compactref/reference.pytests/test_integration.pytests/test_properties.py
| 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) | ||
|
|
There was a problem hiding this comment.
🎯 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:
- If
separatoris present inalphabet,_strip_labelwill delete characters from the generated suffix, causing verification to fail. - If
date_formatproduces variable-length strings (e.g., using%-dor%B), the_body_lengthcalculation 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.
| 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.
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
CompactRef, a reusable configuration for generating, verifying, and sizing compact references.Bug Fixes
Documentation