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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ All notable changes are documented here. The project follows semantic versioning

_No changes yet._

## 0.13.0 - 2026-09-19

- Added an offline-first, bounded OpenReview assignment/aggregate-score edge plan
with exact invitation, group, paper, reviewer, and digest validation. Real
writes require an explicitly injected transport and matching SHA-256
confirmation; dry-run is the default. Per-batch read-back supports safe
reconciliation after ambiguous partial failures, with one atomic
no-overwrite local plan/receipt evidence file and synthetic mock integration
tests. No live API write adapter or venue credentials are shipped.

## 0.12.0 - 2026-09-19

- Added an opt-in exact, bounded `maximin` strategy for small reviewer panels.
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ authors:
- family-names: "Yan"
given-names: "Weiping"
alias: "appleweiping"
version: 0.12.0
version: 0.13.0
date-released: 2026-09-19
license: MIT
repository-code: "https://github.com/appleweiping/PeerMatchLab"
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,10 @@ only direct group membership and submission metadata; the explicit expertise
option additionally retrieves scoped profile/publication evidence. Neither
mode infers conflicts, expands nested groups, or downloads attachments.

For an offline-first assignment edge plan and a synthetic in-memory write-back
replay, see [assignment edge preparation](docs/openreview-publish.md). The
default is dry-run; no live OpenReview write adapter ships with this package.

## Python API

