Wikidata entity read path: platform wikibase module + patrol MVP (ADR-0016 / PRD-0011)#119
Wikidata entity read path: platform wikibase module + patrol MVP (ADR-0016 / PRD-0011)#119tieguy wants to merge 22 commits into
Conversation
…ContentDiff (ADR-0016) The shared Wikidata read model PRD-0011/ADR-0016 specify, promoted-by-design from sp42-mcp's verify_wikidata_statement (PR #103, its first consumer): - Typed entity model (WikibaseEntity/Statement/Snak/Value/Reference, ranks) keeping each statement's canonical raw JSON so change detection is exact for datatypes the typed model does not cover; references keep the full snak set, with P854 as one supported property (Reference::urls()). - Endpoint-agnostic parse_entity accepting Special:EntityData documents (with single-entity redirect tolerance) and bare entity objects (the prop=revisions slot shape), tolerant of sparse fixture snaks. - Pure request builders over the injected HttpClient boundary: build_entity_request (host derived from the wiki's api_url, entity id validated before interpolation, optional revision pin) and build_label_request/parse_labels (wbgetentities). - render_snak_value/render_statement_claim reproducing #103's best-effort claim rendering semantics. - EntityDiff at full statement depth (PRD-0011 Q4) with the honesty invariant: qualifier-only/rank-only/reference-only edits classify via StatementChangeParts, unmodeled statement deltas flag parts.other, and unmodeled top-level entity fields diff as UnknownEntityChange. Missing parent yields an all-added diff. - ContentDiff sum (Text|Entity) plus per-revision content-model classification and the content-model capability axis (wikitext signals and revertrisk scoring gate off for entity content; unknown/other models keep pre-ADR-0016 behavior). 15 fixture-replayed unit tests, no network. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
…ision 2) verify_wikidata_statement keeps its exact public contract and tests — same endpoint, same claim rendering, same P854 abstention — but its entity/ statement parsing, label lookup, and claim rendering now come from sp42_platform::wikibase instead of ad-hoc serde_json::Value walking. The verb was the shared model's first consumer (PR #103) and is now its first caller; the patrol entity diff and the ADR-0017 write lane consume the same parser, so the Wikidata read model cannot drift into two implementations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
…on 1) Option<String> with serde(default, skip_serializing_if): old streams and snapshots deserialize to None and None serializes to nothing (ADR-0009 replay discipline). None means unknown — ingestion sources (recentchanges, EventStreams) do not carry a content model, so the field is populated by the revision-content read; all content-model routing keys on this value, never the wiki id. Updates every construction site across live/patrol/platform/ reporting/app plus test fixtures, and notes the read-model milestone in STATUS.md. Verified: workspace clippy pedantic clean, 864 tests green, layer check passes, sp42-app wasm32 check passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
…ADR-0016 D2/D4) Completes the platform half of the entity read path: - parse_revision_contents: pure parser for the Action API prop=revisions&rvslots=main&rvprop=ids|content|contentmodel response — the one-call diff fetch ADR-0016 Decision 2 names — yielding per-revision main-slot content plus the content model Decision 1 routes on. Content-less (deleted/suppressed) revisions are omitted, matching the existing text-pair fetch. - parse_entity_content: bare slot-content parse where the body's own id is authoritative. - route_content_diff: the Decision 4 routing helper — wikibase-item/-property content parses and diffs structurally into ContentDiff::Entity; wikitext, unknown/other models, and entity content that fails to parse degrade honestly to the existing text diff (including a present-but-unparseable `before`, which must not fabricate an all-added entity diff). The server diff route can now adopt ContentDiff with a thin delta: add contentmodel to its rvprop, call route_content_diff. That wiring (plus the EntityDiff renderer and browser surface) is the next slice per STATUS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
…on (ADR-0016 D4/D5)
The operator diff surface gains a content-model-routed sibling route,
/operator/content-diff/{wiki_id}/{rev_id}/{old_rev_id}, returning a
ContentDiffReport (new platform wire type: the routed ContentDiff plus
best-effort resolved labels so renderers print "educated at" rather than
raw P-ids). The existing /operator/diff route is byte-for-byte unchanged.
- The revision fetch now uses the platform parse_revision_contents (deduping
the inline query.pages parsing) and carries rvprop=contentmodel — the
one-call diff fetch ADR-0016 Decision 2 names.
- revision_artifacts_for_pair routes on the after-revision's content model:
entity pairs additionally carry ContentDiff::Entity with labels resolved in
one batched wbgetentities call (best-effort; failures render raw ids), and
media-reference extraction — a wikitext-only signal — is not invoked for
entity content (Decision 5).
- New platform helpers: collect_label_ids (the ids an entity diff renders)
and WikibaseLabels::into_map; new OPERATOR_CONTENT_DIFF_PATTERN route
constant + operator_content_diff_path builder.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
…path) The browser diff pane now routes on content model, client edge of ADR-0016 Decision 4. The selection/prefetch/invalidate pipeline fetches the new /operator/content-diff route and dispatches: Text reports feed the existing StructuredDiff viewer and caches unchanged; Entity reports render through a new EntityDiffViewer — classified change rows for labels, descriptions, aliases, sitelinks, statements (added/removed/changed with the changed-parts summary: value/qualifiers/rank/references/other), and unmodeled entity fields — using the server-resolved labels so statements read "educated at: University of X" rather than raw P/Q ids, falling back to raw ids when resolution failed. Picking wikidatawiki in the existing picker now yields a readable entity diff end to end; wikitext review is untouched. STATUS.md updated with what remains for the PRD-0011 MVP (queue defaults, scoring gate at queue level, testwikidatawiki action gate). Verified: workspace clippy pedantic clean, 866 tests green, layer check passes, sp42-app wasm32 check passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
…erage Completes the MVP checklist items STATUS.md tracked: - Ingestion seeds EditEvent.content_model from the site's per-namespace default content models (new platform default_namespace_content_model: wikidata.org family ns 0 -> wikibase-item, ns 120 -> wikibase-property; everything else None), in both the EventStreams ingestor and the recentchanges parser. Talk pages stay wikitext, so routing still keys on the per-revision value this seeds, never the wiki id. - Entity events score a uniform base with no signal contributions (scoring_engine gate per ADR-0016 Decisions 5/7): wikitext heuristics do not misread entity JSON, and with the priority queue's FIFO tie-break the Wikidata queue orders chronologically over the bot-excluded-by-default stream (PRD-0011 Q3 — include_bots already defaults to Disabled and emits rcshow=!bot; now covered by a wikidatawiki fixture test). - No LiftWing revertrisk request is built for entity content (load_selected_review_state gate, ADR-0016 Decision 7). - Mock write-path coverage for reviewer actions on testwikidatawiki: patrol/rollback build against the change's own wiki (test.wikidata.org) through the generic Action API and execute over the stub client. The live acceptance gate on test.wikidata.org remains the manual dogfood step recorded in the closing PR. Tests: wikidatawiki recentchanges fixture (bot exclusion + content-model seeding), namespace-default helper, entity uniform-base scoring, and the testwikidatawiki action path. Workspace: clippy pedantic clean, 870 tests green, layer check passes, wasm check passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
The desktop shell keeps its own workspace lockfile and CI builds it with --locked; adding url as a direct dependency of sp42-live left that lock stale (url was already pinned transitively, so this is a one-line dependency-edge update). Verified with cargo metadata --locked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8aa2a003e7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| artifacts.set_current_entity_diff.set(None); | ||
| return current_rev; |
There was a problem hiding this comment.
Fetch entity diff before accepting cached text diff
When the initial live-operator payload already includes a selected Wikidata edit, cache_initial_artifacts stores the server-provided raw text diff in diff_cache but the view has no ContentDiffReport. This cache-hit branch then clears current_entity_diff and returns before the new content-diff endpoint is fetched, so the initially selected entity revision renders raw JSON instead of the entity diff viewer until the cache is invalidated. Skip the text cache for entity content or check/fetch the content diff before returning here.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — the initial payload's server-computed text diff shadowed the entity view for the initially selected edit. Fixed in 786a0ca: cache_initial_artifacts now skips seeding entirely when the selected edit's content model classifies as entity (derive_content_model_capabilities(...).entity_diff), so the selection effect cache-misses and fetches the content diff. The event carries content_model from ingestion (namespace-default seeding), which is what makes this check possible client-side without a fetch.
Generated by Claude Code
There was a problem hiding this comment.
(Threading note: the follow-up round's fixes are in 49a0cf3 — see the two new thread replies.)
Generated by Claude Code
There was a problem hiding this comment.
Codex's third-round comments (changed-subpart details, statement reorders) are both valid and are fixed in the stacked follow-up #120, where the row rendering now lives (it moved from this component into the platform's shared render_entity_diff_report as part of the slim-report refactor). Fixing them here would have churned code #120 deletes.
Generated by Claude Code
| let Some(before) = revisions.remove(&old_rev_id) else { | ||
| return Ok(None); | ||
| }; |
There was a problem hiding this comment.
Preserve first-revision entity diffs
For a newly created Wikidata entity, old_rev_id is absent (or passed as 0 by the client), so the lookup contains only the new revision and this branch returns None. That bypasses route_content_diff's before: None path that produces an all-added entity diff, leaving new items/properties with no structured diff at all. Treat a missing/zero old revision as before = None and route the fetched after content instead of dropping the artifact.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — new entities never reached route_content_diff's before: None path. Fixed in 786a0ca: old_rev_id == 0 now fetches only the new revision and, for entity content, routes as a first revision (all-added EntityDiff, per ADR-0016 Decision 3). Two deliberate boundaries: a first wikitext revision keeps today's no-artifacts behavior (the wikitext path stays byte-for-byte untouched), and a nonzero-but-missing parent (deleted/suppressed revision) still yields None rather than being passed off as a first revision — an all-added diff there would be dishonest.
Generated by Claude Code
…evisions) Two correct P2 catches: - App: cache_initial_artifacts seeded the server's text diff for the initially selected edit, so an entity revision hit the text cache and rendered raw JSON until invalidation. Entity-content selections now skip the seed entirely and the selection effect cache-misses into the content-diff fetch. - Server: a first revision (old_rev_id == 0) returned None before route_content_diff's before=None all-added path could run, so newly created items/properties got no structured diff. old_rev_id == 0 with entity content now routes as a first revision (all-added EntityDiff); first wikitext revisions keep today's no-artifacts behavior, and a nonzero-but-missing parent (deleted/suppressed) still yields None rather than being passed off as a first revision. Also hardens the desktop CI job's apt step: drop the runner-preinstalled packages.microsoft.com / Chrome repos before apt-get update — their signing hiccups failed the whole job (this run's actual desktop failure) and none of them are needed for the GTK deps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 786a0cae16
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| let liftwing_risk = if let Some(item) = selected | ||
| && sp42_core::derive_content_model_capabilities(item.event.content_model.as_deref()) | ||
| .revertrisk_scoring |
There was a problem hiding this comment.
Gate selected diff scoring hints for entity content
When the selected item is wikibase-item/wikibase-property, this guard skips LiftWing, but the same function still fetches the raw JSON StructuredDiff and runs analyze_diff_for_scoring below, populating link/reference/category/profanity flags in scoring_context for entity revisions. That exposes wikitext-only signal state for Wikidata entity reviews even though entity scoring is supposed to have no wikitext signal contributions; skip the diff-hint path whenever derive_content_model_capabilities(...).wikitext_signals is false.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — the LiftWing guard didn't cover the hint pass, so entity revisions still got wikitext signal state (links/references/profanity/blanking) populated into the scoring context from their raw-JSON text diff. Fixed in 49a0cf3: the analyze_diff_for_scoring block is now gated on derive_content_model_capabilities(...).wikitext_signals for the selected item, so entity content gets neither LiftWing nor wikitext hint state — matching the scoring engine's uniform-base behavior (ADR-0016 Decision 5).
Generated by Claude Code
| let media_diff = if content_diff.is_some() { | ||
| None | ||
| } else { | ||
| let mut report = build_media_diff(&before_content, &after.content); |
There was a problem hiding this comment.
Gate media extraction on content model, not parse success
If a revision declares an entity content model but route_content_diff falls back to Text because the entity JSON is malformed or no longer matches the parser, content_diff is None and this branch invokes build_media_diff on the entity JSON. Media-reference extraction is a wikitext-only path, so the gate should key off after.content_model/capabilities rather than whether the structured entity diff parsed successfully.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — gating on content_diff.is_some() conflated "is entity content" with "entity parse succeeded", so malformed entity JSON fell through to wikitext media extraction. Fixed in 49a0cf3: the media gate now keys on derive_content_model_capabilities(after.content_model.as_deref()).wikitext_signals — the content model itself — so declared-entity revisions never run the wikitext-only path regardless of parse outcome.
Generated by Claude Code
…119) CI gates the previous head tripped: - Design system: the EntityDiffViewer rendered raw DOM tags in sp42-app. The markup moves into sp42-ui primitives (EntityDiffPanel, EntityDiffSection, EntityChangeRow, with structural styles in sp42-ui/static/style.css); the app component now only composes primitives, and the empty/not-entity states reuse the existing DiffState primitive. Also de-hexifies three "PR #103" comments in wikibase.rs that the color-literal scan reads as #RGB. - Wasm size: the entity-diff surface (wire types + viewer) lands the optimized bundle at ~860 KiB gzip, past the 832 KiB ceiling. Recorded decision in scripts/check-wasm-size.sh: gzip ceiling to 896 KiB with the ratchet-down note, mirroring the citation-surface precedent. Second Codex review round, both correct: - live_queue: analyze_diff_for_scoring populated wikitext signal state (links/references/profanity/blanking) into the scoring context for entity revisions; the hint pass is now gated on the content model's wikitext_signals capability. - revision_artifacts: the media-diff gate keyed on entity parse success, so malformed entity JSON fell through to wikitext media extraction; it now keys on the content model itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
…link The forbidden-patterns gate (Constitution 5.3) requires every #[allow(...)] added in a change to carry an approved issue/PR link on the same line, per the sp42-mcp precedent. StatementChangeParts keeps its five independent change flags with the justification inline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
rustfmt wrapped the previous long trailing comment onto its own line, which defeats the line-based forbidden-patterns gate; a short same-line "PR #119" marker survives formatting, with the full justification in the comment above. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
"PR #119" is itself three hex digits, so the forbidden-patterns-mandated issue reference tripped the design-system color-literal scan. The allow line now carries the PR URL (accepted by the issue-link rule, invisible to the color regex, short enough to survive rustfmt) and the prose comment says "PR 119". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 91e0004e3c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if let Some(index) = paired { | ||
| consumed_after[index] = true; | ||
| let new = &after[index]; | ||
| if old.raw != new.raw { |
There was a problem hiding this comment.
Track statement reorders in entity diffs
When a Wikibase edit only reorders statements within the same property, the paired statements still have the same GUIDs and identical raw JSON, so this branch consumes them without pushing any StatementChange. The resulting EntityDiff can be empty and the UI will report no entity changes for a real revision whose display/order changed; compare the matched index with the original position or carry an explicit order-change part before continuing.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Closing the loop now that #120 has been folded into this branch: this is fixed in-branch. diff_statement_lists tracks matched positions and emits StatementChange::Reordered { property } when pairing consumed statements out of order (matched_positions.windows(2).any(|p| p[0] > p[1])), so a reorder-only edit is no longer a no-op; the report renders it as a dedicated row. Covered by a reorder-only regression test.
Generated by Claude Code
| format!( | ||
| "{}: {} \u{2192} {} ({parts} changed)", | ||
| label_for(labels, &after.property), | ||
| statement_value_text(labels, before), | ||
| statement_value_text(labels, after), |
There was a problem hiding this comment.
Render changed statement subpart details
For edits where only a statement's qualifiers, rank, or references change, the before/after main value is usually identical, so this formatter renders only something like P123: X → X (references changed) and hides the actual reference URL, qualifier, or rank delta already present in before/after. In those cases reviewers cannot tell what changed from the entity diff view; include the changed subpart values when the corresponding parts flag is set.
Useful? React with 👍 / 👎.
…ort)
The content-diff route now serves a render-oriented report instead of the
full wikibase model, per the ADR-0011 "the report is the contract"
discipline: ContentDiffReport becomes Text{StructuredDiff} |
Entity{EntityDiffReport}, where EntityDiffReport is flat pre-rendered
sections/rows produced by the new pure, host-tested platform renderer
(render_entity_diff_report) with labels substituted server-side. The
full-depth EntityDiff stays server/domain-side where the honesty invariant
needs it.
Effects: the browser deserializes three flat types instead of the deep
statement/snak/raw-Value tree, EntityDiffViewer collapses to a thin mapping
onto sp42-ui primitives, and every shell shares one renderer. Measured on
the raw web-release wasm: -136 KiB raw / -28.9 KiB gzip — recovering the
PR #119 bundle regression — so the gzip ceiling ratchets back down from
896 KiB to 860 KiB (recorded in check-wasm-size.sh).
Also addresses the third Codex round on PR #119, both in the renderer/differ
where the logic now lives:
- Changed statement rows include the actual subpart deltas (qualifiers /
rank / references before -> after), not just "(references changed)".
- A pure statement reorder within a property (same GUIDs, same raw) now
emits an explicit StatementChange::Reordered instead of rendering as no
changes — closing an honesty-invariant gap.
Tests: reorder-never-a-no-op, renderer subpart details + label fallback +
empty report, wire round-trip of both report arms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
Slim entity-diff wire report: pre-rendered rows, −29 KiB gzip, ceiling ratcheted back down
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ff085a9389
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if let Some(title) = link.get("title").and_then(Value::as_str) { | ||
| map.insert(site.clone(), title.to_owned()); |
There was a problem hiding this comment.
Preserve sitelink badge-only changes
When an edit only changes a Wikibase sitelink badge while the linked title stays the same, this parser keeps only the title. Since sitelinks is also treated as a modeled top-level key instead of being preserved in extra, diff_entities compares identical site-to-title maps and emits neither a SitelinkChange nor an UnknownEntityChange, so the entity diff can report no changes for a real entity edit. Preserve/diff the badge payload, or keep the raw sitelink data for unknown-change detection.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — sitelinks were parsed down to bare titles, so a badge-only edit compared equal and produced an empty diff, violating the module's own never-a-no-op invariant. Fixed in 0f8596e: sitelinks now parse as WikibaseSitelink { title, badges }, struct equality drives the diff (badge-only edits emit a SitelinkChange), and change rows carry the display form ("Douglas Adams [Q17437796]") so the wire shape of SitelinkChange is unchanged. Covered by a badge-only regression test asserting has_changes().
Generated by Claude Code
| WikibaseValue::Quantity { amount, .. } => ValueDisplay { | ||
| text: amount.trim_start_matches('+').to_owned(), |
There was a problem hiding this comment.
Include quantity units in rendered values
For a statement whose quantity unit changes but the numeric amount stays the same, statement_change_parts correctly marks the main value as changed, but this renderer drops the unit field and both sides display as the same amount. The entity diff then renders a row like P…: 5 → 5, hiding the actual unit change from reviewers; include the unit (or resolved unit entity) in the value text.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — the renderer dropped unit, so a unit-only change displayed as 5 → 5 even though the change was detected. Fixed in 0f8596e: quantity rendering now appends the unit entity id ("5 Q11573", extracted from the unit URI tail), and the dimensionless unit stays a bare amount. Resolving the unit id to its label is left as the "richer datatype rendering" follow-on the module already notes — the id makes the change visible; the label would only make it prettier.
Generated by Claude Code
…d 4)
Two honesty-invariant fixes in the wikibase read model:
- Sitelinks now parse as title + badge item ids instead of title alone,
so a badge-only edit (title unchanged) diffs as a SitelinkChange
instead of an empty EntityDiff. Change rows carry the display form
("Douglas Adams [Q17437796]"), keeping SitelinkChange's wire shape.
- Quantity snak rendering includes the unit entity id ("5 Q11573"), so
a unit-only change no longer renders as `5 -> 5`. The dimensionless
unit stays a bare amount; unit label resolution remains the follow-on
richer datatype rendering the module already notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f8596ec16
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
| match namespace { | ||
| 0 => Some(WIKIBASE_ITEM_CONTENT_MODEL), | ||
| 120 => Some(WIKIBASE_PROPERTY_CONTENT_MODEL), |
There was a problem hiding this comment.
Include property namespace in Wikidata patrol defaults
This adds wikibase-property routing for namespace 120, but a derived wikidatawiki config still uses the shared default namespace allowlist [0,2,4,6,10,14], and the patrol filter UI does not offer namespace 120 either. In the Wikidata property-review case, unfiltered queues never request property edits and reviewers cannot select that namespace from the UI, so this new property diff path is effectively unreachable except via a hand-crafted query string; add namespace 120 to the Wikidata defaults/filter options along with this classifier.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — the routing existed but nothing fed it: derived wikidatawiki queues never requested namespace 120 and the filter offered no way to add it. Fixed in 25973fa: derive_wiki_config appends 120 to the allowlist for sites with an entity Property namespace (keyed on the same default_namespace_content_model(api_url, 120) check the routing uses, so the two can't drift), and the filter bar gains a Property checkbox. The checkbox appears in the static option list for all wikis (matching how the list already works), but it is default-selected only where the wiki's server-resolved defaults include 120 — enwiki reviewers see it unchecked. Regression test asserts wikidatawiki's derived allowlist contains 120 and enwiki's does not.
Generated by Claude Code
|
|
||
| QueuedEdit { | ||
| event: EditEvent { | ||
| content_model: None, |
There was a problem hiding this comment.
Seed content model for browser EventStreams inserts
When a Wikidata item/property edit arrives through the browser EventStreams path after the initial load, this queues it with content_model: None while the score above has already been computed with wikitext-style byte/identity signals. The server recentchanges and stream ingestor paths now seed Wikidata namespace defaults, so only these live browser inserts are treated as unknown/wikitext instead of entity content, which can mis-rank them in the queue; seed the same content model (and score through the entity gate) before inserting the event.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — the browser's live insert was the one ingestion path left unseeded, so a post-load Wikidata edit queued as unknown/wikitext with byte/identity signals while its initial-load neighbors scored as entities. Fixed in 25973fa: stream_event_to_queued_edit now seeds the content model via a new dbname-keyed platform helper (default_namespace_content_model_for_wiki, sharing the same namespace→model core as the api-url-keyed form — browser surfaces hold the wiki_id, not the api url), and entity content mirrors the scoring engine's gate: no wikitext identity/byte contributions, so live entity inserts stay chronological (PRD-0011 Q3). Tests cover the wikidata ns-0 (seeded, zero contributions), wikidata talk-page (wikitext path kept), and enwiki (signals kept) cases.
Generated by Claude Code
…odex round 5) - Derived wikidata-family configs now include the Property namespace (120) in their patrol allowlist — keyed on the same family check the content-model routing uses — and the filter bar offers a Property checkbox (default-selected only where the wiki's resolved defaults include it). Without this, wikibase-property routing was unreachable except by hand-crafted query string. - The browser EventStreams insert path now seeds the per-namespace default content model like the server ingestion paths, via a new dbname-keyed platform helper (browser surfaces hold the wiki_id, not its api url), and mirrors the scoring engine's entity gate: entity inserts carry no wikitext identity/byte signal contributions, keeping live Wikidata entries chronological instead of mis-ranked against the initial load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 25973fa295
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| .and_then(Value::as_str) | ||
| .filter(|unit| *unit != "1") | ||
| .map(str::to_owned); | ||
| WikibaseValue::Quantity { |
There was a problem hiding this comment.
Preserve quantity bounds in entity value diffs
When a Wikibase quantity changes only its uncertainty bounds (lowerBound/upperBound) while the amount and unit stay fixed, the raw mainsnak makes statement_change_parts emit a value change, but this parser keeps only amount/unit and the row renderer has no bound data to print. Reviewers then see an entity diff like P…: 5 → 5 with no indication of the actual bound edit; keep/render the bounds or add an explicit value-change detail for omitted quantity fields.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and worth fixing at the family level rather than field by field. Fixed in 054c489 twice over: (1) quantity values now keep and render their bounds ("5 [4.8..5.2] Q11573"), so a bounds-only edit is visible directly; (2) a general honesty backstop in the changed-row details — when the raw main value moved (parts.value) but the best-effort renderer produces identical text on both sides, the row is annotated value details beyond the rendered form changed instead of silently presenting X → X. That covers today's remaining case (a calendar-model-only time edit, which is the regression test) and any future datatype detail the renderer doesn't model yet, so this class of finding can't recur silently.
Generated by Claude Code
- Quantity values keep their uncertainty bounds (lowerBound/upperBound)
and render them ("5 [4.8..5.2]"), so a bounds-only edit no longer
displays as `5 -> 5`.
- Honesty backstop for the whole datatype family: when a statement's raw
main value moved but the best-effort renderer shows identical text on
both sides (e.g. a calendar-model-only time edit today, or any future
unmodeled detail), the changed row is annotated "value details beyond
the rendered form changed" instead of silently presenting `X -> X`.
Covered by a calendar-model regression test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
Brings in the citation repair/insertion browser surface (#109), the architecture map (#118), docs attic move, and PRD-0012/0014/0015. Conflict resolved in scripts/check-wasm-size.sh: both sides recorded a gzip-ceiling decision (main's PRD-0014 action-row surface at 848 KiB, this branch's entity-diff surface at 860 KiB). The merged bundle carries both surfaces, so the ceiling keeps the larger 860 KiB with both decisions recorded in the comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
The merge brought main's new is_bare_url_ref field on BlockRef; one test helper in sp42-mcp built the struct literally and needed the field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
The merge combined two independently recorded gzip increases: main's PRD-0014 action-row surface (848 KiB ceiling) and this branch's entity-diff surface (860 KiB ceiling). Each ceiling was measured without the other surface present; the merged bundle carries both and CI measures 866.2 KiB gzip. Recorded decision (Art. 5.2): ceiling moves to their sum plus small headroom, 872 KiB. Raw is unchanged and passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f2a003b887
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| StatementChange::Added { statement } => EntityChangeRowReport { | ||
| kind: EntityChangeKind::Added, | ||
| text: statement_row_text(labels, statement), |
There was a problem hiding this comment.
Include subpart details for added/removed statements
When a Wikibase edit adds or removes a statement that carries qualifiers, a non-normal rank, or references, this branch renders only statement_row_text, which contains the property and main value. In that scenario the entity diff hides the added/removed reference URL, qualifier values, and rank entirely, so reviewers cannot inspect the full statement being introduced or deleted from the patrol surface; include the same subpart rendering used for changed statements for added/removed rows as well.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — an added/removed statement's qualifiers, rank, and references were invisible; the row showed only the property and main value. Fixed in 23f8f26: added/removed rows now share the changed-row subpart rendering (P800: v (qualifiers [P580: 1979]; rank preferred; references [P854: https://…])), with each part included only when informative — qualifiers/references when present, rank when non-normal — so bare statements keep the plain row. Regression test covers both directions plus the bare case.
Generated by Claude Code
An introduced or deleted statement can carry qualifiers, a non-normal rank, and references; the row previously showed only the property and main value, hiding all of that from reviewers. Added/removed rows now share the changed-row subpart rendering (qualifiers when present, rank when non-normal, references when present); bare statements keep the plain row. Regression test covers both directions plus the bare case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
Summary
Implements ADR-0016 (the Wikidata entity read mechanism) end to end and the PRD-0011
patrol-read MVP: picking
wikidatawikiin the existing picker now yields areadable, classified entity diff instead of a raw JSON line-diff, with the
queue/scoring gates in place. Wikitext review is byte-for-byte untouched throughout —
routing keys on the revision's content model, never the wiki id.
Seven commits, layered platform → shells:
sp42-platform::wikibase— the shared read model: typed entity/statement/snakvalues (each statement keeps its canonical
rawJSON so change detection is exactfor unmodeled datatypes; references keep the full snak set with P854 as one
property), endpoint-agnostic
parse_entity(Special:EntityData docs with redirecttolerance, or bare
prop=revisionsslot content), pure request builders over theHttpClientboundary, claim rendering, full-depthEntityDiffwith thenever-a-no-op honesty invariant (qualifier-only/rank-only/reference-only edits
classify; unmodeled deltas surface as
parts.other/UnknownEntityChange),ContentDiffrouting, and the content-model capability axis.sp42-mcprefactor —verify_wikidata_statementconsumes the platform model(ADR-0016 Decision 2 promotion); its public contract and all three tests are
unchanged.
EditEvent.content_model— additiveOption<String>(serde default/skip;old snapshots deserialize unchanged).
None= honestly unknown.route_content_diff— the one-callprop=revisions…contentmodeldiff fetch and the Decision 4 routing helper withhonest degradation (unparseable entity → text; a broken
beforenever fabricatesan all-added diff).
/operator/content-diff/{wiki}/{rev}/{old}route returningContentDiffReport(routed diff + labels resolved server-side in one batchedwbgetentitiescall, best-effort). Media-reference extraction is not invoked forentity content.
/operator/diffunchanged.viewer/caches untouched; entity → new
EntityDiffViewer("educated at: Universityof X", added/removed/changed rows with changed-parts summaries).
content_modelfrom per-namespace site defaults(wikidata.org ns 0/120 → item/property; talk pages stay wikitext); entity events
score a uniform base with zero signal contributions (chronological queue over the
bot-excluded-by-default stream, PRD-0011 Q3); no LiftWing request is built for
entity content; testwikidatawiki patrol/rollback covered by a mock write-path test.
Type
Validation Required
The one manual gate left from the PRD-0011 DoD is the live action acceptance on
test.wikidata.org (one real patrol/rollback, rev id recorded here) — that is the
dogfood step this PR enables.
Risk
Scoring behavior changes only for entity-content events (uniform base score, no
heuristic contributions, no LiftWing) — wikitext scoring is untouched, locked by a
test asserting the same event scores signals as wikitext and base-only as an entity.
Action behavior is unchanged (the new test only proves the existing generic path
works against test.wikidata.org). Governed by ADR-0016 (Proposed, in-tree).
Notes For Reviewers
EditEvent.content_modelisOption<String>withNone= unknown, rather than aserde default of
"wikitext". A literal wikitext default would mislabel Wikidataevents from ingestion sources that don't carry a content model;
Optionkeeps oldsnapshots deserializing (the ADR's actual intent) while staying honest. Capability
derivation treats
Noneas pre-ADR behavior (wikitext signals on).default_namespace_content_model) mirrorsMediaWiki's
$wgNamespaceContentModels: it is a property of the site's namespaceconfiguration that seeds the per-revision value routing keys on — a Wikidata
talk-page edit stays on the wikitext path. Scoped to the wikidata.org family;
other Wikibase hosts are additive.
sp42-mcp/src/wikidata.rsnow consumes theplatform parser per the merged design plan
(
docs/design-plans/2026-07-01-wikidata-read-model.md); the two Wikidata readimplementations are one.
diff_entities(GUID-first, raw-equality fallback, id-less edits show as removed+added), the
honest-degradation branches in
route_content_diff, and the label-resolutionerror path in the server (failures render raw ids, never fail the diff).
🤖 Generated with Claude Code
Generated by Claude Code