Skip to content

feat!: v0.3.0 API break, concrete constructors, error taxonomy and evidence floors - #100

Open
ccuetoh wants to merge 3 commits into
mainfrom
claude/codebase-review-v0.1.1-6goaju
Open

feat!: v0.3.0 API break, concrete constructors, error taxonomy and evidence floors#100
ccuetoh wants to merge 3 commits into
mainfrom
claude/codebase-review-v0.1.1-6goaju

Conversation

@ccuetoh

@ccuetoh ccuetoh commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Lands the v0.3.0 batch from the pre-stable review: 14 findings, all of the remaining API breaks, plus the threat model and release policy.

Token derivation does not change. HKDF info strings, format discriminators and the fingerprint encoding are untouched, the golden vector files are byte-identical to main, and v0.2 tokens and persisted fingerprints stay valid. The one exception is spelled out in the changelog: normalization no longer applies canonical field rules to custom orgs, so a non-sriracha path in an identifier, date, contact or name namespace normalizes differently now.

Breaking

  • token.New returns *token.Tokenizer and session.New returns *session.Session. Both interfaces are gone, and ./mock and mockery with them. Consumers that want an interface can declare a narrow one.
  • Equal returns (bool, error). Key rotation and schema drift stop looking identical to a different person.
  • Match and Calibrate take a MatchPolicy. Its zero value is the old behavior; DefaultMatchPolicy adds a two-field evidence floor, so records agreeing only on country=US stop reporting a match at 1.000.
  • session.Option replaces token.Option on session.New, with WithKeyID, WithStrictFingerprint and a WithTokenOptions escape hatch.
  • token.New requires a 32 byte secret.
  • ValidateRecord and RecordFromMap return joined errors with sentinel leaves instead of []error.
  • Renames: MustParsePath to MustParseFieldPath, ROCPoint to PRPoint, Calibration.ROC to .PR, AnnotatedToken.Version to .FieldSetVersion. token.Score and fieldset.Validate are deleted.

Fixed

  • A Session refuses to score tokens minted under a different FieldSet. The fingerprint was computed at New to catch drift but was only stamped, never checked.
  • Calibrate returns the plateau midpoint. The old tie-break returned the bottom edge, the operating point sitting closest to the non-match distribution.
  • Absence is len(field) == 0 everywhere, so a JSON round trip that turns null into "" cannot flip a verdict.
  • Field-specific normalization no longer fires on custom orgs, where it silently stripped separators from acme::identifier::mrn and forced acme::date::hired into ISO 8601.

Docs

THREAT_MODEL.md states what each token form leaks and who has to hold the secret, and says plainly that tokens are pseudonymous rather than anonymous. SECURITY.md is rewritten around tags, since a fix on main is invisible to go get. RELEASING.md and GOVERNANCE.md are new. 19 godoc examples, including the wire round trip the library exists for.

The v0.2 tag is two components, which Go does not resolve. RELEASING.md records the format and the README install line drops the version pin.

Verification

100% coverage held, golangci-lint clean, go vet clean, all nine example programs run, golden vectors pass with their files untouched.

Quality metrics on the gated bench suite are unchanged from v0.2: OpenSanctions AUROC 0.9152 and best F1 0.8745, FEBRL4 and NCVR AUROC 1.0000 with F1 at or above 0.999.

The diff was then reviewed adversarially across five lenses, with every finding sent to an independent skeptic. 10 of 29 survived and are fixed in the last commit; the other 19 were refuted, most as pre-existing behavior or design preference. Three of the survivors were behavioral:

  • Tokenizing checked only the destroyed flag, but the pooled HMACs read their key lazily. A buffer destroyed by memguard.Purge or its interrupt handler left the flag false, so the HMAC keyed itself with nil and emitted tokens derived under an empty secret. Key liveness now gates tokenization.
  • Calibrate computed F1 as the harmonic mean of precision and recall, which lands a unit in the last place away from itself for count pairs that are mathematically equal. That fragmented plateaus and returned an edge. F1 now divides the counts.
  • Calibrate kept pairs with nothing comparable in the sweep, where threshold 0 counted them as matches even though Match never calls them one.

Each of the three fails under mutation of its guard.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BLyvbE4thBJxFxs8xq6xJm


Generated by Claude Code

claude added 3 commits August 24, 2026 03:44
Lands the pre-stable API batch from the v0.1.1 review.

Breaking:
- token.New and session.New return concrete *Tokenizer and *Session.
  Both interfaces are gone, and ./mock with them.
- Equal returns (bool, error) so key rotation and schema drift stop
  looking identical to a non-match.
- Match and Calibrate take a MatchPolicy. Its zero value is the old
  behavior; DefaultMatchPolicy adds a two-field evidence floor so
  records sharing only a country stop scoring 1.0.
- ValidateRecord and RecordFromMap return joined errors with sentinel
  leaves instead of []error.
