# X archive shape

The X (Twitter) Basic Data Export is a ZIP whose extracted layout looks like:

```
Archive of <handle>/
├── Your archive.html         (UI entrypoint — ignored)
├── data/
│   ├── account.js            (owner identity: accountId, username, displayName)
│   ├── tweets.js             (operator-authored tweets, replies, quote-tweet authorship)
│   ├── tweet-headers.js      (tweet metadata including deletions)
│   ├── direct-messages.js    (1:1 DMs)
│   ├── direct-messages-group.js  (group DMs; absent if none)
│   ├── profile.js            (operator profile metadata — bio, location)
│   ├── tweet_media/          (images, videos, GIFs attached to tweets)
│   ├── direct_messages_media/  (images, videos attached to DMs)
│   └── ... (many other .js files — ignored unless this skill adopts them)
└── assets/                   (frontend bundle — ignored)
```

## `.js` wrapper grammar

Every data file ships as a JSONP-style shim — a single assignment to a global, then a JSON value:

```
window.YTD.<feed>.part<N> = <json>
```

Where:

- `<feed>` is a stable token per file (`account`, `tweet`, `tweet_headers`, `direct_messages`, `direct_messages_group`, `profile`, …).
- `<N>` is the partition index — single-part archives use `0`; very large archives may split into `part0`, `part1`, …
- `<json>` is the post-assignment payload — always an array at the top level for the files this skill reads (`tweet`, `tweet_headers`, `direct_messages`, `direct_messages_group`); an array containing one entry for the single-record files (`account`, `profile`).

The wrapper is fixed and verifiable. The skill MUST verify it before stripping:

```js
const WRAPPER_RE = /^\s*window\.YTD\.([a-z_]+)\.part(\d+)\s*=\s*/;
```

A file whose first non-whitespace bytes do not match `window.YTD.<feed>.part<N> = ` is a defective input. The parser MUST loud-fail with `phase=parse reason="missing window.YTD wrapper on <filename>"` rather than attempt JSON.parse on the raw bytes — the failure mode the task spec calls out is silent zero-tweet imports on unwrapped-JSON input.

## Top-level shapes (read by this skill)

### `data/account.js`

```json
[
  { "account": {
      "accountId": "1234567890",
      "username": "joelsmalley",
      "displayName": "Joel Smalley",
      "createdAt": "2009-...",
      "email": "..."
  } }
]
```

The skill reads `account.accountId` (the X numeric id of the operator), `account.username` (the handle without the `@`), and `account.displayName`. `email` is ignored.

### `data/tweets.js`

```json
[
  { "tweet": {
      "id_str": "1700000000000000000",
      "full_text": "<verbatim tweet text>",
      "created_at": "Wed Jul 03 14:23:01 +0000 2024",
      "in_reply_to_status_id_str": "169...",
      "in_reply_to_screen_name": "adamlangley",
      "entities": {
        "user_mentions": [ { "screen_name": "adamlangley", "name": "...", "id_str": "..." } ],
        "urls": [ { "expanded_url": "https://...", "display_url": "..." } ],
        "media": [ { "media_url_https": "...", "type": "photo|video|animated_gif", "id_str": "..." } ]
      },
      "quoted_status_id_str": "169..."
  } }
]
```

The skill reads the documented fields and ignores everything else. `created_at` is a Twitter-flavoured RFC 2822 string; convert to ISO 8601 with UTC offset before rendering.

### `data/tweet-headers.js`

```json
[
  { "tweet": {
      "tweet_id": "1700000000000000000",
      "user_id": "1234567890",
      "created_at": "Wed Jul 03 14:23:01 +0000 2024"
  } }
]
```

Used for deletion detection: any `tweet.tweet_id` in `tweet-headers.js` with no matching entry in `tweets.js` is a deleted tweet — render it in the transcript as `[deleted on <iso>]`.

### `data/direct-messages.js` and `data/direct-messages-group.js`

```json
[
  { "dmConversation": {
      "sessionId": "1234567890-9876543210",
      "messages": [
        { "messageCreate": {
            "id": "170...",
            "senderId": "1234567890",
            "recipientId": "9876543210",
            "text": "Hello",
            "createdAt": "2024-09-12T14:32:01.000Z",
            "mediaUrls": []
        } },
        { "reactionCreate": { ... } }
      ]
  } }
]
```

For 1:1 DMs the `sessionId` is `<id1>-<id2>` (numeric senderIds joined with `-`, lexicographic order). For group DMs the sessionId is opaque. Only `messageCreate` envelopes are message turns; `reactionCreate`, `joinConversation`, `leaveConversation` are skipped at the normaliser (counted under `systemSkipped`).

## What this skill ignores

- `profile.js` (bio, location) — outside scope; the operator profile is already captured in `:AdminUser`.
- `following.js`, `followers.js`, `mute.js`, `block.js` — relationship graphs flatten cleanly into `:Person` natural-key MERGEs; out of scope for this task.
- All other `.js` files (`like.js`, `bookmark.js`, etc.) — out of scope until a future task adopts them.
