Skip to content

Release 1.1.0-rc.3#20

Merged
gkostkowski merged 41 commits into
release/1.1.0from
develop
Jun 10, 2026
Merged

Release 1.1.0-rc.3#20
gkostkowski merged 41 commits into
release/1.1.0from
develop

Conversation

@gkostkowski

Copy link
Copy Markdown
Contributor

Changes

  • Review-status workflow: reviewed_since_placement metadata, cluster-size ordering, and review counter in decision summary responses
  • Materialised cluster-size index with atomic updates; filter by reviewed_since_placement
  • Idempotent curator actions; reject excludes current placement from candidates
  • Operational scripts: backfill_cluster_sizes, verify_cluster_sizes, backfill_previous_review_count
  • E2E and manual test coverage for review-status and cluster-size scenarios
  • Fixed: stale decision documents, aggregation pipeline ordering, TOCTOU race on concurrent actions, zero-count index entries
  • Removed Meaningfy contact references; practical setup notes added to docs

meanigfy and others added 30 commits June 1, 2026 14:59
Replace Meaningfy-specific attributions, emails, and example hosts with
project-neutral wording, GitHub-native contact channels, and placeholder
hosts. Drop stale copyright line from LICENSE. Update pyproject authors
to the Publications Office of the European Union.
Closes TEDSWS-524 and TEDSWS-522.

- Sort-by-cluster-size and cluster-size distribution stats via maintained
  cluster_sizes projection collection (ClusterSizeIndex port + Mongo adapter +
  integrator hooks in DecisionStoreService.store_decision + backfill / verify
  scripts).
- previous_review_count counter on every decision, atomically incremented on
  each user-action save and preserved across ERE re-integration; decision_id
  filter on /curation/user-actions for the full curator timeline.
- Optional ?reviewed=true|false filter on /curation/decisions via gated
  $lookup against user_actions on the curation path; bulk-sync path
  (query_decisions_paginated) unchanged.
- cluster_size field on CanonicalEntityPreview, populated from
  ClusterSizeIndex.get_size.
- RegistryStatistics gains cluster_size_average (renamed from
  average_cluster_size), cluster_size_median, cluster_size_p95,
  cluster_size_max, cluster_singletons_count.
- Idempotency fix in user_action_service._check_not_already_curated: removes
  the updated_at-gated guard so re-actions on fresh decisions are rejected
  consistently.

Indexes added: user_actions.about_entity_mention, cluster_sizes.size.
Task outcome files written by the implementer agents during the phased
TEDSWS-524 + TEDSWS-522 implementation. Filed under TEDSWS-528 by the
agents (branch-name aliasing); content covers phases 2 through 7.
Short-circuit storing an ERE outcome only when the full outcome (placement and
candidates) is unchanged, instead of keying on cluster_id alone. A re-assessment
that returns the same cluster with a changed confidence now writes through and
advances updated_at, so the decision re-surfaces for curator review and shows
the fresh confidence.
Add reviewed_since_placement (derived per request) alongside previous_review_count
on DecisionSummary, and filter the decisions list by ever_reviewed and
reviewed_since_placement. The UI composes the four curator states (not reviewed,
up to date, needs revisit, reviewed more than once) from the two primitives; the
old reviewed parameter is kept as a deprecated alias.

Apply the review filter before sort/limit so review-filtered pages no longer
under-fill and terminate pagination early.
Correct the user_actions join key to the about_entity_mention triad, describe the
cursor and filter-before-limit pipeline behaviour, and add an implementation delta
plan covering the material-outcome short-circuit, the four-state read surface, and
the pagination fix.
Use $in:[0,None] for the ever_reviewed=false filter (engine-portable, matches a
missing counter), dump the entity-mention triad once for both the query and the
result key, and reference field-path constants in the lookup. Add tests for the
never-reviewed counter branch, the combined ever_reviewed + reviewed_since_placement
path, and the never-reviewed row.
Specify the post-review follow-ups, the effect of ERE-outcome integration on the
decision/cluster-size/user-action stores, and TEDSWS-530 (curator reject/assign must
reach ERE). Reject-all now excludes the current placement plus candidates; delivery
stays best-effort with the outgoing payload logged for auditability.
…ines

Cross-check against entity-resolution-docs: add normative-basis anchors,
correct the ERE-integration write guards (separate stale-ordering guard from
material-change idempotency), confirm the reject-all exclusion-set bug via the
candidates[0]/[1:] split, and record the best-effort delivery deviation.
…ize delete-on-zero, review-state read-port)

C (TEDSWS-530): "reject all" now excludes the current placement AND all
candidates (deduplicated, order-preserving); the outgoing ERE payload summary
is logged at INFO for auditability; best-effort delivery is retained.

B: cluster_sizes shift deletes an entry once it reaches zero and never persists
a negative count (no-upsert decrement + delete-on-<=0), so stats never observe a
size:0 row.

