Skip to content

feat(id): signed themes and layered board views (#2813) - #3971

Merged
dezren39 merged 3 commits into
mainfrom
queue/5812-2813-signed-themes-layered-views
Nov 1, 2026
Merged

dezren39 merged 3 commits into
mainfrom
queue/5812-2813-signed-themes-layered-views

Conversation

@dezren39

@dezren39 dezren39 commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Implements #2813 (queue task 5812), the phase after the signed board object graph v1 (#2812, PR #3892 — merged).

What

All in pkgs/id/src/boards.rs, extending the existing signed-object substrate:

  • Theme object v1 (BoardObject::Theme): design tokens (name-validated BTreeMap, ≤64 tokens, values ≤512 bytes of plain data), a closed layout enum (list / grid / compact), content-addressed asset references (metadata only, no embedded payloads), and forked_from fork lineage, validated at insert to name an existing theme (never itself).
  • Board.theme_ref applies a theme by reference. Themes deliberately name no board: a theme that carried its board's hash could never be referenced from that board's own root hash, so themes are portable signed packages and the board root names one. The referenced theme must already be in the graph when the board is accepted (publish the theme first). Pre-theme_ref v1 board bodies still decode (#[serde(default)] + skip-if-none, covered by a test).
  • Three-layer view composition (BoardGraph::compose_view): board layer (signed moderation log + author tombstones + resolved theme), host layer (caller-supplied hashes the serving host will not serve), personal layer (one viewer's hides + muted authors). Transparency rule — hidden ≠ erased: hidden objects stay in the graph, are omitted from objects, and are reported in hidden with the layer and, for board-layer hides, the moderation action / tombstone and its reason. moderation_log lists every moderation action for the board in every view: the log is public.
  • Board-layer hide state is order-independent (signed hides must outnumber signed restores), matching the role-grant existence philosophy already in this module, so every peer composes the same view in any arrival order. Superseded versions are neither shown nor hidden — the projection shows the replacement; history stays in the graph.

Custom HTML templates remain out of v1: a theme is data, never markup.

Tests

Five new tests in the boards module: theme fork lineage (missing / non-theme / self references), theme shape rejection (bad token names, empty values, duplicate assets), theme_ref resolution + rejection + legacy body decode, the layered hide → restore → re-hide cycle with transparency assertions, and personal/host layer hides with no board record.

CI is the verification route for this crate (no builds on the sandbox); targeted cargo test -p id boards is the suite these tests live in.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Theme object v1: design tokens, closed layout enum, content-addressed asset refs, fork lineage. Board.theme_ref applies a theme by reference (theme published first; themes name no board, keeping the content-addressed graph acyclic). BoardGraph::compose_view projects the three layers - personal filter, board view, host view - under the transparency rule: hidden objects stay in the graph and are reported with the layer and record that hid them, and the board moderation log is public in every view. Custom HTML templates stay out of v1: a theme is data, never markup.
@dezren39
dezren39 requested a review from a team as a code owner November 1, 2026 20:59
@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 23 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f06c4c35-7b8f-4182-b2cb-621ab91c08ef
Commits

Reviewing files that changed from the base of the PR and between 70a595a and 3f6d45f.

Files selected for processing: 1
  • pkgs/id/src/boards.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T21:47:06.963730Z 3f6d45f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 5a3dd4884c

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkgs/id/src/boards.rs Outdated
let BoardObject::Theme(theme) = &theme_object.object else {
bail!("board theme reference is not a theme object")
};
Some(theme.clone())

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 Apply host filtering before returning the resolved theme

When layers.host_hidden contains the board's theme hash, compose_view still returns that theme's full body through BoardView.theme. Themes also bypass the later filtering loop because their board_ref() is None, so the suppression is never reported in hidden. This makes the host layer ineffective for a signed theme package that the host explicitly refuses to serve. Check the referenced theme against the applicable layers before including it in the projection.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 2333c6d. The resolved theme is now checked against the host layer (and the personal layer: personal hides + muted theme author) before it is returned; a suppressed theme yields theme: None and a hidden entry naming the theme hash and the suppressing layer. Covered by host_layer_can_suppress_the_board_theme.

Comment thread pkgs/id/src/boards.rs Outdated
Comment on lines +871 to +872
BoardObject::Tombstone(value) if &value.board == board => {
tombstones.entry(value.target.clone()).or_insert_with(|| hash.clone());

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 Exclude superseded tombstones from active deletion effects

When an author supersedes a tombstone targeting object A with another tombstone targeting object B, both objects remain hidden because this pass applies every historical tombstone. validate_links permits this same-author, same-kind replacement, and Supersede.replacement explicitly replaces the earlier object in a projection, but the superseded set is only checked later while selecting visible content. Resolve superseded records before gathering active tombstone effects so the obsolete tombstone does not continue hiding A.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 2333c6d. compose_view now gathers the board's supersede relations in a first pass, and a superseded moderation action or tombstone record has no projection effect (it remains part of the signed history, and moderation actions remain in the public log). Covered by superseded_tombstone_loses_effect_and_keeps_its_signed_reason: superseding a tombstone on post A with one on post B makes A visible again and hides only B.

Comment thread pkgs/id/src/boards.rs Outdated
.unwrap_or((hash.clone(), None));
(Some(action), reason)
} else {
(tombstone.cloned(), Some("author tombstone".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 the signed tombstone reason in hidden entries

When an author supplies a deletion reason in Tombstone.reason, the composed view replaces it with the literal "author tombstone". Consequently, consumers of BoardView.hidden lose the author's recorded explanation, contrary to HiddenObject.reason's documented purpose of reporting the reason carried by the hiding record. Retain the tombstone's reason alongside its hash when collecting tombstones and return that recorded value here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 2333c6d. The tombstone collection now keeps the record's hash together with its signed reason, and the hidden entry reports that recorded reason verbatim (no more literal "author tombstone" replacement). Asserted in superseded_tombstone_loses_effect_and_keeps_its_signed_reason for both the original and the replacement tombstone.

Comment thread pkgs/id/src/boards.rs
Comment on lines +856 to +860
BoardObject::ModerationAction(value) if &value.board == board => {
moderation_log.push(hash.clone());
match value.action {
ModerationActionKind::Hide => {
*hide_balance.entry(value.target.clone()).or_default() += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce moderator revocations before applying moderation effects

After the owner revokes a moderator with a RoleAttestation that supersedes their grant, that former moderator can still sign new hides or restores that change this view. Graph insertion intentionally accepts historical grants through has_role, whose documentation explicitly delegates revocation enforcement to the view/serving layers, but this new projection counts every accepted moderation action without checking the author's current authority. Resolve the current role attestations before applying moderation effects, exempting the board owner, while retaining revoked moderators' actions in the public log.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 2333c6d. compose_view now resolves the current moderator frontier before applying any moderation effect: role attestations superseded via their signed supersedes links are history, not authority (frontier follows the causal links, never arrival order), so a revoked moderator's hides/restores no longer change the projection. Their actions remain in the public moderation_log, as required. Covered by the new test view_enforces_moderator_revocation_but_keeps_the_public_log.

…(Codex review)

Dispositions all four Codex findings on PR #3971: (P1) compose_view resolves the current moderator frontier via signed supersedes links - a revoked moderator's actions stay in the public log but lose effect; (P2) superseded moderation/tombstone records have no projection effects; (P2) hidden entries preserve the signed tombstone reason; (P2) host/personal layers can suppress the board theme, reported in hidden. Three new tests cover revocation, superseded tombstones with reasons, and theme suppression.

@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: 2333c6d429

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkgs/id/src/boards.rs Outdated
let current_moderators: BTreeSet<String> = self
.objects
.iter()
.filter(|(hash, _)| !attestation_superseded.contains(hash))

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 Exclude superseded grants from the authority frontier

When the owner replaces a moderator grant G with a revocation R using a generic Supersede(G, R) record, rather than R's supersedes field, validate_links accepts the replacement and the projection removes G from visible objects. However, this filter ignores the general superseded set, so G still adds the moderator to current_moderators and their hide actions continue affecting the view. Exclude generically superseded grants when computing current authority so the two accepted replacement mechanisms do not produce contradictory moderation behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 3f6d45f. The authority frontier now excludes attestations in the general (active) superseded set as well as those named by an attestation's own supersedes field, so both accepted replacement mechanisms revoke identically. Covered by generic_supersede_of_a_grant_also_revokes_authority: grant replaced by a revocation through a generic Supersede record ends the moderator's hide effect while the action stays in the public log.

Comment thread pkgs/id/src/boards.rs Outdated
Comment on lines +885 to +887
if let BoardObject::Supersede(value) = &signed.object {
if &value.board == board {
superseded.insert(value.previous.clone());

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 Stop applying superseded supersede records

A Supersede record can itself be superseded: for same-author posts A/B/C/D, the graph accepts S1=Supersede(A,B), S2=Supersede(C,D), and S3=Supersede(S1,S2). This loop nevertheless applies S1 and suppresses A even though S1 has been replaced. Consequently, editing a relationship leaves its obsolete effect active, contrary to the stated projection rule that superseded records have no effect. Resolve which relationship records remain active before applying their edges.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 3f6d45f. Supersede relations are now resolved to an active frontier by fixed point — a record stays active while no active record names it as its previous version (supersede cycles are impossible; insertion rejects them) — and only active records contribute edges to the superseded set. Covered by superseded_supersede_record_has_no_effect using exactly the A/B/C/D + S1/S2/S3 construction from this finding: A and B are current again, C stays replaced by D.

@dezren39

dezren39 commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@codex review — round 1 findings (1 P1, 3 P2) are fixed in 2333c6d and each thread carries its disposition reply; targeted suite cargo test -p id --lib boards passes 16/16 on the laptop.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 2333c6d429

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…2813, Codex round 2)

Two round-2 Codex findings on PR #3971: a grant replaced through a generic Supersede record (not the attestation supersedes field) now also leaves the authority frontier, and supersede relations are resolved to an active frontier by fixed point - a superseded supersede record contributes no edge. Two tests cover both mechanisms.

@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: 3f6d45fe57

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkgs/id/src/boards.rs
Comment on lines +909 to +912
let next: BTreeSet<ArtifactHash> = active_records
.difference(&named)
.cloned()
.collect();

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 Reconsider removed records when resolving the supersede frontier

When supersede records are replaced more than once, this loop permanently removes records that should become active again. For same-author posts A/B/C/D, the graph accepts S1=Supersede(A,B), S2=Supersede(C,D), S3=Supersede(S1,S2), and S4=Supersede(S3,S2). S4 disables S3, so S1 should reactivate and suppress A. Instead, the first iteration removes both S1 and S3, and active_records.difference(&named) can never restore S1, leaving A incorrectly visible. This two-level chain exposes a failure not covered by the existing one-level regression test. Resolve activity against the complete record set, allowing a record to reactivate when its suppressor becomes inactive.

Useful? React with 👍 / 👎.

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.

1 participant