Skip to content

Releases: bryanmatthewsimonson/xray

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 16 Jul 05:35
02ca725

Added

  • Phase 24.3 — rotation warnings + key recovery UX. Switching,
    generating, or importing a primary identity now confirms with the
    previously-silent consequences (entity-sync blobs encrypted to the
    old key become unreadable; publish stamps stay per-identity; derived
    entity keys can't be re-derived from the new primary). Options gains
    a Restore entity keys action (the restoreDerivedKeys surface —
    keystore loss recovers derived-era entities' original pubkeys), and
    published entity profiles open with an honest self-description line
    ("An X-Ray subject record maintained by — a research dossier
    …, not the subject posting") so generic NOSTR clients render an
    honestly-labeled record. Completes Phase 24.

  • Phase 24.2 — creator binding on the wire. Entity identities are
    now provably the creator's: a kind-30069 OwnedKeys manifest
    (primary-signed, replaceable — revocation is republish-without-the-
    key) lists every owned entity pubkey and publishes automatically with
    entity batches (fingerprint-gated); entity-signed events (kind 0 /
    30067) gain a ['p', creator, '', 'creator'] backlink and a
    NIP-26-format delegation tag minted by the primary (Local mode)
    — the strongest self-contained proof, verified by X-Ray itself with
    fail-closed condition checking (kind whitelist + created_at window).
    The portal badges entities ✓ creator-bound (manifest + token) or
    ◐ partially bound (one of the two). Wire: new kind 30069 +
    two additive tags on entity events — existing consumers unaffected.
    docs/NIP_DRAFT.md §"Kind 30069" + §"creator binding".

  • Phase 24.1 — durable entity keys (deterministic derivation). New
    entity keypairs are now derived from the primary identity
    (HKDF-SHA256, domain xray-entity-v1, info = the entity id, mod n) —
    same primary + same entity ⇒ the same pubkey, forever. A lost or
    reset keystore is recoverable: EntityModel.restoreDerivedKeys()
    re-derives every missing owned key (derived-era entities get their
    original pubkey back; legacy random keys re-derive to a new one,
    reported honestly). Existing random keys stay valid — the stored key
    always wins, no forced migration. The full NIP-26 tradeoff analysis
    and the layered design (derivation + owned-keys manifest + NIP-26
    token + honest kind-0) live in docs/ENTITY_IDENTITY_DESIGN.md; the
    binding wire (kind 30069 + delegation tags) is the next slice. No
    wire change in this slice.

  • Phase 23.2 — publish the corpus analysis. The Phase-20 corpus
    synthesis (summary / opposing positions / cruxes / load-bearing claims
    / gaps) can now be published to NOSTR from the case dashboard's
    "Publish brief…" button, so a stranger with only a keypair can read
    the insights. Two artifacts, user-signed and cross-linked: a
    readable kind-30023 long-form article (readable in any NOSTR
    client — the "article that shows the insights") and a structured
    kind-30068 CaseBrief
    event (rendered richly in X-Ray). Both are
    prose/data only — no fused score, no verdict (the Phase-20 firewall
    carried onto the wire, guard-tested), and the reviewer-facing
    proposals never publish. The portal reads both back into a new
    Briefs tab with an inspector summary. Wire: new addressable
    kind 30068 (additive; existing consumers unaffected).
    docs/NIP_DRAFT.md §"Kind 30068 — CaseBrief".

  • Phase 23.1b — per-link evidence role (citation intent). Each
    outbound link in a capture can be tagged with WHY the article cites it
    evidence (the primary source relied on), mention, supports,
    disputes, or reviews — from the reader's Media & source dialog. A
    lay-relabelled CiTO subset. This is what marks a debater's article as
    disputing the primary paper vs. citing it as evidence; combined
    with the target's source-type, it's a visible secondary→primary
    derivation edge. The portal inspector lists a capture's roled "cited
    sources." Wire (additive, kind 30023): an optional 4th positional
    on the existing link tag, whitelisted both directions — no new kind.
    docs/NIP_DRAFT.md §"link tag".

  • Phase 23.1 — source-type provenance (evidence classification). Any
    capture can now be declared a primary-record / primary-research /
    reporting / analysis / reference source from the reader's 🎙
    Media & source
    dialog — auto-suggested from scholarly identifiers
    and schema.org type, confirmed by the user. Distinguishes an
    originating primary source (e.g. the Proximal Origins paper) from a
    write-up that cites it. Grounded in the library primary/secondary/
    tertiary distinction + the epistemic-audit source taxonomy. The portal
    shows a primary-source badge. Wire (additive, kind 30023): one new
    ['source-type', <value>] tag, whitelisted both directions, distinct
    from content_format/media and from the verdict tier ladder — no
    new kind. docs/NIP_DRAFT.md §"source-type".

  • Phase 22 — URL-first media metadata + reader transcript attach
    (22.1–22.2).
    The URL is the episode's identity: capture a podcast
    episode's page (Spotify, Apple, Substack, YouTube, a custom site) like
    any page, then the reader's 🎙 Media… button declares what the URL
    contains (podcast / video — user-declared, never inferred), sets the
    universal podcast IDs, and attaches a pasted/uploaded transcript as a
    ## Transcript section upserted into the captured body (bounded
    replace on re-attach; YouTube's own suffixed transcript sections are
    never touched). The body change re-hashes honestly (prior version
    snapshotted); metadata-only saves never touch the hash; the
    speaker→claim prefill now works on any article carrying
    transcript_meta. Wire (additive, kind 30023): one new
    ['media', 'podcast'|'video'] tag, whitelisted both directions —
    no new kind, existing consumers unaffected. docs/NIP_DRAFT.md
    §"media tag". The Phase 21 portal import remains as the no-URL
    fallback.

  • Phase 21 — podcast transcript import (21.1–21.3). Paste or upload a
    podcast transcript (SRT, WebVTT with <v> voice tags, or Speaker:
    lines) from the portal — library header or a case view — and it becomes
    an ordinary archive record: a speaker-labeled markdown body, a
    contentType:'transcript' / platform:'podcast' article, and (when the
    user supplies them) universal podcast identifiers. It joins cases via
    the 20.2 picker and feeds 20.4 corpus synthesis untouched; selecting a
    turn prefills "who said it" with that turn's speaker. Wire (additive,
    kind 30023):
    new show / podcast_guid / podcast_episode_guid /
    feed_url / itunes_id tags with NIP-73 i co-emits
    (podcast:guid:…, podcast:item:guid:…), a feed_url second-r, and
    a transcript_meta (<format>:<turns>:<speakers>) manifest — bodies
    live in content, and all round-trip through reconstructArticleFromEvent.
    No new kind. docs/NIP_DRAFT.md §"podcast identity tags".

  • Phase 20 — case-first: making a case corpus usable (20.1–20.4).
    Driven from the live COVID/Rootclaim corpus. Union membership — a
    case includes an article that is tagged with the case entity OR
    referenced by a claim; tag-only articles are now first-class sources
    everywhere (the portal case view stopped reading "0 sources"), with a
    processed/unprocessed distinction and an "extract claims" nudge.
    Add-to-case outside the reader (case-membership.js) — an "Add
    sources…" picker on the portal case view and the side-panel case
    detail tags archived articles into a case (local-only, republish hint
    on published records) with a "✕ remove" affordance. Local case
    entity graph
    (case-graph.js + case-graph-view.js) — the case at
    center, its member articles, tagged/claimed entities, co-tag
    adjacency, and the contradiction edges the dossier's knots compute;
    the spokes-graph empty state routes to the case dashboard. LLM
    corpus synthesis
    behind the new caseSynthesis flag (+ llmAssist

    • key, default off) — a map/reduce over the member articles that
      drafts a grounded case brief (summary, positions, cruxes of
      disagreement side by side, load-bearing claims, coverage gaps) plus
      reviewable cross-article proposals; every quote is machine-grounded,
      there is no fused score or verdict, and the brief is stored in the
      precious xray-audits DB (v2 case-briefs store, export-included)
      and carried in the case export. No new wire kind — proposals
      materialize as ordinary 30040/30055 on accept.
      docs/CASE_SYNTHESIS_DESIGN.md.
  • Phase 19 — entity dossiers, end to end (19.2–19.8). The
    provenance-pinned knowledge base: facts ride claims (additive kind-30040
    fact/valid_from/valid_to/observed_at tags, band-precision ISO
    dates, parseClaimEvent read-back); the computed-on-read
    entity-dossier.js assembler over the alias family (unknown-by-default
    field table, contested-never-resolves, judgments routed to the
    integrity record — no score anywhere, string-guard tested); the portal
    dossier view + side-panel compact table with evidence click-through;
    the reader "📇 Add fact" flow with honest-precision date inputs and an
    inform-never-block conflict pre-flight; the case scope editor
    (authored framing — side panel, case export, portal case header, and
    per-member "dossier →" links); LLM fact extraction (kind:'fact',
    default OFF, quote-grounded, "never supply a value from your own
    knowledge"); and — behind the new entityCorpusPublishing flag
    (default off, full disclosure) — enriched kind-0 profiles
    (published-claim facts only, contested fields omitted, "per "
    attribution) plus the new kind-30067 entity fact sheet (contested
    fields both sides, every fact a-referencing its published claim),
    with hash-gated automatic republish and a manual portal republish.

  • Phase 17 Part A — entity health + canonical sweep (E1+E3). The
    ...

Read more

v0.5.1

Choose a tag to compare

@github-actions github-actions released this 09 Jun 06:24
3cc7489

Fixed

  • Inline person/place names no longer vanish from article captures on
    sites that wrap them in popup-classed elements (e.g.
    josephsmithpapers.org). Readability's unlikelyCandidates blocklist
    matches the substring popup and was deleting the whole wrapper —
    visible name included — leaving dangling punctuation. The extractor now
    unwraps aside.popup-wrapper to its reference text before Readability
    runs.
  • Publishing no longer fails with invalid: tag val was not a string
    on pages whose JSON-LD carries a non-string articleSection (array) or
    inLanguage (object). The kind-30023 builder now sanitizes every tag
    value to a string — flattening arrays, extracting name/@value from
    schema.org objects, and dropping anything unstringifiable — so a single
    odd metadata field can't make relays reject the whole event.

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 29 May 08:59
306a173

This release consolidates three feature lines onto main: default-to-local
signing + the consolidated Settings hub, the Phase 9a crowdsourced
URL-metadata data model + NIP draft, and the Phase 9 social-media identity
layer.

Added

  • Signing method is now an explicit user preference
    (preferences.signing_method, values 'local' | 'nip07' | 'nsecbunker')
    with local signing as the default. Replaces the auto-detect-NIP-07-
    then-NSecBunker probe at content-script init.
  • Signer façade (src/shared/signer.js) — single sign call site that
    dispatches to Crypto.signEvent (local), the injected NIP-07 client, or
    NSecBunkerClient.signEvent. The whole publish path goes through it.
  • Local primary identity in a dedicated local_primary_identity
    storage key with its own Storage.primaryIdentity namespace (Generate /
    Import nsec / Show nsec / Reset). Kept distinct from keypair_registry
    so an entity-key export never leaks the user's own nsec.
  • Idempotent signing-method migration — existing profiles land on
    signing_method=local, signing_method_configured=false so a one-time
    setup banner fires on the next Settings open.
  • Signing tab redesign in Options — radios for Local / NIP-07 /
    NSecBunker, per-method panels, NSecBunker Test connection button,
    and an always-visible Active method: … line at the top of the tab.
  • Per-relay flags surfaced in Options → Relays — URL / read / write
    / enabled rows replace the textarea. The structured shape persists as
    preferences.relays; preferences.default_relays (URL list) is
    auto-synced to the enabled+writable subset for back-compat with every
    reader (nostr-client.js, background, reader, sidepanel).
  • CONFIG override controls in Options → Advanced — article cache
    enabled/budget, min content length, max claim length. Applied via a
    new applyConfigOverrides() at content-script init.
  • FAB panel header gained a settings cog and entity-browser icon.
    Clicking either messages the SW (xray:openSettings,
    xray:openEntities), so the FAB itself is a settings entry point.
  • FAB relay picker now reads from Storage.relays.get(), filtered
    to enabled+writable relays. Settings → Relays drives what the FAB
    offers; static CONFIG.relays no longer leaks through.
  • Quick-actions bar at the top of the Options page (Toggle Capture
    / Entity Browser / Capture tips), so Options is a complete settings
    hub on its own.
  • Storage.relays.set() — structured-shape writer; round-trips
    through Storage.relays.get() and keeps default_relays in sync.
  • 12 new tests in tests/signer.test.mjs covering all three
    signing branches, primaryIdentity round-trip, the signing_method
    migration, and Storage.relays.set().
  • Phase 9a — crowdsourced URL-metadata data model (src/shared/metadata/).
    The wire-format foundation for annotations, fact-checks, ratings,
    topic-scoped trust, and helpfulness votes, conforming to
    docs/NIP_DRAFT.md:
    • NIP-73-conformant URL normalizer (url-normalizer.js);
      Utils.normalizeUrl is now a thin wrapper over it.
    • W3C Web Annotation selector capture + a confidence-scored
      resolution cascade (anchor-capture.js / anchor-resolver.js).
    • Builders for kinds 30050 (Annotation), 30051 (FactCheck), 30052
      (Rating), 30053 (TopicTrust), 9803 (HelpfulnessVote) + the kind
      30023 responds-to tag extension (builders.js).
    • First-order trust graph (kind 3 + kind 30053) and a v1
      binary-trust ranker (trust-graph.js / ranker.js).
    • Feature flags gating the not-yet-surfaced kinds; IDB v2 metadata
      stores in archive-cache.js. Data model only — no UI yet.
    • docs/NIP_DRAFT.md — the authoritative wire-format spec
      ("Web Content Annotations, Fact-Checks, and Topic Trust").
  • Phase 9 — social-media identity layer (src/shared/identity/).
    Turns a captured author (commenter or post author) into a stable,
    cross-capture identity and lets the user collapse cross-platform
    accounts into one canonical person:
    • Deterministic accountPubkey per <platform>:<stableId>
      (platform-account.js) — an identifier-only key that never signs;
      a Storage.platformAccounts registry in chrome.storage.
    • Published comments and post authors now carry a stable p-tag
      identity (kind 30041 / 30023), via recordAccount + the existing
      buildCommentEvent socket and a new optional authorAccountPubkey
      on buildArticleEvent.
    • YouTube comment capture — parses the InnerTube
      /youtubei/v1/next responses (reusing the Phase 8a api-interceptor;
      both legacy commentThreadRenderer and modern commentEntityPayload
      shapes), keyed on the commenter's channelId. Scroll the comments
      into view before capturing (see docs/CAPTURE_GUIDE.md).
    • Manual account↔entity linking in the sidepanel Entity Browser,
      with alias-chain-aware resolveAccountToEntity. v1 is
      local-index-only (no kind 32126 relay publish) and manual-link-only.
  • Combined test suite: 519 passing (up from 223 at v0.4.0), adding
    tests/metadata-*, tests/identity-*, tests/youtube-comments, and
    tests/platform-account-event.

Changed

  • Toolbar-icon click no longer opens a popup. The icon now toggles
    the FAB capture panel on the active tab via chrome.action.onClicked,
    mirroring the keyboard shortcut. On non-injectable pages
    (chrome://, file://, extension pages) it falls back to opening
    the Options page so the click is never a silent no-op.
  • Right-click menu on the toolbar icon expanded to host the
    jump-actions the popup used to provide: Toggle Capture / Entity
    Browser / Settings… / View Keypair Registry / Export Keypair
    Registry / Capture tips.
  • content/ui.js publish flow simplified — Local + NIP-07 collapse
    into a single Signer.signEvent path; NSecBunker keeps its
    per-publication-keypair flow.
  • Background xray:sign / xray:getPubkey route through Signer
    instead of NIP07Client directly. The reader-page publish flow
    respects the user's chosen method.
  • README, ROADMAP, SMOKE_TEST, CONTRIBUTING updated to reflect the
    current signing model, the popup removal, and the consolidated
    Settings hub. Test counts and bundle counts brought current.

Removed

  • Toolbar popup (src/popup/) — files deleted, popup bundle
    dropped from esbuild.config.mjs, action.default_popup removed
    from manifest.json. The popup duplicated state already shown in
    the FAB header and split jump-actions across multiple surfaces.
  • Forward-looking design plans under docs/plans/ — the five
    tentative documents (NIP-COURT-OF-PUBLIC-OPINION,
    evidentiary-standards, protocol-adoption-guide,
    trust-reputation-system, ui-ux-design) are out of scope for
    X-Ray's current phase and have been removed. The Phase 9 row that
    referenced them is gone from the roadmap.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 24 Apr 17:13

Added

  • Phase 8d — Facebook handler (src/shared/platforms/facebook.js).
    Third and final hard-tier platform; Phase 8 now complete.
    Recognizes all eleven FB post URL shapes: /<user>/posts/<id>,
    /<user>/videos/<id>, /<user>/photos/<set>/<id>, /watch/?v=,
    /reel/<id>, /permalink.php, /story.php, /share/p|v|r/<code>/,
    /photo/?fbid=, and /groups/<g>/posts|permalink/<id>/. Four
    parallel extraction paths with explicit provenance:
    • GraphQL response interception via the Phase 8a api-hook
      buffer — scored recursive walk picks the focal story by
      longest message.text + bonuses for feedback/attachments.
      Recursive findCreationTime fishes the timestamp out of
      comet_sections.timestamp.story.creation_time and similar
      nestings; owner accepted alongside actors[0] for the author.
    • Open Graph + Twitter Card meta tags — parser handles the
      "<Author>: \"<body>\"" and "<Author> wrote on Facebook: <body>"
      shapes plus optional leading engagement-count prefixes.
    • DOM scrape scoped to [role="dialog"] (post-detail modal)
      / [role="article"][aria-posinset] (feed unit) via
      pickFocalScope — every scraper (author, body, verified flag,
      post date, images) bounded so sibling posts visible behind the
      modal never leak into the capture.
    • HTML snapshot + screenshot — always-on evidence layer.
      Post date extracted from absolute aria-label dates
      ("Monday, April 21, 2026 at 9:30 PM"), <time datetime>,
      or short-text relative-time tokens ("12h", "3d", "45m"). Event
      builder emits post_id, post_kind, author_handle, and
      platform_account: facebook:<handle> tags. Reader renders a
      Facebook-specific header with author block, engagement chips, an
      extractedFrom provenance chip, and inline screenshot evidence.
      Image gallery scraped from the modal-scoped <img> tags
      (fbcdn.net host filter, 200px size floor, signing-token-preserving
      dedup) and embedded in the markdown body. 35 new tests (URL
      grammar × 11 shapes, og:description variants, GraphQL walker
      across nested envelopes, image extractor with srcset/data-src/
      avatar-filter coverage, relative-time + absolute-date parsers,
      findCreationTime against nested-story shapes).
  • Capture quality hints + user documentation.
    • docs/CAPTURE_GUIDE.md — user-facing walkthrough covering the
      correct way to capture from Instagram, Facebook, TikTok, plus
      brief mentions of the easy-tier platforms. Includes a
      symptom → cause → fix table for common bad-capture cases.
    • In-reader hint banner — amber dashed-border <details> above
      the article metadata when extractedFrom === 'none', body text
      is short, media extraction missed on a photo post, or the
      author handle is empty. Platform-specific retry instructions,
      linked back to the capture guide.
    • Platform-aware FAB tooltip — hovering the FAB on Instagram,
      Facebook, or TikTok shows a one-line reminder of the right
      URL shape to capture from, before the user clicks.
    • Popup adds a "Capture tips (Instagram, Facebook, …)" button
      that opens the guide on GitHub.
  • Instagram post-item inline user fallback — when og-description,
    URL path, and description-pattern author extraction all come up
    empty (post-detail pages loaded via /p/<shortcode>/ without a
    username prefix), extractMediaFromGraphQL now also surfaces the
    post item's embedded user object. Used as the fourth
    handle-resolution fallback; also feeds profile enrichment when
    no dedicated data.user response was in the buffer.

Fixed

  • Instagram captures were rejected by relays with
    "invalid: tag val was not a string" because user.pk from the
    REST /api/v1/media/…/info/ response is a number. Fixed at two
    layers: normalizeUserShape now stringifies at the normalization
    boundary, and the event-builder author_id emission is
    defensively String()-wrapped. Regression test asserts every
    tag value in a built article event is typeof 'string'.

  • Facebook captures produced "null (@handle)" bylines when the
    author name came from a path other than og-meta/GraphQL.
    Defensive guard: author ? "author (@handle)" : "@handle".

  • Facebook capture misattribution chips — the author/extraction
    source was re-inferred at the end instead of being tracked at
    assignment, defaulting to og-meta even when og-meta contributed
    nothing. Now recorded as the extraction runs.

  • Facebook picked the wrong story from multi-story GraphQL
    responses (first-quack walker grabbed a sibling or comment node).
    Replaced with a candidate-scoring pass: longest message.text
    wins, with bonuses for feedback metadata and attachments.

  • Facebook image scraper pulled feed posts from behind the modal.
    Scoped to the focal post via pickFocalScope (dialog → aria-posinset
    article → article → document).

  • Facebook DOM body scraper swallowed an adjacent profile-feed
    post
    when the focal post's GraphQL story had no message.text
    and the scraper walked the whole document for longest
    <div dir="auto">. Same pickFocalScope scoping applied.

  • Facebook screenshot captured a 680×80 sliver because
    pickScreenshotTarget walked up from a thumbnail-strip image
    that passed the 200px floor. Floor raised to 400px; falls back
    to the whole post container when no large media qualifies.

  • Facebook title with embedded newlines split into two link-lines
    in the reader because the 80-char truncation landed mid-paragraph.
    truncate now collapses whitespace and cuts at word boundaries.

  • Phase 8c — Instagram handler (src/shared/platforms/instagram.js).
    Recognizes instagram.com/p/<id>/, /reel/<id>/, /tv/<id>/,
    and the /<user>/p/<id>/ and /<user>/reel/<id>/ variants.
    Capture path: Open Graph + Twitter Card meta tags as the
    load-bearing data source (server-rendered, stable contract),
    defensive DOM scrape for fields meta doesn't cover (post date
    via <time datetime>, verified-account flag via aria-label),
    and the Phase 8a evidence layer (HTML snapshot + screenshot).
    Reader gets an Instagram header with author handle (verified ✓
    when applicable), engagement counts, post-kind chip
    (post/reel/igtv), and an extractedFrom provenance chip.

  • Phase 8b — TikTok handler (src/shared/platforms/tiktok.js).
    First hard-tier platform shipped. Recognizes
    tiktok.com/@<user>/video/<id> URLs. Three-layer capture model
    in production: structured extraction from
    __UNIVERSAL_DATA_FOR_REHYDRATION__ / SIGI_STATE /
    __NEXT_DATA__ (defensive across all three SSR shapes) +
    bounded HTML snapshot of the video container + element-cropped
    screenshot. Reader gets a video-shaped header with author chip
    (verified ✓ when applicable), play/like/comment/share counts,
    music attribution, an sourceShape provenance chip, and an
    inline collapsible "📸 Screenshot evidence" panel.

  • Phase 8a — Anti-obfuscation infrastructure for upcoming
    hard-tier platform handlers (FB/IG/TikTok). Three standalone
    modules, all tested, none wired to a platform yet:

    • src/shared/html-snapshot.js — bounded sanitized outerHTML
      extractor (strips <script>, on* handlers, data: URLs,
      iframes; truncates to a byte cap with a marker; SHA-256 helper).
    • src/shared/screenshot.js + background-side handler — element-
      cropped screenshots via chrome.tabs.captureVisibleTab + an
      OffscreenCanvas crop. Pure crop-math helper covered by unit
      tests across DPR 1/1.5/2 + viewport-clamp edges.
    • src/page/api-interceptor.js — MAIN-world fetch + XHR hook
      that captures responses to URL/header-pattern matches and posts
      them back to the content script. Bundled to
      dist/api-interceptor.bundle.js for on-demand injection via
      chrome.scripting.executeScript. Pattern matcher extracted to
      src/shared/api-pattern.js for unit-testability.
  • Article-shape evidence layer — new optional article.evidence
    field carries { screenshot, screenshotHash, screenshotUrl, htmlSnapshot, htmlSnapshotHash }. Event builder surfaces the
    hashes/URL as event tags (screenshot_sha256, screenshot_url,
    html_snapshot_sha256); archive-reader inverse rehydrates them.
    Bodies stay in event content; tags carry verifiable refs.

  • 30 new tests (html-snapshot, screenshot crop math,
    api-pattern) bringing total to 126 (up from 96).

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 23 Apr 22:32

Added

  • Real toolbar icons — purple-on-purple X-Ray scan-lens
    treatment replaces the plain placeholder X. Source SVG at
    icons/source.svg; rasterize with npm run icons.
  • Release pipelineCHANGELOG.md, npm run version:set for
    lockstep package.json/manifest.json bumps, .github/workflows/release.yml
    fires on v* tag push and creates a GitHub Release with the
    packaged .zip attached.
  • Test coverage for the wire-protocol surface — new tests for
    Utils.normalizeUrl, EventBuilder.buildRelayListEvent /
    buildEntitySyncEvent, normalizeRelayUrl,
    deserializeEntityFromSync's schema tolerance, and the full
    userscript migration round-trip (96 tests total, up from 67).
  • YouTube Shorts capture — FAB now recognizes youtube.com/shorts/<id>
    URLs. Captures produce metadata-rich artifacts (thumbnail, channel,
    duration, view count, video id) with a SHORT chip in the reader
    header. Outbound events get an is_short tag.
  • Userscript migration importer — Options page gains a "Migrate"
    tab that ingests a JSON blob exported from the
    nostr-article-capture userscript's storage. Handles
    user_identity, entity_registry, relay_config,
    article_claims, evidence_links with schema normalization
    (privkey/privateKey, 16-/64-char entity ids).
  • Native OS notifications for publish — long-running publish
    flows now fire chrome.notifications so completion is visible
    even when the user has tabbed away from the reader.
  • NIP-65 relay-list sync — Push publishes a kind-10002 event with
    the device's relay list; Pull discovers it on other devices and
    offers a one-click "Add to my list" prompt.
  • Archive banner sensitivity setting — Options → Advanced exposes
    a three-way preference (Always / Only when richer / Never) for the
    Phase 7 archive-reader banner. Default is Always (any non-identical
    archived copy surfaces).
  • NIP-04 read fallback in entity sync — pull path now decrypts
    legacy NIP-04 events from the userscript era alongside NIP-44 v2.
  • Per-relay + per-format pull diagnostics — sync log shows
    received counts per relay and a Format split: N NIP-44, M NIP-04
    line.
  • Firefox sidebar supportsidebar_action manifest entry plus
    feature-tested opener that prefers browser.sidebarAction.toggle()
    on Firefox, then chrome.sidePanel.open() on Chrome/Edge.
  • Engineering journaldocs/JOURNAL.md is now the chronological
    log of bugs, design decisions, and external platform changes.
  • Smoke test checklistdocs/SMOKE_TEST.md codifies the
    ~20-min manual pre-release sweep across Phases 0–7, with an
    agent-runnable subset called out.

Changed

  • Entity-sync deserializer accepts both userscript and X-Ray
    payload shapes, normalizing on input. Previously rejected
    userscript-pushed entities with Malformed.
  • Sync log layout has a max-height with internal scroll so verbose
    pulls don't push the rest of the side panel out of view.
  • NIP-78 sync filter now uses raw URL normalization (lowercase
    scheme/host, trailing-slash strip) when comparing remote-vs-local
    relay lists, eliminating spurious "missing" matches.

Fixed

  • Popup signing-state badge now reflects reality — content script
    writes xr_signing_state to chrome.storage.local after NIP-07
    detection. Previously always showed "not detected".
  • Twitter focal-tweet detection broadened past
    data-testid="tweet" to handle the tweetDetail container; URL/id
    backfill from <a href*="/status/"> anchors.
  • YouTube transcript dedup — visible cues that YouTube renders
    3× in the DOM no longer triple in the published markdown.
  • Archive banner over-firing on short-form captures (single
    tweets) suppressed via threshold check.
  • Sidepanel CSS view-toggle[hidden] attribute now honored,
    so the inactive view actually disappears instead of overlapping
    the active one.

Removed

  • Forward-looking references to the (separate) Keystone browser
    project from the README. X-Ray and Keystone are independent.