A: the reviewed_since_placement read moved to a curation-owned ReviewStateReader
port/adapter so the decision-store no longer reads the user_actions collection
(A1); has_current_action aligned to strict $gt (A5); list_decisions issues its
independent reads concurrently (A4); CursorPage.count documented as an upper
bound when the review filter is active (A3).

Fixes two latent bugs surfaced by new FerretDB integration tests:
- missing await on the review-filter and cluster-size aggregations, and
- previous_review_count not stripped before Decision model validation.
- Consolidate derived-field stripping in MongoDecisionRepository._from_document
  (previous_review_count + cluster_size) so there is one strip funnel for all
  read paths; drop the inline cluster_size comprehension.
- Scope the ReviewStateReader docstring honestly: it moves the flag-attachment
  read to curation; the review-filter $lookup intentionally remains in the
  decision-store aggregation for keyset pagination.
- Replace stale action-vocabulary in accept/reject/assign docstrings with the
  contract field names.
- Tests: assert the curation action still succeeds when the ERE publish is
  swallowed; pin the empty-candidates reject (exclusion = [placement]); add
  integration coverage for find_by_id on a counter-bearing decision and a B5
  re-integration acceptance (counter preserved, updated_at advances,
  reviewed_since_placement flips false).
chore: remove Meaningfy attributions and contact references (TEDSWS-528)
…(TEDSWS-524-2)

Milestone 1 of TEDSWS-524-2. Replaces the derived `reviewed_since_placement`
boolean with a stored field on the decision row, joining the existing
`previous_review_count` counter. The cross-collection `$lookup user_actions`
and `ReviewStateReader` are removed from the curation list hot path; the
review predicate now lives in one place (the writer) instead of three.

Lifecycle:
  - Integrator resets `reviewed_since_placement=false` on insert and on every
    material placement advance, in the same atomic `$set` as `updated_at`.
  - `record_review(decision_id, action_created_at)` replaces
    `increment_review_count`. It uses an aggregation-update pipeline to
    increment the counter unconditionally and conditionally set the flag to
    true via `$cond` against the stored placement boundary, so delayed or
    out-of-order curator actions never regress the flag.

Reads:
  - `find_review_metadata` returns both primitives in one projection,
    replacing the separate counter fetch and per-page `ReviewStateReader`
    `$or`-of-N read.
  - `find_with_filters` applies `reviewed_since_placement` as a plain
    stored-field `$match`. `CursorPage.count` is exact for every filter
    combination (resolves A3).

Resolves from `.claude/memory/epics/TEDSWS-524-1-specs.md`:
  - A1 read side, A2, A3, A4, A5, B suite, S-1/S-2/S-3/S-5, F-1/F-2/F-3/F-4.
  - C1 (cluster-size cursor placement) deferred to milestone 2.

Includes:
  - new compound index `decisions_reviewed_since_placement_id`
  - `seed_db.py` aligned to mirror the production lifecycle (no separate
    backfill script)
  - FerretDB aggregation-update pipeline smoke test
  - lifecycle + paginated-across-pages integration tests (the regression
    net previously missing)
  - BDD scenarios for the four UI review states
  - OpenAPI schema regenerated
…elds (TEDSWS-524-2)

Milestone 2 of TEDSWS-524-2 (C1 from .claude/memory/epics/TEDSWS-524-1-specs.md).

The keyset cursor predicate on the derived `cluster_size` field was
previously merged into the stage-1 `$match` of the aggregation pipeline,
where the field does not yet exist. Page 2+ matched nothing and pagination
terminated prematurely; the existing integration test only exercised page 1
so CI never caught it.

Fix: extract `_apply_cursor_condition` to decide cursor placement, and
forward the cursor predicate to `_fetch_with_cluster_size_sort` as a
separate kwarg. The aggregation now inserts the cursor `$match` between
`$project` (which removes `_cluster_meta`) and `$sort`, where
`cluster_size` is materialised. Stored-field filters (entity_type,
confidence, ever_reviewed, reviewed_since_placement, mention_identifiers)
remain in stage 1 — still indexable.

`cluster_size` stays a derived projection; no denormalisation onto the
decision row. Trade-off: cluster-size ordering still costs one `$lookup`
on PK per page, accepted at the 3k/day volume.

Resolves: C1 and (by extension) C2 from TEDSWS-524-1-specs.md.

Coverage:
  - unit: cursor-not-in-stage-1 when cursor=None; cursor-after-\$addFields
    when cursor present; stored-field filters stay in stage 1.
  - integration: cluster_size ASC and DESC paginated-across-pages tests
    (the regression net previously missing) + non-under-fill invariant.
The previous _check_not_already_curated guard issued a count_documents
read followed by a separate save, leaving a TOCTOU window in which two
concurrent submissions could both pass and both record. Empirically
reproduced on 2026-06-07: two ACCEPT_TOP rows 1 ms apart, counter 2,
duplicate ERE publishes.

