Skip to content

Wikidata entity read path: platform wikibase module + patrol MVP (ADR-0016 / PRD-0011)#119

Open
tieguy wants to merge 22 commits into
mainfrom
claude/wikidata
Open

Wikidata entity read path: platform wikibase module + patrol MVP (ADR-0016 / PRD-0011)#119
tieguy wants to merge 22 commits into
mainfrom
claude/wikidata

Conversation

@tieguy

@tieguy tieguy commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements ADR-0016 (the Wikidata entity read mechanism) end to end and the PRD-0011
patrol-read MVP: picking wikidatawiki in the existing picker now yields a
readable, 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:

  1. sp42-platform::wikibase — the shared read model: typed entity/statement/snak
    values (each statement keeps its canonical raw JSON so change detection is exact
    for unmodeled datatypes; references keep the full snak set with P854 as one
    property), endpoint-agnostic parse_entity (Special:EntityData docs with redirect
    tolerance, or bare prop=revisions slot content), pure request builders over the
    HttpClient boundary, claim rendering, full-depth EntityDiff with the
    never-a-no-op honesty invariant (qualifier-only/rank-only/reference-only edits
    classify; unmodeled deltas surface as parts.other / UnknownEntityChange),
    ContentDiff routing, and the content-model capability axis.
  2. sp42-mcp refactorverify_wikidata_statement consumes the platform model
    (ADR-0016 Decision 2 promotion); its public contract and all three tests are
    unchanged.
  3. EditEvent.content_model — additive Option<String> (serde default/skip;
    old snapshots deserialize unchanged). None = honestly unknown.
  4. Revision-content parse + route_content_diff — the one-call
    prop=revisions…contentmodel diff fetch and the Decision 4 routing helper with
    honest degradation (unparseable entity → text; a broken before never fabricates
    an all-added diff).
  5. Server — new /operator/content-diff/{wiki}/{rev}/{old} route returning
    ContentDiffReport (routed diff + labels resolved server-side in one batched
    wbgetentities call, best-effort). Media-reference extraction is not invoked for
    entity content. /operator/diff unchanged.
  6. Browser — the patrol diff pipeline dispatches on the report: text → existing
    viewer/caches untouched; entity → new EntityDiffViewer ("educated at: University
    of X", added/removed/changed rows with changed-parts summaries).
  7. MVP gates — ingestion seeds content_model from 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

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Build, CI, or release
  • Security-sensitive change

Validation Required

cargo clippy --workspace --exclude sp42-desktop --all-targets   # pedantic clean
cargo test --workspace --exclude sp42-desktop                   # 870 passed, 0 failed
./scripts/check-layering.sh                                     # passes (wikibase is platform)
cargo check -p sp42-app --target wasm32-unknown-unknown         # passes
cargo fmt --all --check

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

  • Touches auth/session/cookies/CSRF
  • Touches deployment/runtime configuration
  • Touches desktop packaging
  • Touches scoring or Wikimedia action behavior
  • Needs an ADR

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

  • One deliberate deviation from ADR-0016 Decision 1's wording:
    EditEvent.content_model is Option<String> with None = unknown, rather than a
    serde default of "wikitext". A literal wikitext default would mislabel Wikidata
    events from ingestion sources that don't carry a content model; Option keeps old
    snapshots deserializing (the ADR's actual intent) while staying honest. Capability
    derivation treats None as pre-ADR behavior (wikitext signals on).
  • Namespace-default seeding (default_namespace_content_model) mirrors
    MediaWiki's $wgNamespaceContentModels: it is a property of the site's namespace
    configuration
    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.
  • Convergence with Citation verification over MCP — read-only agent surface (MVP) #103 delivered: sp42-mcp/src/wikidata.rs now consumes the
    platform parser per the merged design plan
    (docs/design-plans/2026-07-01-wikidata-read-model.md); the two Wikidata read
    implementations are one.
  • Places most worth scrutiny: the statement pairing rules in 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-resolution
    error path in the server (failures render raw ids, never fail the diff).

🤖 Generated with Claude Code


Generated by Claude Code

claude added 7 commits July 7, 2026 19:11
…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
@tieguy tieguy requested a review from schiste as a code owner July 7, 2026 20:57
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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +272 to +273
artifacts.set_current_entity_diff.set(None);
return current_rev;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Threading note: the follow-up round's fixes are in 49a0cf3 — see the two new thread replies.)


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines 370 to 372
let Some(before) = revisions.remove(&old_rev_id) else {
return Ok(None);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +443 to +445
let liftwing_risk = if let Some(item) = selected
&& sp42_core::derive_content_model_capabilities(item.event.content_model.as_deref())
.revertrisk_scoring

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +333 to +336
let media_diff = if content_diff.is_some() {
None
} else {
let mut report = build_media_diff(&before_content, &after.content);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

claude added 4 commits July 7, 2026 21:20
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +1217 to +1220
if let Some(index) = paired {
consumed_after[index] = true;
let new = &after[index];
if old.raw != new.raw {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +174 to +178
format!(
"{}: {} \u{2192} {} ({parts} changed)",
label_for(labels, &after.property),
statement_value_text(labels, before),
statement_value_text(labels, after),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/sp42-platform/src/wikibase.rs Outdated
Comment on lines +744 to +745
if let Some(title) = link.get("title").and_then(Value::as_str) {
map.insert(site.clone(), title.to_owned());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread crates/sp42-platform/src/wikibase.rs Outdated
Comment on lines +939 to +940
WikibaseValue::Quantity { amount, .. } => ValueDisplay {
text: amount.trim_start_matches('+').to_owned(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

claude added 4 commits July 10, 2026 19:55
- 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/sp42-platform/src/wikibase.rs Outdated
Comment on lines +1477 to +1479
StatementChange::Added { statement } => EntityChangeRowReport {
kind: EntityChangeKind::Added,
text: statement_row_text(labels, statement),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
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