- session.Option replaces token.Option on session.New.
- token.New requires a 32 byte secret.
- MustParsePath is now MustParseFieldPath, ROCPoint is PRPoint,
  Calibration.ROC is Calibration.PR, AnnotatedToken.Version is
  FieldSetVersion.
- token.Score is deleted. Match subsumes it.

Fixes:
- session.Match, Equal and MatchCLK reject tokens minted under a
  different FieldSet.
- Field-specific normalization no longer applies to custom orgs.
- Calibrate returns the plateau midpoint instead of its bottom edge.
- Absence is len(field) == 0 everywhere, so JSON round-trips cannot
  flip a verdict.

Token bytes and fingerprints are unchanged. v0.2 tokens stay valid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLyvbE4thBJxFxs8xq6xJm
Adds THREAT_MODEL.md, RELEASING.md and GOVERNANCE.md, rewrites the
security policy around tags, and documents the whole API break in the
changelog. Adds 19 godoc examples, including the wire round trip the
library exists for.

The v0.2 tag is two components, which Go does not resolve, so the
README install line drops the version pin and RELEASING.md records the
tag format.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLyvbE4thBJxFxs8xq6xJm
Adversarial review of the branch confirmed ten findings. Three touch
behavior.

Tokenize now refuses to run when the locked key buffers are gone, not
just when Destroy set the flag. The pooled HMACs read their key lazily,
so a buffer destroyed by memguard.Purge or its interrupt handler would
key them with nil and emit tokens derived under an empty secret.

Calibrate computes F1 from the counts. The harmonic mean of precision
and recall lands a unit in the last place away from itself for count
pairs that are mathematically equal, which split one plateau into
several and returned an edge instead of the midpoint.

Calibrate excludes pairs with nothing comparable under every policy.
Match reports them as non-matches at every threshold, so counting them
as matches at threshold 0 fit the threshold against a decision Match
never makes.

The rest are documentation. The changelog said Equal previously
returned false for two all-absent tokens; it returned true, and that is
the one bool that changes in this release. ValidateRecord reports in
schema order, not sorted order. A joined error cannot be ranged over.
Only a Session stamps and checks the fingerprint.

Quality metrics are unchanged on all three corpora.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLyvbE4thBJxFxs8xq6xJm
@github-actions

Copy link
Copy Markdown

🐰 Bencher Report

Branchclaude/codebase-review-v0.1.1-6goaju
Testbedubuntu-latest
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
nanoseconds (ns)
(Result Δ%)
Upper Boundary
nanoseconds (ns)
(Limit %)
BenchmarkNgrams-4📈 view plot
🚷 view threshold
1,003.00 ns
(+6.10%)Baseline: 945.32 ns
1,228.91 ns
(81.62%)
BenchmarkNormalizeCountry-4📈 view plot
🚷 view threshold
444.30 ns
(+10.70%)Baseline: 401.35 ns
521.75 ns
(85.16%)
BenchmarkNormalizeDate-4📈 view plot
🚷 view threshold
693.00 ns
(+27.65%)Baseline: 542.89 ns
705.76 ns
(98.19%)
BenchmarkNormalizeIdentifier-4📈 view plot
🚷 view threshold
702.30 ns
(+15.51%)Baseline: 608.00 ns
790.40 ns
(88.85%)
BenchmarkNormalizeName-4📈 view plot
🚷 view threshold
2,017.00 ns
(+17.24%)Baseline: 1,720.36 ns
2,236.46 ns
(90.19%)
BenchmarkTokenizeDeterministic-4📈 view plot
🚷 view threshold
4,465.00 ns
(+10.06%)Baseline: 4,056.77 ns
5,273.80 ns
(84.66%)
BenchmarkTokenizeProbabilistic-4📈 view plot
🚷 view threshold
28,899.00 ns
(-16.02%)Baseline: 34,411.88 ns
44,735.45 ns
(64.60%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

Copy link
Copy Markdown

🐰 Bencher Report

Branchclaude/codebase-review-v0.1.1-6goaju
Testbedubuntu-latest

⚠️ WARNING: Truncated view!

The full continuous benchmarking report exceeds the maximum length allowed on this platform.

🚨 27 Alerts

🐰 View full continuous benchmarking report in Bencher

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (1aa3492) to head (152f12f).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #100   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           12        13    +1     
  Lines          881       957   +76     
=========================================
+ Hits           881       957   +76     
Files with missing lines Coverage Δ
errors.go 100.00% <100.00%> (ø)
fields.go 100.00% <100.00%> (ø)
fieldset/fieldset.go 100.00% <100.00%> (ø)
fieldset/registry.go 100.00% <ø> (ø)
normalize/normalize.go 100.00% <100.00%> (ø)
recordmap.go 100.00% <100.00%> (ø)
session/session.go 100.00% <100.00%> (ø)
token/bloom.go 100.00% <100.00%> (ø)
token/calibrate.go 100.00% <100.00%> (ø)
token/compare.go 100.00% <100.00%> (ø)
... and 2 more

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.

2 participants