diff --git a/AGENTS.md b/AGENTS.md index 568383d..f549b0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Core product traits right now: Treat these as core constraints unless the owner explicitly changes the product direction. - The app is a single exported client-side shell, not a backend product. -- Artifact payloads live in the URL fragment, using `#agent-render=v1..` for `plain|lz|deflate`, `#agent-render=v1.arx..` for `arx`, and `#agent-render=v1.arx2..` for `arx2`. +- Artifact payloads live in the URL fragment, using the compact `#` form where the single tag char identifies the codec: `p` plain, `l` lz, `d` deflate, `a` arx, `b` arx2. Legacy `#agent-render=v1...` links still decode but are no longer emitted. - The deployed host should not receive artifact contents as part of the initial page request. - Supported artifact kinds are `markdown`, `code`, `diff`, `csv`, and `json`. - Supported codecs are `plain`, `lz`, `deflate`, `arx`, and `arx2`. @@ -83,8 +83,9 @@ Rules: The fragment transport is part of the product surface, not an implementation detail. Current rules: -- fragment key: `agent-render` -- format: `v1..` for `plain|lz|deflate`, `v1.arx..` for `arx`, and `v1.arx2..` for `arx2` +- fragment key: `agent-render` (legacy decode path only; the compact form has no key) +- emitted format: compact `#`, where the single tag char identifies the codec (`p` plain, `l` lz, `d` deflate, `a` arx, `b` arx2); the tag also encodes the active dictionary version for `arx`/`arx2` +- legacy format (still decodable, no longer emitted): `agent-render=v1..` for `plain|lz|deflate`, `agent-render=v1.arx..` for `arx`, and `agent-render=v1.arx2..` for `arx2` - codecs: `plain`, `lz`, `deflate`, `arx`, and `arx2` - fragment size budget: `8192` characters - decoded payload budget: `200000` characters diff --git a/docs/architecture.md b/docs/architecture.md index 043e079..efb6cd2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -97,7 +97,7 @@ The fragment protocol keeps the JSON envelope stable and treats compression stri - `lz` stores compressed JSON via `lz-string` when it produces a smaller fragment - `deflate` stores deflate-compressed UTF-8 JSON bytes when it outperforms other codecs - `arx` applies domain-dictionary substitution, brotli compression (quality 11), and binary-to-text encoding for best-in-class compression. Four wire shapes are candidates: base76 (ASCII, 77 fragment-safe chars), base64url (RFC 4648 `A-Za-z0-9-_` with a `B.` prefix for detection), base1k (Unicode, 1774 chars from U+00A1–U+07FF), and baseBMP (high-density Unicode, ~62k safe BMP code points from U+00A1–U+FFEF, ~15.92 bits/char). The async encoder tries all four and picks the shortest **transport** length (percent-encoded UTF-8 length for non-ASCII), so base64url can win over Unicode encodings on chat-style surfaces. baseBMP produces ~32% fewer characters than base1k and ~55% fewer than base76 for the same compressed bytes, achieving ~70% smaller fragments than deflate on typical payloads (~6.1x compression ratio for 8k markdown). Full pipeline timing is on the order of ~8–14ms for 8k payloads depending on the wire encoding. The substitution dictionary is served as a static file at `/arx-dictionary.json` so agents can fetch it for local compression; a pre-compressed `/arx-dictionary.json.br` variant is also available. The viewer tries the pre-compressed dictionary first on default ARX-family loads, falls back to the JSON file, and only loads external dictionaries when an ARX/ARX2/ARX3 encode or decode path needs them. -- `arx2` keeps the arx compression stack but replaces the JSON envelope with a compact tuple envelope and applies `/arx2-dictionary.json` as an overlay before the shared arx dictionary. The viewer tries `/arx2-dictionary.json.br` first for default overlay loads and falls back to JSON. It uses `v1.arx2..` and decodes back to the standard envelope before validation/rendering. +- `arx2` keeps the arx compression stack but replaces the JSON envelope with a compact tuple envelope and applies `/arx2-dictionary.json` as an overlay before the shared arx dictionary. The viewer tries `/arx2-dictionary.json.br` first for default overlay loads and falls back to JSON. It is emitted with the compact `b` tag (which encodes the dictionary version) and decodes back to the standard envelope before validation/rendering. - `arx3` uses the same tuple envelope, overlay dictionary, shared arx dictionary, and brotli bytes as arx2, then allows the dense baseBMP wire to win by decoded visible character length. This deliberately optimizes copyable visible URL length for trusted Unicode-preserving surfaces; it is not a stronger compressed-byte format than arx2. - packed wire mode (`p: 1`) shortens transport keys before compression, then unpacks back to the standard envelope during decode - automatic async codec selection tries `arx3 -> arx2 -> arx -> deflate -> lz -> plain`; arx compares packed + non-packed candidates, while arx2/arx3 use tuple envelopes diff --git a/docs/payload-format.md b/docs/payload-format.md index ac03d59..8b0ab3b 100644 --- a/docs/payload-format.md +++ b/docs/payload-format.md @@ -9,21 +9,23 @@ Payload contents are untrusted user content. Viewers, agents, and automations sh ## Fragment shape ```text -#agent-render=v1.. (plain | lz | deflate) -#agent-render=v1.arx.. (arx) -#agent-render=v1.arx2.. (arx2) -#agent-render=v1.arx3.. (arx3) +#p (plain) +#l (lz) +#d (deflate) +#a (arx) +#b (arx2) +#c (arx3) ``` -The fragment protocol includes version and codec in the outer format so unsupported formats fail cleanly. Fragment URLs can look long because they carry the artifact payload in the browser-only fragment instead of sending it to the host during the page request. +The compact fragment is a single codec tag char followed by the payload. The tag encodes the codec (and, for `arx`/`arx2`/`arx3`, the active dictionary version) so unsupported formats fail cleanly. The legacy `#agent-render=v1...` form still decodes for back-compatibility but is no longer emitted. Fragment URLs can look long because they carry the artifact payload in the browser-only fragment instead of sending it to the host during the page request. Supported codecs: - `plain` - base64url-encoded JSON - `lz` - `lz-string` compressed JSON encoded for URL-safe transport - `deflate` - deflate-compressed UTF-8 JSON bytes encoded as base64url -- `arx` - domain-dictionary substitution + brotli (quality 11) + binary-to-text encoding. arx fragments include dictionary version metadata in the outer format (`v1.arx..`) so links stay portable across dictionary updates. Four wire shapes are tried and the shortest **transport** size wins (see `computeTransportLength` in `fragment.ts` — non-ASCII Unicode may count longer after percent-encoding): **base76** (ASCII-only, 77 fragment-safe chars), **base64url** (standard RFC 4648 alphabet `A-Za-z0-9-_`, no padding, prefixed with `B.` for detection), **base1k** (Unicode, 1774 chars from U+00A1–U+07FF), and **baseBMP** (high-density Unicode, ~62k safe BMP code points from U+00A1–U+FFEF, ~15.92 bits/char). BaseBMP produces ~32% fewer characters than base1k and ~55% fewer than base76 for the same compressed bytes. BaseBMP payloads are prefixed with a U+FFF0 marker for detection. The viewer’s `arxDecompress` auto-detects the wire shape (including the rare case where a base76 length prefix is also `B.` — it tries base64url first and falls back to base76 if Brotli fails). The substitution dictionary is served at `/arx-dictionary.json` with a pre-compressed `/arx-dictionary.json.br` variant; the viewer tries the `.br` file first on default loads and falls back to JSON. The arx2 overlay dictionary follows the same `.br`-then-JSON default load pattern. -- `arx2` - tuple-envelope transport + arx2 overlay substitution + the shared arx dictionary + brotli (quality 11) + the same four binary-to-text wire shapes. arx2 fragments use `v1.arx2..`, where `dictVersion` is the shared arx dictionary version. Existing `arx` links remain valid; async auto-selection keeps arx2 as the conservative transport-measured tuple codec. +- `arx` - domain-dictionary substitution + brotli (quality 11) + binary-to-text encoding. The compact `a` tag encodes the active dictionary version so links stay portable across dictionary updates. Four wire shapes are tried and the shortest **transport** size wins (see `computeTransportLength` in `fragment.ts` — non-ASCII Unicode may count longer after percent-encoding): **base76** (ASCII-only, 77 fragment-safe chars), **base64url** (standard RFC 4648 alphabet `A-Za-z0-9-_`, no padding, prefixed with `B.` for detection), **base1k** (Unicode, 1774 chars from U+00A1–U+07FF), and **baseBMP** (high-density Unicode, ~62k safe BMP code points from U+00A1–U+FFEF, ~15.92 bits/char). BaseBMP produces ~32% fewer characters than base1k and ~55% fewer than base76 for the same compressed bytes. BaseBMP payloads are prefixed with a U+FFF0 marker for detection. The viewer’s `arxDecompress` auto-detects the wire shape (including the rare case where a base76 length prefix is also `B.` — it tries base64url first and falls back to base76 if Brotli fails). The substitution dictionary is served at `/arx-dictionary.json` with a pre-compressed `/arx-dictionary.json.br` variant; the viewer tries the `.br` file first on default loads and falls back to JSON. The arx2 overlay dictionary follows the same `.br`-then-JSON default load pattern. +- `arx2` - tuple-envelope transport + arx2 overlay substitution + the shared arx dictionary + brotli (quality 11) + the same four binary-to-text wire shapes. The compact `b` tag encodes the shared arx dictionary version. Existing `arx` links remain valid; async auto-selection keeps arx2 as the conservative transport-measured tuple codec. - `arx3` - the same tuple envelope, overlay substitution, shared arx dictionary, and brotli bytes as arx2, with a different selection rule: baseBMP may win by decoded visible character length instead of conservative percent-encoded transport length. This is the compact visible URL mode for trusted surfaces that preserve Unicode fragments. If a platform rewrites, truncates, or previews links aggressively, prefer arx2/base64url or UUID mode instead. The encoder now also supports a packed wire representation (`p: 1`) that shortens key names before compression. Packed mode is transport-only; decoded envelopes normalize back to the standard shape. diff --git a/docs/url-fragments.md b/docs/url-fragments.md index fb5a38d..f8d703a 100644 --- a/docs/url-fragments.md +++ b/docs/url-fragments.md @@ -3,33 +3,37 @@ agent-render links carry the artifact in the URL fragment: ```text -https://agent-render.com/#agent-render=v1.arx3.1. +https://agent-render.com/#c ``` Everything before `#` loads the static app. Everything after `#` is the artifact payload the browser decodes locally. ## What the parts mean -- `agent-render` tells the app this hash belongs to agent-render. -- `v1` is the payload format version. -- `arx3` is the compression codec. -- `1` is the arx dictionary version. +- The first character after `#` is a single codec tag. Here `c` means the `arx3` codec (and its active dictionary version). - `` is the encoded artifact bundle. -Other links may use a shorter shape: +The tag char identifies the codec: ```text -#agent-render=v1.. +#p (plain) +#l (lz) +#d (deflate) +#a (arx) +#b (arx2) +#c (arx3) ``` -where `` is `plain`, `lz`, or `deflate`. ARX-family links include the dictionary version: +For `arx`, `arx2`, and `arx3`, the tag also encodes the active dictionary version. + +Older links may use the legacy shape, which the viewer still decodes: ```text -#agent-render=v1.arx.. -#agent-render=v1.arx2.. -#agent-render=v1.arx3.. +#agent-render=v1.. ``` +where `` is `plain`, `lz`, or `deflate`, and the ARX-family legacy links include the dictionary version (`#agent-render=v1.arx..`, `arx2`, `arx3`). These legacy links are no longer emitted. + ## Why arx exists Artifacts can be bigger than a comfortable URL. The ARX family keeps links shorter by applying agent-render substitution dictionaries, Brotli compression, tuple envelopes for arx2/arx3, and binary-to-text encoding. `arx3` favors compact visible Unicode fragments, so it can look especially strange even though the browser decodes it locally. diff --git a/skills/agent-render-linking/SKILL.md b/skills/agent-render-linking/SKILL.md index 874858d..9d4baaa 100644 --- a/skills/agent-render-linking/SKILL.md +++ b/skills/agent-render-linking/SKILL.md @@ -22,15 +22,23 @@ Source repository: Keep the artifact content in the URL fragment, not in normal query params. -Use this fragment shape: +Use the compact fragment shape `#`, where `` is a single +character identifying the codec: ```text -#agent-render=v1.. (plain | lz | deflate) -#agent-render=v1.arx.. (arx) -#agent-render=v1.arx2.. (arx2) -#agent-render=v1.arx3.. (arx3) +#p (plain) +#l (lz) +#d (deflate) +#a (arx) +#b (arx2) +#c (arx3) ``` +The single tag char encodes the codec (and, for `arx`/`arx2`/`arx3`, the active +dictionary version); the payload follows immediately after it. The legacy +`#agent-render=v1...` form still decodes, but the +viewer no longer emits it — always build the compact form. + Supported codecs: - `plain`: base64url-encoded JSON envelope - `lz`: `lz-string` compressed JSON encoded for URL-safe transport @@ -166,31 +174,33 @@ Set `activeArtifactId` to the artifact that should open first. ## Link construction -Construct the final URL as: +Construct the final URL with the compact `#` fragment: ```text -https://agent-render.com/#agent-render=v1.. (plain | lz | deflate) -https://agent-render.com/#agent-render=v1.arx.. (arx) -https://agent-render.com/#agent-render=v1.arx2.. (arx2) -https://agent-render.com/#agent-render=v1.arx3.. (arx3) +https://agent-render.com/#p (plain) +https://agent-render.com/#l (lz) +https://agent-render.com/#d (deflate) +https://agent-render.com/#a (arx) +https://agent-render.com/#b (arx2) +https://agent-render.com/#c (arx3) ``` For `plain`: 1. Serialize the envelope as compact JSON 2. Base64url-encode it -3. Append it after `v1.plain.` +3. Prepend the tag `p` (the fragment is `#p`) For `lz`: 1. Serialize the envelope as compact JSON 2. Compress with `lz-string` URL-safe encoding -3. Append it after `v1.lz.` +3. Prepend the tag `l` (the fragment is `#l`) For `deflate`: 1. Serialize the envelope as compact JSON (or packed wire form) 2. Encode JSON to UTF-8 bytes 3. Deflate the bytes 4. Base64url-encode the compressed bytes -5. Append it after `v1.deflate.` +5. Prepend the tag `d` (the fragment is `#d`) ## Shared arx dictionaries @@ -214,7 +224,7 @@ To use the dictionary for local `arx` encoding: - Base1k uses 1774 Unicode code points (U+00A1–U+07FF, skipping combining diacriticals and soft hyphen). ~10.79 bits/char - Base64url: standard `A-Za-z0-9-_` (no padding), prefix `B.` — ASCII-only, survives Discord/Slack/Teams-style handling better than Unicode-heavy fragments - Base76 uses 77 ASCII fragment-safe characters. ~6.27 bits/char -5. Prepend `v1.arx..` to form the fragment payload (use the same dictionary version used for substitution) +5. Prepend the tag `a` to form the fragment (the compact tag encodes the active dictionary version, so use the same dictionary version for substitution) The dictionary includes JSON envelope boilerplate patterns, JSON-escaped Markdown syntax, and programming-language patterns that are already present in the shipped corpus. The viewer tries the pre-compressed dictionary first on default ARX/ARX2/ARX3 encode or decode paths, falls back to the JSON file, and falls back again to its built-in table if external fetches fail. @@ -235,9 +245,9 @@ Then apply substitutions in this order: 2. Fetch and apply `https://agent-render.com/arx-dictionary.json` 3. Brotli-compress at quality 11 4. Try baseBMP, base1k, base64url, and base76; choose the shortest transport representation -5. Prepend `v1.arx2..`, using the shared arx dictionary version +5. Prepend the tag `b` (the compact tag encodes the shared arx dictionary version) -For `arx3`, use the same tuple, substitution, and brotli bytes as arx2, then choose the baseBMP wire when the visible character count is the optimization target. Prepend `v1.arx3..`, using the shared arx dictionary version. Do not invent a new dictionary entry unless it is backed by corpus evidence and improves the benchmark gate. +For `arx3`, use the same tuple, substitution, and brotli bytes as arx2, then choose the baseBMP wire when the visible character count is the optimization target. Prepend the tag `c` (the compact tag encodes the shared arx dictionary version). Do not invent a new dictionary entry unless it is backed by corpus evidence and improves the benchmark gate. ## Practical limits diff --git a/skills/selfhosted-agent-render/SKILL.md b/skills/selfhosted-agent-render/SKILL.md index 2e65ffb..31121c4 100644 --- a/skills/selfhosted-agent-render/SKILL.md +++ b/skills/selfhosted-agent-render/SKILL.md @@ -36,7 +36,7 @@ POST /api/artifacts Content-Type: application/json { - "payload": "agent-render=v1.plain." + "payload": "p" } ``` @@ -49,7 +49,7 @@ Response (`201`): } ``` -The `payload` field is the same payload string used in fragment links — the fragment body after `#`. Use the same envelope format and codecs (`plain`, `lz`, `deflate`, `arx`, `arx2`, `arx3`) described in the `agent-render-linking` skill. +The `payload` field is the same payload string used in fragment links — the compact fragment body after `#` (a single codec tag char followed by the payload). Use the same envelope format and codecs (`plain`, `lz`, `deflate`, `arx`, `arx2`, `arx3`) described in the `agent-render-linking` skill. The legacy `agent-render=v1..` form is also accepted for back-compatibility. ### Read an artifact @@ -62,7 +62,7 @@ Response (`200`): ```json { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", - "payload": "agent-render=v1.plain.", + "payload": "p", "created_at": "2025-04-07T12:00:00.000Z", "updated_at": "2025-04-07T12:00:00.000Z", "last_viewed_at": "2025-04-07T14:00:00.000Z", @@ -79,7 +79,7 @@ PUT /api/artifacts/:id Content-Type: application/json { - "payload": "agent-render=v1.plain." + "payload": "p" } ``` @@ -242,13 +242,13 @@ Encode the envelope using the same codec pipeline as fragment links: 1. Serialize envelope as compact JSON 2. Encode with a codec (`plain` = base64url, `lz` = lz-string, `deflate` = deflate + base64url, or the async arx/arx2/arx3 pipelines) -3. Prepend `agent-render=v1..` +3. Prepend the single-character codec tag (`p` plain, `l` lz, `d` deflate, `a` arx, `b` arx2, `c` arx3) 4. POST the resulting string as the `payload` field For simple cases, `plain` codec is sufficient: ```text -agent-render=v1.plain. +p ``` ## TTL behavior @@ -357,7 +357,7 @@ PAYLOAD=$(echo -n '{"v":1,"codec":"plain","artifacts":[{"id":"demo","kind":"mark curl -s -X POST http://localhost:3000/api/artifacts \ -H "Content-Type: application/json" \ - -d "{\"payload\": \"agent-render=v1.plain.$PAYLOAD\"}" + -d "{\"payload\": \"p$PAYLOAD\"}" ``` ## Cleanup guidance diff --git a/src/app/url-explainer/page.tsx b/src/app/url-explainer/page.tsx index 584b7e2..442fc52 100644 --- a/src/app/url-explainer/page.tsx +++ b/src/app/url-explainer/page.tsx @@ -44,7 +44,7 @@ export default function UrlExplainerPage() { Why does this URL look weird?

