Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
What changed in PACT, newest first. Each entry says what an author can now write, or what now
holds that did not; the commits are listed under it for whoever wants the detail.

## 2026-11 — one text and one fingerprint for a value, written down (13 November)

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 were defined by what the reference adapter
happened to do, in three JSON spellings across three files. A second runtime could match them only
by reading that code. Nothing here changes what any of them computes.

- **`spec/fingerprints.yaml`** states the four, with 61 rows a port is held to: the text of a word,
a number, a record and a list (18), in-share fingerprints and answers (9), request digests (9),
snapshot digests (5), the five built-in detectors (11) and declared detector patterns as the
reference's regular expressions read them (9, including `$` before a final newline and `\s` on
U+001F). Every value is written as JSON text, so a row means the same thing in every language.
- **`number-as-written: yes`** marks the six rows whose answer turns on how a number was written
(`1.0` against `1`, `1e+20`, an integer past 2^53). A port whose numbers are one type cannot
reproduce them: Bud Meta Harness's TypeScript port of the four agrees with the other 30 text,
share and digest rows and differs on exactly these six.
- **`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`,
and fails if the markings drift from the rows.

## 2026-11 — how a run ends, the egress boundary said out loud, and two things that read as one thing and did another (15 November)

Four rulings, each made with Bud Agent Flow's engine and corpus in front of it.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1000,11 +1000,11 @@ framework is an extra.
|---|---|
| **Design** | Thesis, 29 binding decisions, FRD (120 requirements), 60-row refusal ledger — complete |
| **Research** | 14 source-grounded studies, ~15,750 lines, over 140 repos (~15 GB) + 57 papers |
| **Code** | Loader, diagnostics, schema engine, CLI, harness, resolver, evals, SLO — **5081 tests (1687 Rust + 3394 adapter), clippy clean, TypeScript type-checked** |
| **Code** | Loader, diagnostics, schema engine, CLI, harness, resolver, evals, SLO — **5145 tests (1687 Rust + 3458 adapter), clippy clean, TypeScript type-checked** |
| **Adapters** | **All 7 named targets**, proven against one shared conformance suite |

```bash
./scripts/test-all.sh # 5081 tests, Rust + 7 adapters, fully offline
./scripts/test-all.sh # 5145 tests, Rust + 7 adapters, fully offline
```

**What is still owed** is not hidden — it lives in
Expand Down
123 changes: 123 additions & 0 deletions adapters/python/tests/test_one_fingerprint_in_every_port.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""A value has one text and one fingerprint, whichever runtime computes them.

`in-share: 5%` sends the same five customers in a hundred down the new path in
every runtime only if every runtime reads the same text out of a value and
takes the same fingerprint of it. A person's yes stays bound to the calls they
saw only if the digest the runtime stored is the one it recomputes. And
`tool-snapshot-digest:` pins a server only if the pin is computed the same way
in the runtime that checks it as in the tool that wrote it. All four were
defined by what this port happened to do — three different JSON spellings in
three files — and a second runtime could only match them by reading the code.

`spec/fingerprints.yaml` writes them down, with the rows they are held to. Each
row's value is JSON text, so it means the same thing in every language; this
file checks that the reference gives every row's answer through the functions a
run actually goes through. A second port reads the same file.

Mutation: write `in-share`'s fingerprint over the value's `repr` instead of
`_text`, or drop `sort_keys=True` from `request_digest`. The in-share rows, or
the request-digest rows whose argument keys are not written in sorted order, go
red here.
"""

from __future__ import annotations

import hashlib
import json
import sys
from pathlib import Path
from types import SimpleNamespace

import pytest
import yaml

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))

from pact_adapters import conditions, detectors, mcp_bridge, suspension # noqa: E402

REPO = Path(__file__).resolve().parents[3]
TABLE = yaml.safe_load((REPO / "spec" / "fingerprints.yaml").read_text())


def _id(row: dict) -> str:
return json.dumps(row, ensure_ascii=False)[:60]


def test_the_table_still_holds_the_rows_it_was_written_with() -> None:
"""A pin that can be emptied is not a pin."""
counts = {part: len(TABLE[part]) for part in ("text", "in-share", "request-digest", "snapshot-digest", "looks-like")}
counts["declared"] = len(TABLE["declared"]["rows"])
floors = {"text": 18, "in-share": 9, "request-digest": 9, "snapshot-digest": 5, "looks-like": 11, "declared": 9}
shrunk = {part: (counts[part], floor) for part, floor in floors.items() if counts[part] < floor}
assert not shrunk, f"spec/fingerprints.yaml lost rows (now, written with): {shrunk}"


@pytest.mark.parametrize("row", TABLE["text"], ids=_id)
def test_a_value_reads_as_one_text(row: dict) -> None:
assert conditions._text(json.loads(row["value"])) == row["text"]


@pytest.mark.parametrize("row", TABLE["in-share"], ids=_id)
def test_a_value_falls_in_one_share(row: dict) -> None:
value = json.loads(row["value"])
fingerprint = hashlib.sha256(conditions._text(value).encode()).hexdigest()[:15]
assert fingerprint == row["fingerprint"]
assert conditions.holds({"in-share": row["in-share"]}, value) is row["holds"]


def test_an_absent_value_is_in_no_share() -> None:
assert conditions.holds({"in-share": "100%"}, None) is False
assert conditions.holds({"in-share": "100%"}) is False


@pytest.mark.parametrize("row", TABLE["request-digest"], ids=_id)
def test_a_request_has_one_digest(row: dict) -> None:
calls = [SimpleNamespace(name=name, args=args) for name, args in json.loads(row["calls"])]
assert suspension.request_digest(calls) == row["digest"]


@pytest.mark.parametrize("row", TABLE["snapshot-digest"], ids=_id)
def test_a_server_has_one_snapshot_digest(row: dict) -> None:
assert mcp_bridge.digest_of(json.loads(row["published"]), row["instructions"]) == row["digest"]


@pytest.mark.parametrize("row", TABLE["looks-like"], ids=_id)
def test_a_built_in_detector_recognises_one_set_of_values(row: dict) -> None:
assert conditions.holds({"looks-like": row["looks-like"]}, json.loads(row["value"])) is row["holds"]


@pytest.mark.parametrize("row", TABLE["declared"]["rows"], ids=_id)
def test_a_declared_detector_reads_its_patterns_one_way(row: dict) -> None:
written = {name: {"looks-like": patterns} for name, patterns in TABLE["declared"]["detectors"].items()}
token = detectors.DECLARED.set(detectors.declared_in({"detectors": written}))
try:
assert conditions.holds({"looks-like": row["looks-like"]}, json.loads(row["value"])) is row["holds"]
finally:
detectors.DECLARED.reset(token)


def test_the_rows_a_one_number_type_port_cannot_reproduce_are_marked() -> None:
"""`number-as-written` is on every row whose answer turns on how a number was written, and on no other.