Fix:
  - record_review keeps its name but now returns bool and gates the
    update_one filter on reviewed_since_placement != True. First concurrent
    caller wins (modified_count == 1 -> True); others see modified_count == 0
    -> False. Atomic at the single-document level.
  - user_action_service runs save -> record_review (claim) -> compensate.
    On False, the just-saved user_action is deleted via delete_by_id and
    AlreadyCuratedError is raised (HTTP 409 unchanged).
  - _check_not_already_curated removed; replaced by _record_or_compensate.
  - has_current_action kept as a repository read-only utility with an
    updated docstring (no longer used for write-path idempotency).

Boundary discipline:
  - record_review signals race-lost via bool, not via a curation exception.
    No new cross-module import from resolution_decision_store to curation.
  - delete_by_id added on UserActionCurationRepository only; the generic
    AsyncWriteRepository base stays save-only.
  - make check-architecture: 10/10 contracts kept.

Known follow-up (deferred): if delete_by_id itself fails on the
compensation path, the original exception propagates as HTTP 500 instead
of AlreadyCuratedError. Rare-on-rare; will be addressed in a follow-up.

Coverage:
  - unit: filter shape, True/False return on modified_count, save -> claim
    order, compensation called when claim fails, no compensation when it
    succeeds (across all three record_* methods).
  - integration (FerretDB): 8-way concurrent record_review yields exactly
    one winner; counter ends at 1, flag at True. Plus sequential second-
    claim returns False, missing-decision returns False.
Add unit test suite for the three operational scripts in src/scripts/:
backfill_cluster_sizes, backfill_previous_review_count, and verify_cluster_sizes.
Includes one intentionally failing TDD-driver test (test_run_backfill_deletes_stale_entries)
that will pass once Task 4 adds stale-entry deletion to backfill_cluster_sizes.
…y repair message

Add _delete_stale_entries to backfill_cluster_sizes.py, called at the end of
_run_backfill (both live and dry-run paths) after upserts complete. Uses
{"_id": {"$nin": sorted(live_cluster_ids)}} to delete cluster_sizes documents
whose cluster no longer exists in decisions. The existing early return for empty
decisions is preserved so no deletion occurs when there are no clusters at all.
Update verify_cluster_sizes.py repair hint to reflect that backfill now also
removes stale entries. Remove xfail marker from test_run_backfill_deletes_stale_entries
and correct its filter assertion to match MongoDB $nin semantics.
…-fixes

fix(scripts): operational script tests, docs, stale-entry repair, batch-size clarification
TEDSWS-524-1: reject exclusions (TEDSWS-530), cluster-size delete-on-zero, review-state read-port
…nt and cluster_sizes

Add BDD feature and step definitions covering the full reviewed_since_placement
lifecycle (fresh placement, curator action, ERE re-placement). Fix scenario isolation
by including cluster_sizes in the MongoDB cleanup list. Align injected decision
documents with the two new materialised fields. Correct the bulk-reevaluation
decision fixture to exclude current_placement from candidates per domain invariant.
…ince-placement

fix(scripts): backfill also sets reviewed_since_placement
Aligns reject_decision with the intended invariant: when a curator
rejects a decision, ERE should re-evaluate excluding both the current
placement and all candidate clusters. Updates the unit and e2e test
assertions accordingly.

Resolves merge conflict in test_user_reevaluation.py (all_refs fixture
variable name), restoring the corrected _make_decision helper.
test(e2e): extend e2e contract tests for curation PRs 120/121/122
… scenarios

Extends resolution_and_curation.http with two new full-cycle scenarios:
S6 covers the accept curation action with confirming ERE re-placement; S7
covers the reviewed_since_placement filter lifecycle (set on action, reset
on re-placement). All new cases marked as passing in the testing report.

Also rewords the notification subscriber worker debug log to accurately
describe both the cross-instance case and unsolicited re-evaluation outcomes.
test(manual): add S6 accept and S7 reviewed_since_placement lifecycle scenarios
- Add --no-cache rebuild tips (make rebuild-clean / make infra-rebuild-clean)
  in the ERS Getting Started section and INSTALL.md steps for ERS and ERE
- Document that entity schema changes require clearing the database volume
  (make down-volumes) to avoid schema mismatch errors on restart
- Fix API_BACKEND_URL default: curation-api:8000 (no http:// prefix);
  Nginx prepends the protocol internally — the http:// prefix caused API errors
- Add host-machine override note for REDIS_HOST in src/infra/.env.example:
  export REDIS_HOST=localhost before running black-box test targets
- Update CHANGELOG.md version tag to 1.1.0-rc.2
docs: practical notes on setup and configuration
@gkostkowski gkostkowski merged commit 0c2f31a into release/1.1.0 Jun 10, 2026
2 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.

2 participants