{
"id": "net.anisota.graph.wordlist",
"defs": {
"main": {
"key": "tid",
"type": "record",
"record": {
"type": "object",
"required": [
"name",
"words",
"createdAt"
],
"properties": {
"name": {
"type": "string",
"maxLength": 64,
"description": "Display name for this word list"
},
"words": {
"type": "array",
"items": {
"ref": "#word",
"type": "ref"
},
"maxLength": 500,
"description": "The muted words/phrases in this list"
},
"source": {
"ref": "#source",
"type": "ref"
},
"enabled": {
"type": "boolean",
"default": false,
"description": "Whether this list is currently active. Scheduling and spell effects further gate an enabled list."
},
"schedule": {
"ref": "#schedule",
"type": "ref",
"description": "Optional active window. Absent means the list applies at all times while enabled."
},
"strength": {
"type": "integer",
"maximum": 100,
"minimum": 0,
"description": "Mute strength percentage: the share of matching posts this list mutes. Which posts fill that share is decided by match confidence — how many of the list's words hit, whether they landed in the post's own text, a hashtag, or only a quoted post, and how much of the post they account for — so a softened list keeps the squarest matches muted and lets the marginal ones through. Deterministic per post, so a post never flickers. Absent means 100 (mute everything it matches)."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "When this list was created"
},
"treatment": {
"type": "string",
"description": "What this list does to a post it mutes, overriding the app-wide muted-words handling: 'hide' removes it from feeds, 'warn' collapses it behind a notice, 'show' leaves it visible and only counts it toward impact. Absent inherits the global setting.",
"knownValues": [
"hide",
"warn",
"show"
]
},
"updatedAt": {
"type": "string",
"format": "datetime",
"description": "When this list was last modified"
},
"description": {
"type": "string",
"maxLength": 300,
"description": "Optional description of what this list covers"
},
"targetFeeds": {
"type": "array",
"items": {
"type": "string",
"maxLength": 512
},
"maxLength": 50,
"description": "Feeds where this list applies: feed generator / list at-uris, or the literal sentinel 'following' for the home timeline. Empty or absent applies everywhere."
},
"syncToBluesky": {
"type": "boolean",
"default": false,
"description": "Whether this list's words are also projected into Bluesky's native mutedWordsPref (managed entries, removed when the list is disabled or deleted)."
}
}
},
"description": "A named, shareable list of muted words/phrases — Anisota's managed layer on top of Bluesky's flat muted-words preference. Each list is a complete, self-contained record so it can be shared by URL and saved (copied) into another user's PDS, mirroring the net.anisota.spell.custom architecture. Muting is applied client-side by the moderation pipeline: multiple enabled lists combine, a list can be scoped to specific feeds, restricted to a time-of-day/day-of-week window, and softened with a strength percentage (mute only that share of matching posts, deterministically per post). A list can optionally be projected into Bluesky's native mutedWordsPref for parity with other clients."
},
"word": {
"type": "object",
"required": [
"value"
],
"properties": {
"value": {
"type": "string",
"maxLength": 100,
"description": "The word or phrase to mute"
},
"targets": {
"type": "array",
"items": {
"type": "string",
"knownValues": [
"content",
"tag"
]
},
"description": "What to match against: post text ('content') and/or hashtags ('tag'). Absent means both."
},
"actorTarget": {
"type": "string",
"description": "Whose posts to apply this word to. Absent means 'all'.",
"knownValues": [
"all",
"exclude-following"
]
}
},
"description": "One muted word or phrase, matching the shape of Bluesky's mutedWord items so a list can be projected into the native preference losslessly."
},
"source": {
"type": "object",
"properties": {
"type": {
"type": "string",
"knownValues": [
"original",
"learned"
]
},
"rootDid": {
"type": "string",
"format": "did",
"description": "DID of the original author at the top of the lineage."
},
"rootUri": {
"type": "string",
"format": "at-uri",
"description": "AT URI of the first author's list — the top of the lineage — propagated to every descendant."
},
"originalDid": {
"type": "string",
"format": "did",
"description": "DID of the user this list was saved from."
},
"originalUri": {
"type": "string",
"format": "at-uri",
"description": "AT URI of the list this copy was saved from (its immediate parent)."
}
},
"description": "Provenance. An 'original' list was created here; a 'learned' list was saved from another user, carrying a backlink to its immediate parent (originalUri/originalDid) and to the top of its lineage (rootUri/rootDid), modelled on net.anisota.spell.custom so save counts can aggregate across re-shares."
},
"schedule": {
"type": "object",
"properties": {
"days": {
"type": "array",
"items": {
"type": "integer",
"maximum": 6,
"minimum": 0
},
"maxLength": 7,
"description": "Days of week the list is active (0 = Sunday … 6 = Saturday)"
},
"endTime": {
"type": "string",
"maxLength": 5,
"description": "Window end as 'HH:MM' local time"
},
"startTime": {
"type": "string",
"maxLength": 5,
"description": "Window start as 'HH:MM' local time"
}
},
"description": "A recurring active window. Days and time window combine: the list applies only on the listed days (absent/empty = every day) and only inside the time window (absent = all day). A window whose end is before its start wraps past midnight; equal start and end means no time bound (all day on the listed days)."
}
},
"$type": "com.atproto.lexicon.schema",
"lexicon": 1
}