- The long part after #agent-render= is the artifact itself, compressed into the URL fragment so a static host can show it without receiving the content in the page request. + The long part after the # (which starts with a one-character codec tag) is the artifact itself, compressed into the URL fragment so a static host can show it without receiving the content in the page request.

@@ -52,7 +52,7 @@ export default function UrlExplainerPage() {

The shape

- https://agent-render.com/#agent-render=v1.arx.1.<compressed-payload> + https://agent-render.com/#a<compressed-payload>

Everything before # loads the app. Everything after # stays in the browser and tells the app what to render. diff --git a/src/components/home/sample-link-data.ts b/src/components/home/sample-link-data.ts index 634f556..61b63c4 100644 --- a/src/components/home/sample-link-data.ts +++ b/src/components/home/sample-link-data.ts @@ -14,44 +14,44 @@ export type SampleLinkCard = { export const sampleLinkCards = [ { title: "Maintainer kickoff", - hash: "#agent-render=v1.deflate.VVLbThsxEP2VkXkBiUTiNVVbtRUtUqFCBbUPLBKOPZtY67UtezYhBf69x5sL8LBeey5nzpmZJ5XU7OxUrcbTqJmy3HotrE6V4HWlXRB8nKlzpottC4eGI0dte53wymp296TcO1uHV69zZ-M67JBuUgYUvca0rxnT3sJQqx_R-7gmNOET3S6Z0jD3zlBZsvcU4poCsy2kKXOwnEFQllqoZfaFvOsYrtblIhPjdUFgFtdqI6dIlurT3s-16ShlXjleT2upo0P9MyomJq7GCd093tPvsQztVZF1mY34DbmAykwVg_M--mZIKWahH04uhjl993oVM1u62qeLnnsGq2BxLR0YFyn77J_MibJe08Xt1SUqlRpsab4hTEcPXvaBX6x9wwiHR9tG0K2K1sc1UEdhl3oIZklmyaar1ar5GUQzusKEm2gZCi6_ooDZc_VOJhMY3pzV-GdUupvEMxqj7Qb_c-skZqc9SuTYMxXRm4L2CLq-zfwWLWNCwVT8Q-J1jnYw4v5BYpt178KC1k6W5HVYDHrBAHRpx-h61BUHSQNADxhfM4SC1FZ2DH5T50wwrapVAw3ptQ9HdP6o--R3RNBVH01XfQ8PD3UE_DhOrkW3xEW0lQ1on4cVeyzEMRgueg4ygz4UW5zQUxMIWyhDDpX_6J1CfJbyFzKOGwUNQSbbRf3YqBP6TI3KlXqjaIZ770oBVqM-NOFlZFIJjWtQd-sw4sOum-xKgk7q49xBSx05lKMMKGME8CSdOE_Vy_3Lfw", - fragmentLength: 790, + hash: "#dVVLbThsxEP2VkXkBiUTiNVVbtRUtUqFCBbUPLBKOPZtY67UtezYhBf69x5sL8LBeey5nzpmZJ5XU7OxUrcbTqJmy3HotrE6V4HWlXRB8nKlzpottC4eGI0dte53wymp296TcO1uHV69zZ-M67JBuUgYUvca0rxnT3sJQqx_R-7gmNOET3S6Z0jD3zlBZsvcU4poCsy2kKXOwnEFQllqoZfaFvOsYrtblIhPjdUFgFtdqI6dIlurT3s-16ShlXjleT2upo0P9MyomJq7GCd093tPvsQztVZF1mY34DbmAykwVg_M--mZIKWahH04uhjl993oVM1u62qeLnnsGq2BxLR0YFyn77J_MibJe08Xt1SUqlRpsab4hTEcPXvaBX6x9wwiHR9tG0K2K1sc1UEdhl3oIZklmyaar1ar5GUQzusKEm2gZCi6_ooDZc_VOJhMY3pzV-GdUupvEMxqj7Qb_c-skZqc9SuTYMxXRm4L2CLq-zfwWLWNCwVT8Q-J1jnYw4v5BYpt178KC1k6W5HVYDHrBAHRpx-h61BUHSQNADxhfM4SC1FZ2DH5T50wwrapVAw3ptQ9HdP6o--R3RNBVH01XfQ8PD3UE_DhOrkW3xEW0lQ1on4cVeyzEMRgueg4ygz4UW5zQUxMIWyhDDpX_6J1CfJbyFzKOGwUNQSbbRf3YqBP6TI3KlXqjaIZ770oBVqM-NOFlZFIJjWtQd-sw4sOum-xKgk7q49xBSx05lKMMKGME8CSdOE_Vy_3Lfw", + fragmentLength: 767, kind: "markdown", artifactCount: 1, }, { title: "Viewer bootstrap", - hash: "#agent-render=v1.deflate.bY7BCsIwDIZfJeSksCle5_DoCwhenIe6ZVrs2tJlczD27mYdgqCXQL785PtH9JjtEuzjLDHDimqjmDBBlu2s6UUBbs5xy0F5wUpwH3HaPsgYQQGzy4j69_AUVLrq8-37uuF2EFz_x3MTGrwLDHVnS9bOwtLlNKdWaxgLCxCIu2Ahb5S2h2NQ94Ysp95JjipQgXWtSoZFAFGQb2N4X9hJPEY8s3G6Tm8", - fragmentLength: 247, + hash: "#dbY7BCsIwDIZfJeSksCle5_DoCwhenIe6ZVrs2tJlczD27mYdgqCXQL785PtH9JjtEuzjLDHDimqjmDBBlu2s6UUBbs5xy0F5wUpwH3HaPsgYQQGzy4j69_AUVLrq8-37uuF2EFz_x3MTGrwLDHVnS9bOwtLlNKdWaxgLCxCIu2Ahb5S2h2NQ94Ysp95JjipQgXWtSoZFAFGQb2N4X9hJPEY8s3G6Tm8", + fragmentLength: 224, kind: "code", artifactCount: 1, }, { title: "Phase 1 sample diff", - hash: "#agent-render=v1.deflate.bZHbTsMwDIZfxYqQCuqZDS6KkPoI3FOkhdbtIrK0SrKD1PXdcZNNG2y-qJr49-_488gGVuQR27lvzQrWYCu5RRYxS6ePNTcIORi-GSRCI9qWMpwyA7f1mv41Kz5HJq5ufmYXL5wtNEokk-Scbu_c0SNcCcRxJyzw1Og6XaOUfWINfP85VkqoBg-Q-0iSZx-QZ9nrclmpOI7_OVQqDMMbm7KEOI8WEObRC5QlFeJh6LWFdqtqK3oFnUa0j08wUg5Ao91qBYFzCN7I9L5e8Q0WYKwWqnO1IUDdK2PBct2hhXeYJXA8QrDvtWycl5OdWqxciwgeRl8xrUgBU6VuGO1QG-p8oXS5qJTCPbSC1rbpG1riCY-nl_lIkoUPjy1tcJeqrZTXxK4tZ2ZZlBEzR-xMwI93EtJ8QZbkSTbPRcvd03LNIIVl09f0Cw", - fragmentLength: 462, + hash: "#dbZHbTsMwDIZfxYqQCuqZDS6KkPoI3FOkhdbtIrK0SrKD1PXdcZNNG2y-qJr49-_488gGVuQR27lvzQrWYCu5RRYxS6ePNTcIORi-GSRCI9qWMpwyA7f1mv41Kz5HJq5ufmYXL5wtNEokk-Scbu_c0SNcCcRxJyzw1Og6XaOUfWINfP85VkqoBg-Q-0iSZx-QZ9nrclmpOI7_OVQqDMMbm7KEOI8WEObRC5QlFeJh6LWFdqtqK3oFnUa0j08wUg5Ao91qBYFzCN7I9L5e8Q0WYKwWqnO1IUDdK2PBct2hhXeYJXA8QrDvtWycl5OdWqxciwgeRl8xrUgBU6VuGO1QG-p8oXS5qJTCPbSC1rbpG1riCY-nl_lIkoUPjy1tcJeqrZTXxK4tZ2ZZlBEzR-xMwI93EtJ8QZbkSTbPRcvd03LNIIVl09f0Cw", + fragmentLength: 439, kind: "diff", artifactCount: 1, }, { title: "Data export preview", - hash: "#agent-render=v1.deflate.VY9NasQwDIWvYrR2Ct1m3WV7gqYL1Zaxif-QlcwMw9y9yqRQuhF8T0_S0x06zK8W9md1MIOnkFEILIjSGwoauvbGYjrTnuiiHdROIeHkhhLD_HmH9E9bldzYf7d8nLoZFfuITVQOf_aX03gcR5YU0IldU_V2bKUg35bKDX3BbpVW3y7VLvCOW3XRuEhuzWmI1XipysSE_rbAUo-oxNOIlLN1zZMOybia0TZ2ZE1Olaa6lW9i8sdAR3HR-hSCOs9XzZPM6DmJOYQF4PH1-AE", - fragmentLength: 299, + hash: "#dVY9NasQwDIWvYrR2Ct1m3WV7gqYL1Zaxif-QlcwMw9y9yqRQuhF8T0_S0x06zK8W9md1MIOnkFEILIjSGwoauvbGYjrTnuiiHdROIeHkhhLD_HmH9E9bldzYf7d8nLoZFfuITVQOf_aX03gcR5YU0IldU_V2bKUg35bKDX3BbpVW3y7VLvCOW3XRuEhuzWmI1XipysSE_rbAUo-oxNOIlLN1zZMOybia0TZ2ZE1Olaa6lW9i8sdAR3HR-hSCOs9XzZPM6DmJOYQF4PH1-AE", + fragmentLength: 276, kind: "csv", artifactCount: 1, }, { title: "arx showcase", - hash: "#agent-render=v1.arx3.1.￰¡ທ况㮡猋䘔噑ⶰ瓛ସ妔쓤ㆉ줾岱ꉩ鴍Ⅻ䀇煃쨏鯕ꐦ뚣䨝᭺荕쐖꥚蕲홀抣줶⧂恨㞜굚昒筞嘖곔㊨Ꞡ샱چ踉䘄⯦侽Ҕ柀땗졜鄆劖ᛧ썹ⅶ쑚枣⢢栣床둴偯ł♉깑榉⡦簚ꔁ⢼魶傽캖ừঊﺺ潑筆ڷ㦟瓆ⷔ鍋您擌붨ꋀ䒽蒐훉ϫ찟ﯓ꿂␒駄꜂허Ƥሎ턛鏞믥ڻ꾪㕺ﰢ㏑碖胆숩䕏펦띆⼈鄇䯸讓䪜ᙵ⭺Ṡ鱸癰ͱ쑧㏧ⴳꥁ寀䓗乌仰鿢姝䧌븴䰮⻉䈉䚦왰ɝ紎㍷폺䘶猕哃ࡸꉩ冟坩휢㘙뤔魼砿㩧䒗篐ඇ茗念䢯㖅艧뷗ﵥ敧覍ൿ匒옞閜福탇䈥쬨曪㠓櫚끫㯞祘멡胅㙤⪸騛尰譬拹չ淥ꆃ쁹뭻さ痭憕莼䏬赚櫣ꂛ⨎⌙歇釓Ố戵穠줽閆㚠틋옕鼜훉ᇢꔰ眾㻨搓욱⦈鋂ώ뷐史쨳抃萐죰雇쐷㳻䜖ᕝ∍⃇墫颱ẑ恘歬â逵ᖒᾁ╷ﵨȹ牎㈣উ貂奼쉞茸箋堵ꇏ壊ᡧẄ㴬☿ᮑ孪洮怒躡㹻鸷務Ӎ蒺筒간袨塷⯀⳸얁䎤틣拻Ṭ㪙㩷雋㾭ȡ彰뛠謺㋈嶭㷪ઠꂈڶ넪ﴧ咝宆雗䩍ᛧ༤馶姗爼污晶涙灭ᄴ鲄⾁缛ꚰ䆷쎼䈒홡躮ᄳ絡䦾pଛ취뀓糳觸젳뫜灆ɳ틟᥼趞䠇☔穮䮜ﭑ崒쇏盾휛﬏戡镵䠄ᵫ闦㯦悑䗹榃슌钾䖋彷㺶薢⧷圄餒옹⬌㦑Ê蒻䉽뮹ݿ솰駫還衅픠⨣㆒䮲倎䯧묗ﻧኾ䵣ꈬ旸ニ㭌彚ඤጥ㑯ꘇᏘ椛ꅺ菅ᶫ퐢营탼佉葃닌࡭栫ﳣ⭱숼쯋겢匘割䐡펒喀肨蕢뤇帲쒿嚇क़ꭏஓ䱊蛸螼ዚ領쁎鮴蝰﷮쐊蹱ꥬ䜱䖂﬿쫫訜ሁⓉ䦰ꤊ嶱뿝킷ᯑ芊㨏갾ᛇ茭홷텆酉觾쯩裁߿䶱骯놙糨랰뱋∺쳝⓿掲䳌렊䞡漂嚩夆⹎㍁㞢ᔃ傥鮟땢♯娹リ鹛垣క씥㞸猏鄷䰼聎瞿僞㩡䬄滪䯌稊ゅꆸ憸融픗ⵘ㽆찶憎ᕷ캿櫄쿄몰屎こ攎㑕ꞏ恋趤༠呥લ໾冎鋅닃䰖錣뱋䥏蓢ᛌ她좬増鼿㆑⩅骉혝䦧忥餒팤旎섢Ђ曓ㅦ贮ݠ邆ટ훔䝫湂㊍ߊ罸铟ဖ榽軗ﯞǍ툚㴼彞带꽻鞡薵齥曩⤙꩟럾᪜计墵쾈쥖鷁ﰮ叝앯䧹郖篷칵덒晶癤㱫螃핊ꛩ颔⛼᢯결螲䭛콹䏓䅐뺍릏㖬儸⍭쉥ậ㠟种凟봊篳≚纛⳹ẜ饔續掚䱁㔷籕㐥蒾숳嫲ᗁˊ翇䔶뎮ᗞ现ꅨ鼃㼪ᖫ☗皧ꬍ틈牪ﮟ㱎䨃㫭럋ఖᶠ僂㝰⸨ꚧ냳녡마崚꾨産૫ꪞ㞣鑳ꟺ૧兟㵂ť䮫莲閗䯃⏭뿋阡൧仙㕴乜誴㥅滠켼¬즛펃䁦鬜⣻똢ꗕ躇엑渌鿍攅䛳좥ᠧ黇䪍㠠꒳䳚囮⥿긓릒ﴲহ㷼現⪱돌庙૵掷ﮠ⺬㏴꩘஄ꧪ〨㧓ᢝ鲛溻膳麅殡墂޸밝∇꠨儱뽑庶᯴閔뉌᫬迢䚼娉暫᥾뗶柈ٻ㩶黔㖝ꟓ럭癭ṧ㓈岒墼䨞鈖⼦⿖礇ᾉʑ안擴鶱♾轑窔㠆ꎋ篸≷폪뽵Ұ烻멝䡹⛙픮⻐웃殡ᮄꤒᡞ啾屿⧻댳ḧ闣釀É㢞뷟丸贑ỻ麎꣉넽윞ମ섈좃塼힛贫蝏㭺␓첫䬱厼園⼟튐苽驪힪Ň퐐︐ꗒﱕꦓ癳吹켘훌桡鍎뉝顚练艫ង灤覮浏袟唂繥珆餻ﴹ㕸泵ⱦ姐迳鉡츢撸⢟䱁䠫핏䢢즜聵矸䊯櫋䑸袸솧ꪃ婴埦뽵ᓆ姄ऑ垿꾜刈ᛇ䯔뭲紫Ӥ襡禺㢮恐狚ⷉ᣺툊桾⒥쳱冰厃▋㣿ܛ셹뚞ﷲ栾蠋뜺㕣ꉣᛋ封䣸矴耀咆┎喯ꇗ喏ꎸ棾⦉䵐Ӣ怨골﫿ѵ姸滮횳颮匶怪놟鳭놫柫贊쎖ℕ㼰ꖂ䖽룲ﴬ㍱캠ﱫ鵃쾒Ẃ觋跻Ä닑婢锟Њꀯ׼䒨ꦅ隣㮇䋓ࢀᣥ㭦霐䴣⺪甘﬽ꈞ㭩㇘灥夀鹷턨꡹掾俩抂藋濯㹴糞쑔鉶⣷ḽ꿖风繰﷗唥蘏㔉崹⦜嘃៯ᒖ鹿쇉핗晄喝獡뎉ꡪ諦峫㔗觠␷홳翭㟍೼ⰹ嫶ﳺ쥞䋠䕾텄눎娳”Є䀾傗겱矡ꇳ튏㻁⳸粱ﯓ씂떴⥪ꔝ㧊癶뙏뾤碓탨㣪쐫䖯슸轝ت跃熲빗泮㤋ꄰꁋⓚ㽾焁畂팵恥⤯謎䋕蟬磺髏ⅽ䌊徳ㄆ广⡇筨顯㲽珃鈆餋⌁갖悞՚켶␄녌給鿦씨ꂋ鯢녤襡⵷갑䠱迫ዕ䯵◮䧛ꐠ䣯큪䯠纫⍍孓䘪㥘墳ᢡ돆ꅘ㜇僤ふ夺풂몮旽ኈ遷挱팊₠搘铙鷜激翬噊얆輹跞丰橚ᓧ豃Ễꤣ䢙ந៶⦖睾鲴ﺟࡁ꽢孋僩즥ꧧ첟⸁猨崂ᖪ׳䭿ᓕ腔䉤挧쫗멚茳ꢡ龀ꈱᆖ挩䲖ᆪ潴聪륎痧振蕠఍ᾥ氶뾫茹⩐艬⸑前ﮏ鞁춍࿤硻။퓝䦒䭏䫆ꆁ㟶ꫝ㏀䫋샚匾쏄鰊ـ술ꋂ楲삈摇艅횓盧꒦♕ꂝ렚樭ˊჼ寧↮ꞻ蔃᪓⤎턑⥗ㇳຉ摶⟍ō䊦莝პ鼅颶淞餢偠뽃灻襸첁⴦龂赸頉㓲닄곝更턍ⲋ⸽茿獥엻", - fragmentLength: 1487, + hash: "#c￰¡ທ况㮡猋䘔噑ⶰ瓛ସ妔쓤ㆉ줾岱ꉩ鴍Ⅻ䀇煃쨏鯕ꐦ뚣䨝᭺荕쐖꥚蕲홀抣줶⧂恨㞜굚昒筞嘖곔㊨Ꞡ샱چ踉䘄⯦侽Ҕ柀땗졜鄆劖ᛧ썹ⅶ쑚枣⢢栣床둴偯ł♉깑榉⡦簚ꔁ⢼魶傽캖ừঊﺺ潑筆ڷ㦟瓆ⷔ鍋您擌붨ꋀ䒽蒐훉ϫ찟ﯓ꿂␒駄꜂허Ƥሎ턛鏞믥ڻ꾪㕺ﰢ㏑碖胆숩䕏펦띆⼈鄇䯸讓䪜ᙵ⭺Ṡ鱸癰ͱ쑧㏧ⴳꥁ寀䓗乌仰鿢姝䧌븴䰮⻉䈉䚦왰ɝ紎㍷폺䘶猕哃ࡸꉩ冟坩휢㘙뤔魼砿㩧䒗篐ඇ茗念䢯㖅艧뷗ﵥ敧覍ൿ匒옞閜福탇䈥쬨曪㠓櫚끫㯞祘멡胅㙤⪸騛尰譬拹չ淥ꆃ쁹뭻さ痭憕莼䏬赚櫣ꂛ⨎⌙歇釓Ố戵穠줽閆㚠틋옕鼜훉ᇢꔰ眾㻨搓욱⦈鋂ώ뷐史쨳抃萐죰雇쐷㳻䜖ᕝ∍⃇墫颱ẑ恘歬â逵ᖒᾁ╷ﵨȹ牎㈣উ貂奼쉞茸箋堵ꇏ壊ᡧẄ㴬☿ᮑ孪洮怒躡㹻鸷務Ӎ蒺筒간袨塷⯀⳸얁䎤틣拻Ṭ㪙㩷雋㾭ȡ彰뛠謺㋈嶭㷪ઠꂈڶ넪ﴧ咝宆雗䩍ᛧ༤馶姗爼污晶涙灭ᄴ鲄⾁缛ꚰ䆷쎼䈒홡躮ᄳ絡䦾pଛ취뀓糳觸젳뫜灆ɳ틟᥼趞䠇☔穮䮜ﭑ崒쇏盾휛﬏戡镵䠄ᵫ闦㯦悑䗹榃슌钾䖋彷㺶薢⧷圄餒옹⬌㦑Ê蒻䉽뮹ݿ솰駫還衅픠⨣㆒䮲倎䯧묗ﻧኾ䵣ꈬ旸ニ㭌彚ඤጥ㑯ꘇᏘ椛ꅺ菅ᶫ퐢营탼佉葃닌࡭栫ﳣ⭱숼쯋겢匘割䐡펒喀肨蕢뤇帲쒿嚇क़ꭏஓ䱊蛸螼ዚ領쁎鮴蝰﷮쐊蹱ꥬ䜱䖂﬿쫫訜ሁⓉ䦰ꤊ嶱뿝킷ᯑ芊㨏갾ᛇ茭홷텆酉觾쯩裁߿䶱骯놙糨랰뱋∺쳝⓿掲䳌렊䞡漂嚩夆⹎㍁㞢ᔃ傥鮟땢♯娹リ鹛垣క씥㞸猏鄷䰼聎瞿僞㩡䬄滪䯌稊ゅꆸ憸融픗ⵘ㽆찶憎ᕷ캿櫄쿄몰屎こ攎㑕ꞏ恋趤༠呥લ໾冎鋅닃䰖錣뱋䥏蓢ᛌ她좬増鼿㆑⩅骉혝䦧忥餒팤旎섢Ђ曓ㅦ贮ݠ邆ટ훔䝫湂㊍ߊ罸铟ဖ榽軗ﯞǍ툚㴼彞带꽻鞡薵齥曩⤙꩟럾᪜计墵쾈쥖鷁ﰮ叝앯䧹郖篷칵덒晶癤㱫螃핊ꛩ颔⛼᢯결螲䭛콹䏓䅐뺍릏㖬儸⍭쉥ậ㠟种凟봊篳≚纛⳹ẜ饔續掚䱁㔷籕㐥蒾숳嫲ᗁˊ翇䔶뎮ᗞ现ꅨ鼃㼪ᖫ☗皧ꬍ틈牪ﮟ㱎䨃㫭럋ఖᶠ僂㝰⸨ꚧ냳녡마崚꾨産૫ꪞ㞣鑳ꟺ૧兟㵂ť䮫莲閗䯃⏭뿋阡൧仙㕴乜誴㥅滠켼¬즛펃䁦鬜⣻똢ꗕ躇엑渌鿍攅䛳좥ᠧ黇䪍㠠꒳䳚囮⥿긓릒ﴲহ㷼現⪱돌庙૵掷ﮠ⺬㏴꩘஄ꧪ〨㧓ᢝ鲛溻膳麅殡墂޸밝∇꠨儱뽑庶᯴閔뉌᫬迢䚼娉暫᥾뗶柈ٻ㩶黔㖝ꟓ럭癭ṧ㓈岒墼䨞鈖⼦⿖礇ᾉʑ안擴鶱♾轑窔㠆ꎋ篸≷폪뽵Ұ烻멝䡹⛙픮⻐웃殡ᮄꤒᡞ啾屿⧻댳ḧ闣釀É㢞뷟丸贑ỻ麎꣉넽윞ମ섈좃塼힛贫蝏㭺␓첫䬱厼園⼟튐苽驪힪Ň퐐︐ꗒﱕꦓ癳吹켘훌桡鍎뉝顚练艫ង灤覮浏袟唂繥珆餻ﴹ㕸泵ⱦ姐迳鉡츢撸⢟䱁䠫핏䢢즜聵矸䊯櫋䑸袸솧ꪃ婴埦뽵ᓆ姄ऑ垿꾜刈ᛇ䯔뭲紫Ӥ襡禺㢮恐狚ⷉ᣺툊桾⒥쳱冰厃▋㣿ܛ셹뚞ﷲ栾蠋뜺㕣ꉣᛋ封䣸矴耀咆┎喯ꇗ喏ꎸ棾⦉䵐Ӣ怨골﫿ѵ姸滮횳颮匶怪놟鳭놫柫贊쎖ℕ㼰ꖂ䖽룲ﴬ㍱캠ﱫ鵃쾒Ẃ觋跻Ä닑婢锟Њꀯ׼䒨ꦅ隣㮇䋓ࢀᣥ㭦霐䴣⺪甘﬽ꈞ㭩㇘灥夀鹷턨꡹掾俩抂藋濯㹴糞쑔鉶⣷ḽ꿖风繰﷗唥蘏㔉崹⦜嘃៯ᒖ鹿쇉핗晄喝獡뎉ꡪ諦峫㔗觠␷홳翭㟍೼ⰹ嫶ﳺ쥞䋠䕾텄눎娳”Є䀾傗겱矡ꇳ튏㻁⳸粱ﯓ씂떴⥪ꔝ㧊癶뙏뾤碓탨㣪쐫䖯슸轝ت跃熲빗泮㤋ꄰꁋⓚ㽾焁畂팵恥⤯謎䋕蟬磺髏ⅽ䌊徳ㄆ广⡇筨顯㲽珃鈆餋⌁갖悞՚켶␄녌給鿦씨ꂋ鯢녤襡⵷갑䠱迫ዕ䯵◮䧛ꐠ䣯큪䯠纫⍍孓䘪㥘墳ᢡ돆ꅘ㜇僤ふ夺풂몮旽ኈ遷挱팊₠搘铙鷜激翬噊얆輹跞丰橚ᓧ豃Ễꤣ䢙ந៶⦖睾鲴ﺟࡁ꽢孋僩즥ꧧ첟⸁猨崂ᖪ׳䭿ᓕ腔䉤挧쫗멚茳ꢡ龀ꈱᆖ挩䲖ᆪ潴聪륎痧振蕠఍ᾥ氶뾫茹⩐艬⸑前ﮏ鞁춍࿤硻။퓝䦒䭏䫆ꆁ㟶ꫝ㏀䫋샚匾쏄鰊ـ술ꋂ楲삈摇艅횓盧꒦♕ꂝ렚樭ˊჼ寧↮ꞻ蔃᪓⤎턑⥗ㇳຉ摶⟍ō䊦莝პ鼅颶淞餢偠뽃灻襸첁⴦龂赸頉㓲닄곝更턍ⲋ⸽茿獥엻", + fragmentLength: 1465, kind: "json", artifactCount: 5, description: "Tuple compression, Brotli, and visible-length Unicode encoding compress 5 rich artifacts into a single URL fragment.", }, { title: "Malformed manifest", - hash: "#agent-render=v1.deflate.bY69DoJAEIRfhUyNBNor7X0Cz2KVPYPAHdk7TQzh3V1-ChJpNpn5diYzYoCpcnyW-4BBza6jxMiRVF2oc0F6rrOefOM4JgWk4C6hZX_auQJzHdEcslbdVwx-az0vD_tK9x8rtsA8arQ-yyyEO6bIFkZFWVRFaZGvKAn5OARJK3RCz569qo0LU_2dWZI3q4fpNv0A", - fragmentLength: 220, + hash: "#dbY69DoJAEIRfhUyNBNor7X0Cz2KVPYPAHdk7TQzh3V1-ChJpNpn5diYzYoCpcnyW-4BBza6jxMiRVF2oc0F6rrOefOM4JgWk4C6hZX_auQJzHdEcslbdVwx-az0vD_tK9x8rtsA8arQ-yyyEO6bIFkZFWVRFaZGvKAn5OARJK3RCz569qo0LU_2dWZI3q4fpNv0A", + fragmentLength: 197, kind: "json", artifactCount: 1, }, diff --git a/src/components/viewer-shell.tsx b/src/components/viewer-shell.tsx index 12b04fc..122b44a 100644 --- a/src/components/viewer-shell.tsx +++ b/src/components/viewer-shell.tsx @@ -5,7 +5,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties } from "react"; import { MAX_FRAGMENT_LENGTH, - PAYLOAD_FRAGMENT_KEY, artifactKinds, type ArtifactPayload, type ParsedPayload, @@ -157,7 +156,7 @@ function getArtifactById(envelope: PayloadEnvelope, artifactId: string | null): function getHashPreview(hash: string): string { if (!hash) { - return `#${PAYLOAD_FRAGMENT_KEY}=v1.plain.`; + return "#d"; } if (hash.length <= 220) { @@ -467,7 +466,7 @@ export function ViewerShell() {

Protocol shape

- #{PAYLOAD_FRAGMENT_KEY}=v1.<codec>.<payload> + #<codec-tag><payload>

`; + return "#d"; } if (hash.length <= 220) { diff --git a/src/lib/payload/fragment-arx.ts b/src/lib/payload/fragment-arx.ts index 0683c50..91de2d3 100644 --- a/src/lib/payload/fragment-arx.ts +++ b/src/lib/payload/fragment-arx.ts @@ -6,7 +6,6 @@ import { arx3DecompressEnvelope, arxCompressPayloads, arxDecompress, - getActiveDictVersion, isExternalArx2OverlayDictionaryLoaded, isExternalDictionaryLoaded, loadArxDictionary, @@ -14,7 +13,7 @@ import { } from "@/lib/payload/arx-codec"; import { packEnvelope } from "@/lib/payload/wire-format"; import { - PAYLOAD_FRAGMENT_KEY, + compactTagForCodec, type PayloadCodec, type PayloadEnvelope, } from "@/lib/payload/schema"; @@ -122,10 +121,9 @@ export async function buildArxCandidates( const json = JSON.stringify( packed ? packEnvelope(payloadEnvelope) : payloadEnvelope, ); - const dictVersion = getActiveDictVersion(); const payloads = await arxCompressPayloads(json); const makeCandidate = (payload: string): CandidateFragment => { - const value = `${PAYLOAD_FRAGMENT_KEY}=v1.arx.${dictVersion}.${payload}`; + const value = `${compactTagForCodec("arx")}${payload}`; return { value, codec: "arx", @@ -151,10 +149,9 @@ export async function buildArx2Candidates( await ensureArx2DictionariesLoaded(); const payloadEnvelope = { ...envelope, codec: "arx2" as PayloadCodec }; - const dictVersion = getActiveDictVersion(); const payloads = await arx2CompressEnvelope(payloadEnvelope); const makeCandidate = (payload: string): CandidateFragment => { - const value = `${PAYLOAD_FRAGMENT_KEY}=v1.arx2.${dictVersion}.${payload}`; + const value = `${compactTagForCodec("arx2")}${payload}`; return { value, codec: "arx2", @@ -198,10 +195,9 @@ export async function buildArx3Candidates( await ensureArx2DictionariesLoaded(); const payloadEnvelope = { ...envelope, codec: "arx3" as PayloadCodec }; - const dictVersion = getActiveDictVersion(); const payloads = await arx3CompressEnvelope(payloadEnvelope); const makeCandidate = (payload: string, preferVisibleChars = false): CandidateFragment => { - const value = `${PAYLOAD_FRAGMENT_KEY}=v1.arx3.${dictVersion}.${payload}`; + const value = `${compactTagForCodec("arx3")}${payload}`; return { value, codec: "arx3", diff --git a/src/lib/payload/fragment.ts b/src/lib/payload/fragment.ts index 109ceb7..4d20c86 100644 --- a/src/lib/payload/fragment.ts +++ b/src/lib/payload/fragment.ts @@ -4,6 +4,8 @@ import { normalizeEnvelope } from "@/lib/payload/envelope"; import { packEnvelope, unpackEnvelope } from "@/lib/payload/wire-format"; import { codecs, + codecForCompactTag, + compactTagForCodec, MAX_DECODED_PAYLOAD_LENGTH, MAX_FRAGMENT_LENGTH, PAYLOAD_FRAGMENT_KEY, @@ -152,7 +154,7 @@ function decodePayload(encoded: string, codec: PayloadCodec): string | null { function buildFragment(envelope: PayloadEnvelope, codec: PayloadCodec, packed: boolean): CandidateFragment { const payloadEnvelope = { ...envelope, codec }; const json = JSON.stringify(packed ? packEnvelope(payloadEnvelope) : payloadEnvelope); - const value = `${PAYLOAD_FRAGMENT_KEY}=v1.${codec}.${encodePayload(json, codec)}`; + const value = `${compactTagForCodec(codec)}${encodePayload(json, codec)}`; // Non-ARX codecs produce ASCII-only output, so transport length equals string length. return { value, codec, packed, transportLength: value.length }; } @@ -289,8 +291,8 @@ function selectCandidate(candidates: CandidateFragment[], budget?: number): Cand } /** - * Encodes a payload envelope into the fragment body expected after `#`, for example - * `agent-render=v1.deflate.`. + * Encodes a payload envelope into the fragment body expected after `#`. The body uses the compact + * header — a single codec tag char followed by the payload (e.g. `d` for deflate). * * The sync encoder supports `plain`, `lz`, and `deflate` codecs. If options explicitly * require an arx-family codec, this function throws because ARX compression is async-only. @@ -307,9 +309,9 @@ export function encodeEnvelope(envelope: PayloadEnvelope, options: EncodeOptions /** * Async variant of {@link encodeEnvelope} that also supports ARX candidates. * - * Returns the fragment body string expected after `#`, for example - * `agent-render=v1.arx..` for ARX codecs, or - * `agent-render=v1..` for `plain|lz|deflate`. + * Returns the compact fragment body expected after `#`: a single codec tag char followed by the + * payload (e.g. `c` for arx3). The legacy `agent-render=v1..` form is still + * accepted on decode for back-compat. * * Like the sync version, candidate encodings are generated across enabled codecs and packed * transport variants, then the smallest transport representation is selected (or the smallest @@ -343,11 +345,30 @@ function parseFragmentHeader(hash: string, options?: DecodeOptions): ParsedFragm return { ok: false, errorResponse: { ok: false, code: "too-large", message: `This payload exceeds the supported fragment budget of ${MAX_FRAGMENT_LENGTH.toLocaleString()} characters.` } }; } - const separatorIndex = fragment.indexOf("="); - const key = separatorIndex === -1 ? fragment : fragment.slice(0, separatorIndex); - const value = separatorIndex === -1 ? "" : fragment.slice(separatorIndex + 1); + // Legacy header: `agent-render=v1..` (arx adds a `.` segment). + if (fragment.startsWith(`${PAYLOAD_FRAGMENT_KEY}=`)) { + return parseLegacyFragmentHeader(fragment, fragmentLength); + } + + // Compact header: a single tag char encodes (version, codec, dictVersion); the payload follows. + // The two forms stay unambiguous for a positional reason, not an alphabet one. A payload byte can + // be `=` (base76 uses it as its extended-length marker), so the wire alphabets do not exclude `=`. + // But the compact tag occupies fragment[0], so any payload `=` can only land at fragment[1] or + // later — never at the offset where the legacy `agent-render=` literal places its `=`. A fragment + // that begins with that literal is caught by the legacy branch above; reaching here means it did + // not, so the leading char is a codec tag. + const codec = codecForCompactTag(fragment.charAt(0)); + if (!codec || fragment.length < 2) { + return { ok: false, errorResponse: { ok: false, code: "invalid-format", message: `The fragment format is invalid. Expected #${PAYLOAD_FRAGMENT_KEY}=... or a codec tag followed by a payload.` } }; + } + + return { ok: true, fragment, version: "v1", codec, encoded: fragment.slice(1), fragmentLength }; +} + +function parseLegacyFragmentHeader(fragment: string, fragmentLength: number): ParsedFragmentHeader { + const value = fragment.slice(fragment.indexOf("=") + 1); - if (key !== PAYLOAD_FRAGMENT_KEY || !value) { + if (!value) { return { ok: false, errorResponse: { ok: false, code: "missing-key", message: `Expected a fragment starting with #${PAYLOAD_FRAGMENT_KEY}=...` } }; } @@ -376,8 +397,8 @@ function parseFragmentHeader(hash: string, options?: DecodeOptions): ParsedFragm /** * Decodes a fragment string (with or without leading `#`) into a parsed payload result. * - * Expected fragment format is `#agent-render=v1..`. Validation first enforces - * fragment-key and shape checks and the global visible fragment size budget. On failure, returns a + * Accepts the compact `#` form or the legacy `#agent-render=v1..` + * form. Validation first enforces header and shape checks and the global visible fragment size budget. On failure, returns a * structured `ParsedPayload` error (`empty`, `missing-key`, `invalid-format`, `too-large`, * `invalid-json`, `decoded-too-large`, or `invalid-envelope`) instead of throwing. * @@ -458,9 +479,9 @@ function resolveEnvelope(parsed: unknown, rawLength: number): ParsedPayload { /** * Async fragment decoder that supports all codecs, including the ARX family. * - * Accepted input follows `#agent-render=v1...` where non-ARX payloads use - * `v1..`, and ARX codecs support versioned dictionary payloads (plus legacy arx - * fallback forms). This decoder applies the same header and visible fragment-size checks as the + * Accepts the compact `#` form or the legacy `#agent-render=v1...` form; in the + * legacy form ARX codecs additionally carry a versioned dictionary segment. This decoder applies the + * same header and visible fragment-size checks as the * sync path, then enforces decoded payload size limits before JSON parsing and envelope validation. * * Pass `{ skipFragmentBudget: true }` to bypass the visible fragment size check, diff --git a/src/lib/payload/link-creator.ts b/src/lib/payload/link-creator.ts index 504bbf5..9c5698d 100644 --- a/src/lib/payload/link-creator.ts +++ b/src/lib/payload/link-creator.ts @@ -1,6 +1,7 @@ import { normalizeEnvelope } from "@/lib/payload/envelope"; import { encodeEnvelope, encodeEnvelopeAsync, getVisibleFragmentLength } from "@/lib/payload/fragment"; import { + codecForCompactTag, codecs, MAX_FRAGMENT_LENGTH, PAYLOAD_FRAGMENT_KEY, @@ -97,18 +98,20 @@ function buildArtifact(draft: LinkCreatorDraft): ArtifactPayload { } function getFragmentCodec(fragmentBody: string): PayloadCodec { - const prefix = `${PAYLOAD_FRAGMENT_KEY}=v1.`; - if (!fragmentBody.startsWith(prefix)) { - return "plain"; + // Legacy header: agent-render=v1.. + const legacyPrefix = `${PAYLOAD_FRAGMENT_KEY}=v1.`; + if (fragmentBody.startsWith(legacyPrefix)) { + const codecEnd = fragmentBody.indexOf(".", legacyPrefix.length); + if (codecEnd === -1) { + return "plain"; + } + + const codec = fragmentBody.slice(legacyPrefix.length, codecEnd); + return supportedCodecSet.has(codec) ? (codec as PayloadCodec) : "plain"; } - const codecEnd = fragmentBody.indexOf(".", prefix.length); - if (codecEnd === -1) { - return "plain"; - } - - const codec = fragmentBody.slice(prefix.length, codecEnd); - return supportedCodecSet.has(codec) ? (codec as PayloadCodec) : "plain"; + // Compact header: a single leading tag char encodes the codec. + return codecForCompactTag(fragmentBody.charAt(0)) ?? "plain"; } /** @@ -135,7 +138,7 @@ export function createDraftEnvelope(draft: LinkCreatorDraft): PayloadEnvelope { * * Throws when draft content is empty, envelope normalization fails, or the generated fragment * exceeds `MAX_FRAGMENT_LENGTH`. The returned object always includes: - * - `hash`: `#agent-render=v1...` fragment string + * - `hash`: compact `#` fragment string (single codec tag char + payload) * - `url`: either the hash-only URL or `baseUrl` with hash attached * - `codec`: the selected wire codec in the generated fragment * - `fragmentLength`: character count excluding the leading `#` diff --git a/src/lib/payload/schema.ts b/src/lib/payload/schema.ts index c16bd5d..89cde10 100644 --- a/src/lib/payload/schema.ts +++ b/src/lib/payload/schema.ts @@ -8,6 +8,35 @@ export const codecs = ["plain", "lz", "deflate", "arx", "arx2", "arx3"] as const export type ArtifactKind = (typeof artifactKinds)[number]; export type PayloadCodec = (typeof codecs)[number]; +// Compact fragment header: a single URL-unreserved tag char replaces the legacy +// `agent-render=v1...` prefix. The tag encodes (wire version 1, codec, and +// dictVersion); arx-family tags imply dictVersion 1. The payload stays self-describing (base64url +// `B.` prefix, baseBMP U+FFF0 marker, base76/base1k length prefix), so the alphabet is not in the +// header. Tags come from the RFC-3986 unreserved set so they never percent-escape, and none can +// begin the legacy `agent-render=` literal, which keeps the two header forms unambiguous on decode. +export const compactCodecTags = { + plain: "p", + lz: "l", + deflate: "d", + arx: "a", + arx2: "b", + arx3: "c", +} as const satisfies Record; + +const compactTagToCodec = new Map( + (Object.entries(compactCodecTags) as [PayloadCodec, string][]).map(([codec, tag]) => [tag, codec]), +); + +/** Returns the single-character compact header tag for a codec. */ +export function compactTagForCodec(codec: PayloadCodec): string { + return compactCodecTags[codec]; +} + +/** Resolves a compact header tag char back to its codec, or null when the tag is unknown. */ +export function codecForCompactTag(tag: string): PayloadCodec | null { + return compactTagToCodec.get(tag) ?? null; +} + type BaseArtifact = { id: string; kind: ArtifactKind; diff --git a/tests/arx-codec.test.ts b/tests/arx-codec.test.ts index 4e3a555..971392c 100644 --- a/tests/arx-codec.test.ts +++ b/tests/arx-codec.test.ts @@ -33,7 +33,7 @@ import { loadArx2OverlayDictionarySync, } from "@/lib/payload/arx-codec"; import { encodeEnvelopeAsync, decodeFragmentAsync } from "@/lib/payload/fragment"; -import { codecs, MAX_DECODED_PAYLOAD_LENGTH, MAX_FRAGMENT_LENGTH, type PayloadEnvelope } from "@/lib/payload/schema"; +import { codecs, compactTagForCodec, MAX_DECODED_PAYLOAD_LENGTH, MAX_FRAGMENT_LENGTH, type PayloadEnvelope } from "@/lib/payload/schema"; describe("base76 encoding", () => { it("round-trips empty input", () => { @@ -329,7 +329,7 @@ describe("arx fragment round-trip", () => { it("round-trips an envelope through arx codec", async () => { const hash = `#${await encodeEnvelopeAsync(envelope, { codec: "arx" })}`; - expect(hash).toContain(`v1.arx.${getActiveDictVersion()}.`); + expect(hash.startsWith(`#${compactTagForCodec("arx")}`)).toBe(true); const parsed = await decodeFragmentAsync(hash); expect(parsed.ok).toBe(true); @@ -374,7 +374,8 @@ describe("arx fragment round-trip", () => { }; const autoHash = await encodeEnvelopeAsync(bigEnvelope); - expect(autoHash).toMatch(new RegExp(`v1\\.arx(?:2|3)?\\.${getActiveDictVersion()}\\.`)); + const arxTags = [compactTagForCodec("arx"), compactTagForCodec("arx2"), compactTagForCodec("arx3")]; + expect(arxTags).toContain(autoHash.charAt(0)); }); it("async arx selection can choose the chat-safe base64url wire form", async () => { @@ -395,7 +396,7 @@ describe("arx fragment round-trip", () => { }; const autoHash = await encodeEnvelopeAsync(bigEnvelope, { codec: "arx" }); - expect(autoHash).toContain(`v1.arx.${getActiveDictVersion()}.B.`); + expect(autoHash.startsWith(`${compactTagForCodec("arx")}B.`)).toBe(true); }); @@ -469,7 +470,7 @@ describe("arx2 tuple envelope", () => { artifacts: [bundle.artifacts[0]], }; const hash = `#${await encodeEnvelopeAsync(single, { codec: "arx2" })}`; - expect(hash).toContain(`v1.arx2.${getActiveDictVersion()}.`); + expect(hash.startsWith(`#${compactTagForCodec("arx2")}`)).toBe(true); const parsed = await decodeFragmentAsync(hash); expect(parsed.ok).toBe(true); @@ -840,7 +841,7 @@ describe("arx3 compact tuple envelope", () => { it("round-trips bundles through arx3 fragments", async () => { const hash = `#${await encodeEnvelopeAsync(bundle, { codec: "arx3" })}`; - expect(hash).toContain(`v1.arx3.${getActiveDictVersion()}.`); + expect(hash.startsWith(`#${compactTagForCodec("arx3")}`)).toBe(true); const parsed = await decodeFragmentAsync(hash); expect(parsed.ok).toBe(true); @@ -876,9 +877,10 @@ describe("arx3 compact tuple envelope", () => { const fragment = await encodeEnvelopeAsync(targetEnvelope, { codec: "arx3" }); const url = `https://agent-render.com/#${fragment}`; - const payload = fragment.split(`v1.arx3.${getActiveDictVersion()}.`)[1] ?? ""; + const arx3Tag = compactTagForCodec("arx3"); + const payload = fragment.startsWith(arx3Tag) ? fragment.slice(arx3Tag.length) : ""; - expect(fragment).toContain(`v1.arx3.${getActiveDictVersion()}.`); + expect(fragment.startsWith(arx3Tag)).toBe(true); expect(isBaseBMPEncoded(payload)).toBe(true); expect(url.length).toBeLessThan(1900); diff --git a/tests/compact-header.test.ts b/tests/compact-header.test.ts new file mode 100644 index 0000000..2d09167 --- /dev/null +++ b/tests/compact-header.test.ts @@ -0,0 +1,79 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import arx2DictionaryJson from "../public/arx2-dictionary.json"; +import arxDictionaryJson from "../public/arx-dictionary.json"; +import { loadArx2OverlayDictionarySync, loadArxDictionarySync } from "@/lib/payload/arx-codec"; +import { decodeFragment, decodeFragmentAsync, encodeEnvelope, encodeEnvelopeAsync } from "@/lib/payload/fragment"; +import { compactCodecTags, type PayloadEnvelope } from "@/lib/payload/schema"; + +const envelope: PayloadEnvelope = { + v: 1, + codec: "plain", + title: "Compact header", + activeArtifactId: "doc", + artifacts: [{ id: "doc", kind: "markdown", filename: "doc.md", content: "# Compact\n\nShort and sweet." }], +}; + +function markdownContent(parsed: Extract, { ok: true }>): string { + const artifact = parsed.envelope.artifacts[0]; + return artifact.kind === "markdown" ? artifact.content : ""; +} + +describe("compact fragment header", () => { + beforeAll(() => { + loadArxDictionarySync(arxDictionaryJson); + loadArx2OverlayDictionarySync(arx2DictionaryJson); + }); + + // Characterization: the tag table is the wire contract. If this changes, update the doc-comment in + // schema.ts AND keep the legacy decode path together — a tag remap silently mis-decodes old links. + it("pins the codec -> tag table", () => { + expect(compactCodecTags).toEqual({ + plain: "p", + lz: "l", + deflate: "d", + arx: "a", + arx2: "b", + arx3: "c", + }); + }); + + it("emits a single-char header and drops the legacy key, version, and codec name", () => { + const fragment = encodeEnvelope(envelope, { codec: "deflate" }); + expect(fragment.startsWith("d")).toBe(true); + expect(fragment).not.toContain("agent-render="); + expect(fragment).not.toContain("v1."); + }); + + it("round-trips every codec through the compact header", async () => { + for (const codec of ["plain", "lz", "deflate"] as const) { + const parsed = decodeFragment(`#${encodeEnvelope(envelope, { codec })}`); + expect(parsed.ok).toBe(true); + if (parsed.ok) expect(markdownContent(parsed)).toBe(envelope.artifacts[0].kind === "markdown" ? envelope.artifacts[0].content : ""); + } + for (const codec of ["arx", "arx2", "arx3"] as const) { + const parsed = await decodeFragmentAsync(`#${await encodeEnvelopeAsync(envelope, { codec })}`); + expect(parsed.ok).toBe(true); + if (parsed.ok) expect(markdownContent(parsed)).toBe(envelope.artifacts[0].kind === "markdown" ? envelope.artifacts[0].content : ""); + } + }); + + it("still decodes legacy agent-render=v1... fragments (back-compat)", () => { + // Reconstruct a real legacy deflate fragment (same payload, legacy prefix) and decode it. + const legacy = `#agent-render=v1.deflate.${encodeEnvelope(envelope, { codec: "deflate" }).slice(1)}`; + const parsed = decodeFragment(legacy); + expect(parsed.ok).toBe(true); + if (parsed.ok) expect(parsed.envelope.title).toBe("Compact header"); + }); + + it("saves the full legacy header overhead", () => { + const compact = encodeEnvelope(envelope, { codec: "deflate" }); + const legacy = `agent-render=v1.deflate.${compact.slice(1)}`; + expect(legacy.length - compact.length).toBe("agent-render=v1.deflate.".length - "d".length); + }); + + it("rejects an unknown compact tag", () => { + const parsed = decodeFragment("#zSOMEPAYLOAD"); + expect(parsed.ok).toBe(false); + if (!parsed.ok) expect(parsed.code).toBe("invalid-format"); + }); +}); diff --git a/tests/e2e/viewer.spec.ts b/tests/e2e/viewer.spec.ts index 86a14ca..c39c748 100644 --- a/tests/e2e/viewer.spec.ts +++ b/tests/e2e/viewer.spec.ts @@ -63,12 +63,15 @@ test("creates, copies, and previews a generated homepage link", async ({ page }) await page.getByRole("button", { name: "Generate link" }).click(); const generatedLink = page.getByLabel("Generated agent-render link"); - await expect(generatedLink).toHaveValue(/#agent-render=/); + // Compact header: the fragment is `#<1-char codec tag>…`, never the legacy `#agent-render=…`. + await expect(generatedLink).not.toHaveValue(/#agent-render=/); + await expect(generatedLink).toHaveValue(/\/#\w/); await expect(page.getByText(/chars$/).first()).toBeVisible(); + const linkValue = await generatedLink.inputValue(); await page.getByRole("button", { name: "Copy link" }).click(); await expect(page.getByRole("button", { name: "Copied" })).toBeVisible(); - await expect.poll(() => page.evaluate(() => window.localStorage.getItem("copied-link"))).toContain("#agent-render="); + await expect.poll(() => page.evaluate(() => window.localStorage.getItem("copied-link"))).toBe(linkValue); await page.getByRole("button", { name: "Preview here" }).click(); await waitForViewerState(page, "artifact"); diff --git a/tests/fragment-arx-selection.test.ts b/tests/fragment-arx-selection.test.ts index 2155beb..7020142 100644 --- a/tests/fragment-arx-selection.test.ts +++ b/tests/fragment-arx-selection.test.ts @@ -2,10 +2,10 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import arx2DictionaryJson from "../public/arx2-dictionary.json"; import arxDictionaryJson from "../public/arx-dictionary.json"; -import { getActiveDictVersion, isBaseBMPEncoded, loadArx2OverlayDictionarySync, loadArxDictionarySync } from "@/lib/payload/arx-codec"; +import { isBaseBMPEncoded, loadArx2OverlayDictionarySync, loadArxDictionarySync } from "@/lib/payload/arx-codec"; import { buildArx2Candidates, buildArx3Candidates } from "@/lib/payload/fragment-arx"; import { encodeEnvelopeAsync } from "@/lib/payload/fragment"; -import { PAYLOAD_FRAGMENT_KEY, type PayloadEnvelope } from "@/lib/payload/schema"; +import { compactTagForCodec, type PayloadEnvelope } from "@/lib/payload/schema"; // Mirror of fragment.ts `computeTransportLength` so the candidate pool here is measured with the // exact metric production auto-selection uses. The arx3 baseBMP candidate intentionally opts out of @@ -39,7 +39,8 @@ function computeTransportLength(value: string): number { } function getArx3PayloadBody(value: string): string { - return value.split(`${PAYLOAD_FRAGMENT_KEY}=v1.arx3.${getActiveDictVersion()}.`)[1] ?? ""; + const tag = compactTagForCodec("arx3"); + return value.startsWith(tag) ? value.slice(tag.length) : ""; } describe("arx3-vs-arx2 selection policy", () => { @@ -83,15 +84,16 @@ describe("arx3-vs-arx2 selection policy", () => { // The arx2 baseBMP payload is byte-identical work measured with percent-escaped transport length, // so its budget is far larger than the arx3 baseBMP visible-length budget that wins. + const arx2Tag = compactTagForCodec("arx2"); const arx2BaseBMP = arx2Candidates.find((candidate) => - isBaseBMPEncoded(candidate.value.split(`${PAYLOAD_FRAGMENT_KEY}=v1.arx2.${getActiveDictVersion()}.`)[1] ?? ""), + isBaseBMPEncoded(candidate.value.startsWith(arx2Tag) ? candidate.value.slice(arx2Tag.length) : ""), ); expect(arx2BaseBMP).toBeDefined(); expect(arx2BaseBMP!.transportLength).toBeGreaterThan(chosen.transportLength); // End-to-end: the public auto encoder commits to the same arx3 baseBMP wire. const fragment = await encodeEnvelopeAsync(reportEnvelope); - expect(fragment).toContain(`v1.arx3.${getActiveDictVersion()}.`); + expect(fragment.startsWith(compactTagForCodec("arx3"))).toBe(true); expect(isBaseBMPEncoded(getArx3PayloadBody(fragment))).toBe(true); }); }); diff --git a/tests/fragment.test.ts b/tests/fragment.test.ts index d01f683..a6896f7 100644 --- a/tests/fragment.test.ts +++ b/tests/fragment.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { decodeFragment, decodeFragmentAsync, encodeEnvelope, encodeEnvelopeAsync } from "@/lib/payload/fragment"; -import type { PayloadEnvelope } from "@/lib/payload/schema"; +import { compactTagForCodec, type PayloadEnvelope } from "@/lib/payload/schema"; import { packEnvelope } from "@/lib/payload/wire-format"; import { arxCompressBMP, getActiveDictVersion } from "@/lib/payload/arx-codec"; @@ -52,7 +52,8 @@ describe("fragment payload transport", () => { ); }); - it("rejects fragments with the wrong key", () => { + it("rejects fragments that are neither a legacy key nor a known compact tag", () => { + // Not `agent-render=...`, so it is read as a compact fragment; "n" is not a codec tag. const parsed = decodeFragment("#not-agent-render=v1.plain.abc"); expect(parsed.ok).toBe(false); @@ -60,7 +61,7 @@ describe("fragment payload transport", () => { return; } - expect(parsed.code).toBe("missing-key"); + expect(parsed.code).toBe("invalid-format"); }); it("rejects invalid json payloads", () => { @@ -89,7 +90,7 @@ describe("fragment payload transport", () => { const hash = encodeEnvelope(repetitiveEnvelope); - expect(hash.startsWith("agent-render=v1.plain.")).toBe(false); + expect(hash.startsWith(compactTagForCodec("plain"))).toBe(false); const parsed = decodeFragment(`#${hash}`); expect(parsed.ok).toBe(true); }); @@ -100,7 +101,7 @@ describe("fragment payload transport", () => { }); const parsed = decodeFragment(`#${hash}`); - expect(hash.startsWith("agent-render=v1.plain.")).toBe(true); + expect(hash.startsWith(compactTagForCodec("plain"))).toBe(true); expect(parsed.ok).toBe(true); }); @@ -110,7 +111,7 @@ describe("fragment payload transport", () => { }); const parsed = decodeFragment(`#${hash}`); - expect(hash.startsWith("agent-render=v1.plain.")).toBe(true); + expect(hash.startsWith(compactTagForCodec("plain"))).toBe(true); expect(parsed.ok).toBe(true); }); diff --git a/tests/link-creator.test.ts b/tests/link-creator.test.ts index 3b726ee..9915e89 100644 --- a/tests/link-creator.test.ts +++ b/tests/link-creator.test.ts @@ -5,6 +5,7 @@ import arxDictionaryJson from "../public/arx-dictionary.json"; import { loadArx2OverlayDictionarySync, loadArxDictionarySync } from "@/lib/payload/arx-codec"; import { decodeFragment, decodeFragmentAsync } from "@/lib/payload/fragment"; import { createDraftEnvelope, createGeneratedArtifactLink, createGeneratedArtifactLinkAsync, type LinkCreatorDraft } from "@/lib/payload/link-creator"; +import { compactTagForCodec } from "@/lib/payload/schema"; describe("link creator payloads", () => { it("builds a single-artifact envelope for pasted markdown", () => { @@ -45,8 +46,8 @@ describe("link creator payloads", () => { const generatedLink = createGeneratedArtifactLink(draft, "https://agent-render.com/"); const parsed = decodeFragment(generatedLink.hash); - expect(generatedLink.url).toContain("#agent-render="); - expect(generatedLink.hash).toContain(`v1.${generatedLink.codec}.`); + expect(generatedLink.url).toContain(`#${compactTagForCodec(generatedLink.codec)}`); + expect(generatedLink.hash.startsWith(`#${compactTagForCodec(generatedLink.codec)}`)).toBe(true); expect(parsed.ok).toBe(true); if (!parsed.ok) { return; @@ -104,8 +105,8 @@ describe("link creator payloads", () => { const generatedLink = await createGeneratedArtifactLinkAsync(draft, "https://agent-render.com/"); const parsed = await decodeFragmentAsync(generatedLink.hash); - expect(generatedLink.hash).toContain("#agent-render=v1.arx2."); - expect(generatedLink.url).toContain("#agent-render=v1.arx2."); + expect(generatedLink.hash.startsWith(`#${compactTagForCodec("arx2")}`)).toBe(true); + expect(generatedLink.url).toContain(`#${compactTagForCodec("arx2")}`); expect(generatedLink.codec).toBe("arx2"); expect(generatedLink.envelope.codec).toBe("plain"); expect(parsed.ok).toBe(true); @@ -129,8 +130,8 @@ describe("link creator payloads", () => { const parsed = await decodeFragmentAsync(generatedLink.hash); expect(generatedLink.codec).toBe("arx3"); - expect(generatedLink.hash).toContain("#agent-render=v1.arx3.1."); - expect(generatedLink.url).toContain("#agent-render=v1.arx3.1."); + expect(generatedLink.hash.startsWith(`#${compactTagForCodec("arx3")}`)).toBe(true); + expect(generatedLink.url).toContain(`#${compactTagForCodec("arx3")}`); expect(generatedLink.fragmentLength).toBeLessThan(1900); expect(parsed.ok).toBe(true); }); diff --git a/tests/sample-link-data.test.ts b/tests/sample-link-data.test.ts index 7c5d785..f9d4c89 100644 --- a/tests/sample-link-data.test.ts +++ b/tests/sample-link-data.test.ts @@ -3,6 +3,7 @@ import { sampleLinkCards } from "@/components/home/sample-link-data"; import { loadArx2OverlayDictionarySync, loadArxDictionarySync } from "@/lib/payload/arx-codec"; import { sampleEnvelopes, sampleLinks } from "@/lib/payload/examples"; import { decodeFragmentAsync } from "@/lib/payload/fragment"; +import { compactTagForCodec } from "@/lib/payload/schema"; import arx2DictionaryJson from "../public/arx2-dictionary.json"; import arxDictionaryJson from "../public/arx-dictionary.json"; @@ -41,7 +42,7 @@ describe("homepage sample link data", () => { const sample = sampleLinkCards.find((card) => card.title === "arx showcase"); - expect(sample?.hash).toContain("#agent-render=v1.arx3.1."); + expect(sample?.hash?.startsWith(`#${compactTagForCodec("arx3")}`)).toBe(true); expect(sample?.fragmentLength).toBeLessThan(1900); const parsed = await decodeFragmentAsync(sample?.hash ?? "");