The reference keeps `1.0` apart from `1`, writes `1e20` as `1e+20`, and keeps
integers past 2^53 exact; a port whose numbers are one type does none of the
three. A row whose answer depends on them and is not marked would read, to
that port, as a defect in its own code."""

def turns_on_a_number(value: object) -> bool:
if isinstance(value, bool) or value is None or isinstance(value, str):
return False
if isinstance(value, float):
return value.is_integer() or abs(value) >= 1e16
if isinstance(value, int):
return abs(value) > 2**53
if isinstance(value, dict):
return any(turns_on_a_number(v) for v in value.values())
if isinstance(value, list):
return any(turns_on_a_number(v) for v in value)
return False

for part, key in (("text", "value"), ("request-digest", "calls")):
for row in TABLE[part]:
marked = row.get("number-as-written") is True
assert marked == turns_on_a_number(json.loads(row[key])), (part, row[key])
2 changes: 1 addition & 1 deletion docs/90-REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ them and changes nothing.
|---|---|
| Schema | **43 groups, 262 field entries, 207 distinct field names** (core 123, expert 69, both 15), 40 closed enums, 27 cross-reference constraints, **0 fields missing `tier:` or `surface:`** |
| Rust | 5 crates, **1687 tests**, `unsafe_code = forbid`, clippy-deny, edition 2024 |
| Python | **47 modules** (`find adapters/python/src -name "*.py" \| wc -l`), **3394 tests**, **nine transports** — seven model-bound (`anthropic`, `autogen`, `langchain`, `langgraph`, `ollama`, `openai_agents`, `pydantic_ai`), one bound to a remote *agent* (`a2a`), one deterministic mock; `_metering.py`, `_summarise.py` and `_tool_choice.py` are shared code the transports import, which is what the `_` says — offline by default |
| Python | **47 modules** (`find adapters/python/src -name "*.py" \| wc -l`), **3458 tests**, **nine transports** — seven model-bound (`anthropic`, `autogen`, `langchain`, `langgraph`, `ollama`, `openai_agents`, `pydantic_ai`), one bound to a remote *agent* (`a2a`), one deterministic mock; `_metering.py`, `_summarise.py` and `_tool_choice.py` are shared code the transports import, which is what the `_` says — offline by default |
| TypeScript | 5 modules, 2,305 lines, a second port of **the stepping, the ceilings and the stage path** (`harness.ts:15-21`: *"AC-5.3's bar is the stage path"*) — **not** of the loop entire: no interceptor chain, no durable suspension record, no ledger (`notDoneHere`; a park returns `halted: "suspended"` and writes nothing down). This row is load-bearing for the Phase 3 pricing below (E4) |
| CLI | **6 verbs**: `check`, `show`, `waits`, `discover`, `card`, `help` |
| Worked example | 42 files, 1128 lines, **132 distinct authored keys**, `pact check` → OK (498 settings) |
Expand Down
2 changes: 1 addition & 1 deletion site-docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ so plainly rather than leaving it to be discovered.

| | |
|---|---|
| Tests | **5,081** — 1687 Rust, 3394 adapter |
| Tests | **5,145** — 1687 Rust, 3458 adapter |
| Schema kinds | **69** |
| Framework targets | **7**, over 2 runtimes |
| Eve capabilities accounted for | **101 of 101** |
Expand Down
4 changes: 2 additions & 2 deletions site-docs/status/verified.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ from an earlier document.
| Claim | Value | How to check |
|---|---|---|
| Rust tests | **1687** | `cargo test --workspace` |
| Adapter tests | **3394** | `cd adapters/python && uv run pytest tests/ -q` |
| Total | **5,081** | both of the above |
| Adapter tests | **3458** | `cd adapters/python && uv run pytest tests/ -q` |
| Total | **5,145** | both of the above |
| TypeScript type-checks | clean | `cd adapters/typescript && npx tsc --noEmit` |
| Lints | clean | `cargo clippy --all-targets` |
| Worked example loads | 498 settings | `cargo run -p pact-cli -- check examples/refund-desk` |
Expand Down
Loading
Loading