```python
Expand Down
65 changes: 65 additions & 0 deletions docs/openreview-publish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Assignment edge preparation and mock write-back

This is an offline-first protocol slice, **not a production OpenReview publisher**.
The frozen Matcher reference creates an assignment edge and a matching aggregate-score
edge for every paper/reviewer pair. PeerMatchLab prepares those two edge sets and
audits an independently produced `MatchPlan`; it does not ship an HTTP POST client,
authentication, invitation discovery, or a hosted matching service. No test or
example contacts a real venue.

`prepare_publish_plan` accepts the exact assignment invitation, score invitation,
reviewer group, label, paper IDs, and reviewer IDs in a `PublishScope`. The IDs must
exactly match the audited domain inputs. The plan must have complete demand, no
conflicts, capacity excess, duplicate pair, or other hard-audit violation. Input
scores are copied verbatim to both invitation-specific edges; the canonical JSON
of all scoped IDs and edge content determines the SHA-256 plan digest. The maximum
is 1,000 assignments and 2,000 edges. This preparation makes no network calls.

`publish_assignment_plan` is dry-run by default, even if a transport was supplied.
An actual write requires **all** of `publish=True`, a caller-supplied
`AssignmentWriteTransport`, and `confirm_sha256` equal to the immutable plan digest.
The transport must fetch exact invitation IDs, group membership, and paper IDs,
and must return the complete set of edges for the specified two invitations,
label, and paper heads. Both returned context and existing edge identity/content
are checked before a write. Any unexpected or divergent edge aborts; this code
never deletes or overwrites an edge **provided the injected adapter honors its
required atomic create-only contract**. Each edge insertion must fail on an
existing identity; a plain OpenReview bulk upsert cannot safely implement this
transport because another actor may insert an edge after the preflight read.
Batches contain at most 100 edges.

After every batch, the context and complete edge set are read back. An ambiguous
partial transport failure, unavailable read-back, or not-yet-visible edge returns
an `incomplete` receipt, not a blind automatic retry. Repeating the call re-reads
the remote set and sends only missing edges; existing exact edges produce a
zero-write `complete` receipt. A concurrent actor that changes a label-scoped
edge or venue context causes fail-closed behavior when the conditional-insert
contract is upheld. Operators must inspect
`confirmed_edges`, `failure`, and the authoritative venue state before retrying.
`write_publish_artifact` stores both objects in one canonical JSON file. It
writes a temporary file beside the requested path and hard-links it into place,
so an existing or concurrently created destination is never replaced. The
temporary name is removed even when installation fails. Paper IDs are limited
to 1,000, reviewer IDs to 10,000, and conflicts to 100,000; generator inputs
are read only up to one record past these ceilings before rejection.

Try the purely in-memory synthetic replay:

```bash
uv run python examples/openreview_publish_mock.py
```

It prints one digest, then `dry_run`, `complete`, and `0` attempted batches
on an idempotent re-run. `tests/test_openreview_publish.py` also simulates a
partial POST, stale read-back, changed context, duplicate/foreign edges, and
read-back failure. There are no credentials, real invitation IDs, or network
operations in either sample.

This boundary intentionally leaves OpenReview API v1/v2 edge payload shape,
invitation authorization, pagination of existing edges, eventual-consistency
timing, atomic conditional-insert support, and production conflict policy to a
future separately reviewed adapter. If a real endpoint cannot provide atomic
create-only semantics, the write path must remain disabled; preflight alone
is insufficient.
The caller must not treat a mock-transport `complete` receipt as proof that a
real venue was updated.
4 changes: 2 additions & 2 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Release process

A push of a semantic-version tag such as `v0.12.0` invokes the release
A push of a semantic-version tag such as `v0.13.0` invokes the release
workflow. The workflow refuses a tag that differs from `project.version`,
installs the committed `uv.lock`, runs the static and coverage-gated suite
(including a separate ≥90% covered-branches/total-branches check),
Expand All @@ -17,7 +17,7 @@ Verify a downloaded file with:

```bash
sha256sum --check SHA256SUMS
gh attestation verify peermatchlab-0.12.0-py3-none-any.whl \
gh attestation verify peermatchlab-0.13.0-py3-none-any.whl \
--repo appleweiping/PeerMatchLab
```

Expand Down
68 changes: 68 additions & 0 deletions examples/openreview_publish_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Synthetic assignment-publish replay; no HTTP client or venue credentials."""

from peermatchlab import (
Assignment,
AssignmentEdge,
Document,
Expert,
MatchPlan,
PublishScope,
RemoteContext,
prepare_publish_plan,
publish_assignment_plan,
)


class MemoryTransport:
def __init__(self) -> None:
self.edges: dict[tuple[str, str, str, str], AssignmentEdge] = {}

def inspect_context(self, scope: PublishScope) -> RemoteContext:
return RemoteContext(
scope.assignment_invitation,
scope.score_invitation,
scope.reviewer_group,
scope.paper_ids,
scope.reviewer_ids,
)

def read_edges(self, scope: PublishScope) -> tuple[AssignmentEdge, ...]:
return tuple(self.edges.values())

def post_edges(self, edges: tuple[AssignmentEdge, ...]) -> None:
for edge in edges:
if edge.key in self.edges:
raise RuntimeError("synthetic conditional-insert conflict")
self.edges[edge.key] = edge


documents = (Document("synthetic-paper", "Synthetic retrieval paper", required_experts=1),)
experts = (Expert("~SyntheticReviewer1", "Synthetic Reviewer", capacity=1),)
plan = MatchPlan(
(Assignment("synthetic-paper", "~SyntheticReviewer1", 0.75, 1),),
{"synthetic-paper": 0},
"synthetic",
0.75,
)
scope = PublishScope(
"Synthetic/-/Assignment",
"Synthetic/-/Aggregate_Score",
"Synthetic/Reviewers",
"peer-matchlab-demo",
("synthetic-paper",),
("~SyntheticReviewer1",),
)
prepared = prepare_publish_plan(plan, scope, documents=documents, experts=experts, default_demand=1)
transport = MemoryTransport()
print(prepared.digest)
print(publish_assignment_plan(prepared, transport=transport).status)
print(
publish_assignment_plan(
prepared, transport=transport, publish=True, confirm_sha256=prepared.digest
).status
)
print(
publish_assignment_plan(
prepared, transport=transport, publish=True, confirm_sha256=prepared.digest
).attempted_batches
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "peermatchlab"
version = "0.12.0"
version = "0.13.0"
description = "Transparent, constraint-aware expert matching for offline workflows"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
22 changes: 21 additions & 1 deletion src/peermatchlab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,28 @@
fetch_reviewer_expertise,
write_openreview_snapshot,
)
from peermatchlab.openreview_publish import (
AssignmentEdge,
AssignmentWriteTransport,
PublishPlan,
PublishReceipt,
PublishScope,
RemoteContext,
prepare_publish_plan,
publish_assignment_plan,
write_publish_artifact,
)
from peermatchlab.scoring import MatchScorer, ScoreWeights

__all__ = [
"Affinity",
"AffinityScorer",
"Assignment",
"AssignmentDiagnostics",
"AssignmentEdge",
"AssignmentEngine",
"AssignmentStrategy",
"AssignmentWriteTransport",
"DemandDiagnostic",
"Document",
"EmbeddingProvider",
Expand Down Expand Up @@ -117,6 +130,10 @@
"OpenReviewSnapshot",
"OpenReviewTransportError",
"Publication",
"PublishPlan",
"PublishReceipt",
"PublishScope",
"RemoteContext",
"RetryPolicy",
"ScoreWeights",
"TermContribution",
Expand All @@ -135,6 +152,8 @@
"load_reviewer_ids",
"local_domain_inputs",
"openreview_submissions_from_records",
"prepare_publish_plan",
"publish_assignment_plan",
"rank_keyphrases",
"read_keyphrase_source",
"request_sha256",
Expand All @@ -144,6 +163,7 @@
"write_expertise_run",
"write_keyphrase_run",
"write_openreview_snapshot",
"write_publish_artifact",
]

__version__ = "0.12.0"
__version__ = "0.13.0"
Loading
Loading