Skip to content

One text and one fingerprint for a value, written down for every port - #29

Merged
jithinAB merged 2 commits into
masterfrom
pact-kernel/one-fingerprint-in-every-port
Nov 14, 2026
Merged

jithinAB merged 2 commits into
masterfrom
pact-kernel/one-fingerprint-in-every-port

Conversation

@jithinAB

Copy link
Copy Markdown
Contributor

What a condition reads out of a value, the share in-share: puts it in, the digest a person's yes is bound to and the digest tool-snapshot-digest: pins have to come out the same in every runtime, or two runtimes disagree about the same run: a canary sends different customers down the new path, an approval answered in one runtime does not apply in another, a pinned server reads as changed. All four were defined only by what the reference adapter does, in three JSON spellings across three files (conditions._text, suspension.request_digest, mcp_bridge.digest_of). This writes them down as a table both ports are held to, the way spec/comparisons.yaml holds = 80.

Nothing changes in what any of them computes.

What changes

  • spec/fingerprints.yaml: the four encodings in words, and 61 rows, each value written as JSON text so a row means the same thing in every language:
    • the text a condition reads (18 rows: case and spaces, numbers, a yes, non-ASCII words including İ, Σ and ß, records with keys out of order, nested lists);
    • in-share (9 rows: the 15-hex fingerprint and whether the line holds, shares written as 5% and as 0.95, a value that differs only in case and spaces landing in the same bucket, 0% and 100%);
    • request digests (9 rows: arguments in another order, two calls in either order, non-ASCII arguments, no calls);
    • snapshot digests (5 rows: a schema with its keys reordered keeps the digest, a reworded description or added instructions moves it);
    • looks-like: on the five built-in detectors (11 rows) and on declared patterns as the reference's re reads them (9 rows, including end$ holding on "the end\n" and \s holding on U+001F, where an ECMAScript engine answers the opposite).
  • number-as-written: yes on the six rows whose answer turns on how a number was written: 1.0 against 1, 1e+20, an integer past 2^53. The reference keeps all three; a port whose numbers are one type cannot.
  • adapters/python/tests/test_one_fingerprint_in_every_port.py (64 tests) holds the reference to every row through conditions.holds, suspension.request_digest and mcp_bridge.digest_of, keeps a floor under each part, and fails if a number-turning row is unmarked or a marked row turns on no number.
  • CHANGELOG; headline counts (+64 adapter tests).

Evidence

  • The new test: 64 passed. Mutation: dropping sort_keys=True from request_digest fails the four request-digest rows whose argument keys are not written in sorted order, and nothing else.
  • test_the_documentation_site_tells_the_truth.py with it: 78 passed, 5 skipped (they need a built CLI). No source file changes, so the rest of the suite is untouched; the headline-count test needs the whole suite in one session and was not run here.
  • A second port: Bud Meta Harness's TypeScript port of the text, in-share and request digest (@budmh/pact-schema pactCanonicalText, pactShareBucket, pactRequestDigest) agrees with 30 of the 36 rows of those three parts and differs on exactly the six marked rows.

For the maintainers to decide

  • A second, portable encoding. The six marked rows are the case for one: a JavaScript runtime reads {"by": 1.0} as {"by": 1} before it computes anything, so the Python json.dumps form cannot be reproduced there, and a digest over such arguments differs. Proposal: keep today's form as version 1 and give digests a versioned prefix (pyjson1: for it), then add RFC 8785 (JSON Canonicalization Scheme) as version 2 for new digests, which writes numbers the ECMAScript way in every language. A runtime then compares only digests of one version. Not done here because it changes every stored digest.
  • The pattern dialect. looks-like: patterns are read by Python's re. ECMAScript differs on $ and \s (the declared rows), and Rust's regex refuses look-around, which the built-in card number pattern uses. Proposal: state the portable subset in the specification and have pact check refuse a declared pattern outside it (look-around, back-references, \Z, possessive quantifiers), with $ defined as end of text only. Not done here because it changes what pact check accepts.

🤖 Generated with Claude Code

spec/fingerprints.yaml states what a condition reads out of a value, the in-share fingerprint, the request digest a person's yes is bound to and the snapshot digest tool-snapshot-digest: pins, with 61 rows as JSON text, and marks the six rows whose answer turns on how a number was written. test_one_fingerprint_in_every_port.py holds the reference to every row.
…gerprint-in-every-port

# Conflicts:
#	CHANGELOG.md
#	README.md
#	docs/90-REVIEW.md
#	site-docs/index.md
#	site-docs/status/verified.md
@jithinAB

Copy link
Copy Markdown
Contributor Author

Merged master (2234608) into this branch. CI on the merged head: gate / everything passes. gate / newest-pydantic-ai fails only the 5 tests/test_parity.py cases master itself fails (pydantic-ai's newest release adds capabilities the registry does not list yet), the same as master's run 43389520212 and the earlier PACT PRs merged this way. Marking ready and merging, per the owner's standing rule for pact-kernel's upstream PRs.

@jithinAB
jithinAB marked this pull request as ready for review November 14, 2026 19:53
@jithinAB
jithinAB merged commit 0299680 into master Nov 14, 2026
4 of 5 checks passed
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